126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
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<string, unknown>;
|
|
}
|
|
|
|
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<boolean>(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<boolean>(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<SettingsKey> {
|
|
return ['audio_volume', 'layout_customisation', 'replay_tutorial', 'replay_story_intro'];
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
|
|
private enter(scene: SceneId, payload?: Record<string, unknown>): void {
|
|
this.current = scene;
|
|
this.callbacks.onSceneEnter?.({ scene, payload });
|
|
}
|
|
}
|