Files
2026-05-06 08:17:32 +08:00

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);
}
}