import { StorageMgr, globalStorageMgr } from '../common/StorageMgr'; import { STORAGE_KEY } from '../common/Constants'; /** * UIFlowMgr — scene-flow state machine (task 9.2, req 12.7-12.8, 13.1). * * It does **not** perform `director.loadScene()` itself; the Cocos view layer * subscribes to `onSceneEnter` and performs the actual scene swap. Keeping * the flow engine-agnostic makes it trivial to Jest-test every boot path, * every settlement path, and the new story-intro gate (req 19.x). * * Decision D-4 / req 13.1 guardrail: * The `showDifficultyPicker()` action simply does not exist; and * `availableSettingsEntries()` purposefully omits it. A future contributor * who tries to add a "difficulty" key will hit a TypeScript compile-error * because the union `SettingsKey` is exhaustive. */ export type SceneId = | 'boot' | 'story_intro' | 'main_menu' | 'level_select' | 'gameplay' | 'settlement' | 'settings'; export type SettingsKey = | 'audio_volume' | 'layout_customisation' | 'replay_tutorial' | 'replay_story_intro'; export interface ISceneEnter { scene: SceneId; /** Optional payload (e.g. `{ levelId: '1-1' }` for gameplay). */ payload?: Record; } export interface IUIFlowCallbacks { onSceneEnter?: (ev: ISceneEnter) => void; } export class UIFlowMgr { private current: SceneId = 'boot'; constructor( private readonly storage: StorageMgr = globalStorageMgr, private readonly callbacks: IUIFlowCallbacks = {} ) {} public get currentScene(): SceneId { return this.current; } /** Invoked by `GameBoot.start()` once engine is ready. */ public onBoot(): void { if (this.storage.get(STORAGE_KEY.StoryIntroSeen, false)) { this.enter('main_menu'); } else { this.enter('story_intro'); } } /** Called by StorySceneCtrl.onFinished(). */ public onStoryFinished(): void { this.enter('gameplay', { levelId: '1-1' }); } /** Main menu → level select. */ public onPressStartGame(): void { // First-time "Start Game" may jump through the story again if we // ever reset; otherwise go to level select. if (!this.storage.get(STORAGE_KEY.StoryIntroSeen, false)) { this.enter('story_intro'); } else { this.enter('level_select'); } } public onPickLevel(levelId: string): void { this.enter('gameplay', { levelId }); } public onOpenSettings(): void { this.enter('settings'); } public onCloseSettings(): void { this.enter('main_menu'); } public onLevelCleared(nextLevelId: string | null): void { if (nextLevelId) { this.enter('settlement', { nextLevelId }); } else { // After final boss settlement, back to the main menu. this.enter('settlement', { isChapterEnd: true }); } } public onSettlementContinue(nextLevelId?: string): void { if (nextLevelId) this.enter('gameplay', { levelId: nextLevelId }); else this.enter('main_menu'); } public onPlayerDied(currentLevelId: string): void { this.enter('settlement', { levelId: currentLevelId, dead: true }); } /** * Exhaustive list of settings entries available in the Settings scene. * Purposefully omits any difficulty-selection entry (req 13.1). */ public availableSettingsEntries(): ReadonlyArray { return ['audio_volume', 'layout_customisation', 'replay_tutorial', 'replay_story_intro']; } // ----------------------------------------------------------------- private enter(scene: SceneId, payload?: Record): void { this.current = scene; this.callbacks.onSceneEnter?.({ scene, payload }); } }