消消方块阵换皮表情
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

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 */
}
}
}