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(); 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(); 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 */ } } }