You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
10 KiB
270 lines
10 KiB
import { EventTarget, sys } from 'cc';
|
|
import { GEvent } from '../event/GEvent';
|
|
|
|
const STORAGE_KEY = 'GuideManger_v1_completed_groups';
|
|
|
|
/** 当前引导步快照(发给 UI 绑定节点、文案) */
|
|
export interface IGuideSessionState {
|
|
group: number;
|
|
step: number;
|
|
desc: string;
|
|
/** 本组总步数 */
|
|
totalSteps: number;
|
|
/** 是否为当前组最后一步 */
|
|
isLastStep: boolean;
|
|
}
|
|
|
|
export interface IGuideData {
|
|
group: number;
|
|
step: number;
|
|
desc: string;
|
|
}
|
|
|
|
export const GuideData: IGuideData[] = [
|
|
// --- 组1:引导玩家完成「武器升级」闭环(剧情 guide2,homeGuideIndex 1→2→3→4)---
|
|
{ group: 1, step: 1, desc: '点击主界面底部武器页签,进入武器界面' },
|
|
{ group: 1, step: 2, desc: '在武器列表中点击目标武器卡片(如彩虹飞弹)' },
|
|
{ group: 1, step: 3, desc: '在武器升级界面按引导完成升级操作' },
|
|
{ group: 1, step: 4, desc: '返回主界面,点击底部战斗页签回到章节主页' },
|
|
{ group: 1, step: 5, desc: '在章节主页点击开始战斗(手指提示)' },
|
|
|
|
// --- 组2:引导玩家完成「角色升级」闭环(剧情 guide3)---
|
|
{ group: 2, step: 1, desc: '点击主界面底部角色页签' },
|
|
{ group: 2, step: 2, desc: '在角色界面点击属性升级入口并完成升级' },
|
|
|
|
// --- 组3:引导进入丧尸外围玩法(剧情 guide4)---
|
|
{ group: 3, step: 1, desc: '点击主页丧尸外围入口按钮,进入对应玩法' },
|
|
|
|
// --- 组4:引导玩家切换并确认「地狱难度」(剧情 guide5 + 选难度界面)---
|
|
{ group: 4, step: 1, desc: '点击章节标题,打开难度选择界面' },
|
|
{ group: 4, step: 2, desc: '在选难度界面切换到地狱难度' },
|
|
{ group: 4, step: 3, desc: '点击确定,完成难度选择并关闭界面' },
|
|
|
|
// --- 组5:引导「宠物与抽卡」闭环(剧情 guide6 与后续 homeGuideIndex 10→11→12→13)---
|
|
{ group: 5, step: 1, desc: '点击主界面底部角色页签(宠物线第二段)' },
|
|
{ group: 5, step: 2, desc: '在角色界面点击宠物入口,进入宠物页' },
|
|
{ group: 5, step: 3, desc: '在宠物界面点击抽一次,完成抽取引导' },
|
|
|
|
// --- 组6:剧情后礼包与开战提示(剧情 guide1:礼包队列 + 开始键手指)---
|
|
{ group: 6, step: 1, desc: '关闭或处理超值礼包弹窗(按产品流程)' },
|
|
{ group: 6, step: 2, desc: '在章节主页对开始战斗显示手指提示后,点击开战' },
|
|
|
|
// --- 组7:第一章关卡内「操作教学」闭环(普通难度第1章战斗 UI 引导链)---
|
|
{ group: 7, step: 1, desc: '阅读大波怪预警后,进入后续战斗流程' },
|
|
{ group: 7, step: 2, desc: '按引导学习移动角色' },
|
|
{ group: 7, step: 3, desc: '使用功能按钮延长怪物路线' },
|
|
{ group: 7, step: 4, desc: '在地图上摆放障碍物(含多点摆放)' },
|
|
{ group: 7, step: 5, desc: '点击保存,完成路线编辑' },
|
|
{ group: 7, step: 6, desc: '领取榴莲武器支援并继续战斗' },
|
|
|
|
// --- 组8:首次「技能三选一」闭环(刷新 + 选卡)---
|
|
{ group: 8, step: 1, desc: '在技能选择界面点击刷新,获取技能列表' },
|
|
{ group: 8, step: 2, desc: '点击选择一张技能卡并确认' },
|
|
|
|
// --- 组9:失败界面「免费复活」引导(单次)---
|
|
{ group: 9, step: 1, desc: '在失败界面点击免费复活' },
|
|
|
|
// --- 组10:战斗内「转盘再抽一次」引导(单次)---
|
|
{ group: 10, step: 1, desc: '在转盘界面点击再抽一次' },
|
|
|
|
// --- 组11:抽卡副本「转盘模式」操作闭环(抽奖→铲子→障碍→无银币开局)---
|
|
{ group: 11, step: 1, desc: '在抽卡转盘界面完成首次抽奖操作' },
|
|
{ group: 11, step: 2, desc: '按引导摆放铲子' },
|
|
{ group: 11, step: 3, desc: '按引导摆放障碍物' },
|
|
{ group: 11, step: 4, desc: '在无银币提示下按引导点击开始游戏' },
|
|
];
|
|
|
|
export interface GuideMangerInitOptions {
|
|
/** 从 localStorage 恢复已完成组(默认 true) */
|
|
restoreFromStorage?: boolean;
|
|
}
|
|
|
|
export interface StartGroupOptions {
|
|
/** 若该组曾标记完成,仍重新开始(默认 false) */
|
|
force?: boolean;
|
|
/** 已有进行中的组时是否先终止(默认 true) */
|
|
abortPrevious?: boolean;
|
|
}
|
|
|
|
/**
|
|
* 引导模块:以 GuideData 为唯一步骤表,管理「组 → 多步」会话。
|
|
* UI 监听 GEvent.GuideStepEnter 后自行绑定节点;业务在玩家完成当前步时调用 next()。
|
|
*/
|
|
export class GuideManger extends EventTarget {
|
|
private static _ins: GuideManger | null = null;
|
|
static get Ins(): GuideManger {
|
|
if (!this._ins) this._ins = new GuideManger();
|
|
return this._ins;
|
|
}
|
|
|
|
private _completed = new Set<number>();
|
|
private _activeGroup: number | null = null;
|
|
/** 当前所在步(与 GuideData.step 一致,从 1 开始) */
|
|
private _currentStep = 0;
|
|
private _inited = false;
|
|
|
|
/** 模块初始化(可重复调用,只加载一次存档) */
|
|
init(opts?: GuideMangerInitOptions): void {
|
|
if (this._inited) return;
|
|
this._inited = true;
|
|
if (opts?.restoreFromStorage !== false) {
|
|
this._loadCompletedFromStorage();
|
|
}
|
|
}
|
|
|
|
isSessionActive(): boolean {
|
|
return this._activeGroup !== null;
|
|
}
|
|
|
|
/** 当前步快照;无进行中会话时返回 null */
|
|
getCurrentStep(): IGuideSessionState | null {
|
|
if (this._activeGroup === null) return null;
|
|
const row = this._findRow(this._activeGroup, this._currentStep);
|
|
if (!row) return null;
|
|
const total = this._getTotalSteps(this._activeGroup);
|
|
return this._toState(row, total);
|
|
}
|
|
|
|
isGroupCompleted(groupId: number): boolean {
|
|
return this._completed.has(groupId);
|
|
}
|
|
|
|
getStepsOfGroup(groupId: number): IGuideData[] {
|
|
return GuideData.filter((r) => r.group === groupId).sort((a, b) => a.step - b.step);
|
|
}
|
|
|
|
getAllGroupIds(): number[] {
|
|
const s = new Set<number>();
|
|
for (const r of GuideData) s.add(r.group);
|
|
return Array.from(s).sort((a, b) => a - b);
|
|
}
|
|
|
|
/**
|
|
* 开始某一组的引导:进入该组第 1 步并发出 GuideStepEnter。
|
|
*/
|
|
startGroup(groupId: number, opts?: StartGroupOptions): boolean {
|
|
this.init();
|
|
const total = this._getTotalSteps(groupId);
|
|
if (total === 0) return false;
|
|
if (!opts?.force && this._completed.has(groupId)) return false;
|
|
if (this._activeGroup !== null) {
|
|
if (opts?.abortPrevious === false) return false;
|
|
this.finish(true);
|
|
}
|
|
this._activeGroup = groupId;
|
|
this._currentStep = 1;
|
|
this._emitStepEnter();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 标记「当前步」已完成:进入下一步;若当前步已是本组最后一步,则整组完成并持久化。
|
|
*/
|
|
completeCurrentStep(): boolean {
|
|
if (this._activeGroup === null) return false;
|
|
const total = this._getTotalSteps(this._activeGroup);
|
|
if (this._currentStep < 1 || this._currentStep > total) return false;
|
|
|
|
if (this._currentStep < total) {
|
|
this._currentStep += 1;
|
|
this._emitStepEnter();
|
|
return true;
|
|
}
|
|
this._finishGroupSuccess();
|
|
return true;
|
|
}
|
|
|
|
next(): boolean {
|
|
return this.completeCurrentStep();
|
|
}
|
|
|
|
/**
|
|
* 终止当前会话(不标记整组完成)。
|
|
* @param silent 为 true 时不派发 GuideSessionAbort(用于被 startGroup 抢占)
|
|
*/
|
|
finish(silent = false): void {
|
|
if (this._activeGroup === null) return;
|
|
const g = this._activeGroup;
|
|
const s = this._currentStep;
|
|
this._activeGroup = null;
|
|
this._currentStep = 0;
|
|
if (!silent) {
|
|
const payload = { group: g, step: s };
|
|
this.emit(GEvent.GuideSessionAbort, payload);
|
|
GEvent.Ins.emit(GEvent.GuideSessionAbort, payload);
|
|
}
|
|
}
|
|
|
|
resetGroupCompletion(groupId: number): void {
|
|
this._completed.delete(groupId);
|
|
this._saveCompletedToStorage();
|
|
}
|
|
|
|
resetAllCompletion(): void {
|
|
this._completed.clear();
|
|
this._saveCompletedToStorage();
|
|
}
|
|
|
|
markGroupCompleted(groupId: number): void {
|
|
this._completed.add(groupId);
|
|
this._saveCompletedToStorage();
|
|
}
|
|
|
|
private _finishGroupSuccess(): void {
|
|
const g = this._activeGroup!;
|
|
this._completed.add(g);
|
|
this._saveCompletedToStorage();
|
|
const payload = { group: g };
|
|
this.emit(GEvent.GuideGroupComplete, payload);
|
|
GEvent.Ins.emit(GEvent.GuideGroupComplete, payload);
|
|
this._activeGroup = null;
|
|
this._currentStep = 0;
|
|
}
|
|
|
|
private _emitStepEnter(): void {
|
|
const snap = this.getCurrentStep();
|
|
if (!snap) return;
|
|
this.emit(GEvent.GuideStepEnter, snap);
|
|
GEvent.Ins.emit(GEvent.GuideStepEnter, snap);
|
|
}
|
|
|
|
private _findRow(groupId: number, step: number): IGuideData | null {
|
|
return GuideData.find((r) => r.group === groupId && r.step === step) ?? null;
|
|
}
|
|
|
|
private _getTotalSteps(groupId: number): number {
|
|
const steps = this.getStepsOfGroup(groupId);
|
|
if (steps.length === 0) return 0;
|
|
return Math.max(...steps.map((s) => s.step));
|
|
}
|
|
|
|
private _toState(row: IGuideData, totalSteps: number): IGuideSessionState {
|
|
return {
|
|
group: row.group,
|
|
step: row.step,
|
|
desc: row.desc,
|
|
totalSteps,
|
|
isLastStep: row.step === totalSteps,
|
|
};
|
|
}
|
|
|
|
private _loadCompletedFromStorage(): void {
|
|
try {
|
|
const raw = sys.localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return;
|
|
const arr = JSON.parse(raw) as number[];
|
|
if (!Array.isArray(arr)) return;
|
|
this._completed = new Set(arr.filter((n) => typeof n === 'number'));
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
private _saveCompletedToStorage(): void {
|
|
try {
|
|
sys.localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(this._completed)));
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
|