119 lines
4.4 KiB
TypeScript
119 lines
4.4 KiB
TypeScript
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<string[]>(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<string[]>(STORAGE_KEY.TutorialDone, []);
|
|
if (!current.includes(levelId)) current.push(levelId);
|
|
this.storage.set(STORAGE_KEY.TutorialDone, current);
|
|
}
|
|
}
|