import { StorageMgr, globalStorageMgr } from '../common/StorageMgr'; import { STORAGE_KEY } from '../common/Constants'; /** * Tutorial manager (req 11.1-11.5, task 9.3). * * Pre-defined tutorial sequences for levels 1-1, 1-2, 1-3. Each step has an * ID the view layer uses to drive highlight-arrows; the step is "completed" * when the player performs the action, which the view layer signals via * `reportAction()`. */ export interface ITutorialStep { id: string; /** Human-readable hint (displayed by the view layer). */ hint: string; /** Action id the player must perform to advance. */ requiredAction: string; } export interface ITutorialSequence { levelId: string; steps: ITutorialStep[]; } /** Built-in tutorials for Chapter 1 (req 11.1-11.3). */ export const BUILTIN_TUTORIALS: ITutorialSequence[] = [ { levelId: '1-1', steps: [ { id: 'attack', hint: '点击右下的手里剑按钮', requiredAction: 'fire_shuriken' }, { id: 'joystick', hint: '拖动左下摇杆移动', requiredAction: 'move' }, { id: 'jump', hint: '点击跳跃按钮', requiredAction: 'jump' }, ], }, { levelId: '1-2', steps: [ { id: 'parabolic', hint: '摇杆 45° 并跳跃 — 抛物线跳跃', requiredAction: 'parabolic_jump' }, { id: 'exclusive', hint: '两个攻击按钮互斥,选一个用', requiredAction: 'attack_switch' }, { id: 'parry', hint: '忍者刀可以格挡敌人刀剑', requiredAction: 'parry' }, { id: 'combo', hint: '跳跃中同时攻击', requiredAction: 'jump_attack' }, { id: 'auto_upgrade', hint: '拾取水晶玉自动强化', requiredAction: 'pickup_crystal' }, ], }, { levelId: '1-3', steps: [ { id: 'butterfly', hint: '先击中 BOSS 身旁的蝴蝶', requiredAction: 'hit_butterfly' }, { id: 'boss_identify', hint: '识别 BOSS 攻击模式', requiredAction: 'dodge_boss_attack' }, { id: 'one_shot', hint: '显形后一击必杀', requiredAction: 'hit_revealed_boss' }, ], }, ]; export class TutorialMgr { private currentLevelId: string | null = null; private currentStepIndex = 0; constructor( private readonly storage: StorageMgr = globalStorageMgr, private readonly sequences: ITutorialSequence[] = BUILTIN_TUTORIALS ) {} /** Start the tutorial for `levelId` if not already completed. */ public maybeStart(levelId: string): ITutorialStep | null { if (this.isCompleted(levelId)) return null; const seq = this.sequences.find((s) => s.levelId === levelId); if (!seq) return null; this.currentLevelId = levelId; this.currentStepIndex = 0; return seq.steps[0]; } /** Called by gameplay whenever the player performs an action. */ public reportAction(action: string): ITutorialStep | 'finished' | 'no_op' { if (!this.currentLevelId) return 'no_op'; const seq = this.sequences.find((s) => s.levelId === this.currentLevelId); if (!seq) return 'no_op'; const current = seq.steps[this.currentStepIndex]; if (action !== current.requiredAction) return 'no_op'; this.currentStepIndex++; if (this.currentStepIndex >= seq.steps.length) { this.markCompleted(this.currentLevelId); this.currentLevelId = null; this.currentStepIndex = 0; return 'finished'; } return seq.steps[this.currentStepIndex]; } public isCompleted(levelId: string): boolean { const completed = this.storage.get(STORAGE_KEY.TutorialDone, []); return completed.includes(levelId); } public resetAll(): void { this.storage.remove(STORAGE_KEY.TutorialDone); this.currentLevelId = null; this.currentStepIndex = 0; } public get isActive(): boolean { return this.currentLevelId !== null; } public currentStep(): ITutorialStep | null { if (!this.currentLevelId) return null; const seq = this.sequences.find((s) => s.levelId === this.currentLevelId); return seq?.steps[this.currentStepIndex] ?? null; } private markCompleted(levelId: string): void { const current = this.storage.get(STORAGE_KEY.TutorialDone, []); if (!current.includes(levelId)) current.push(levelId); this.storage.set(STORAGE_KEY.TutorialDone, current); } }