314 lines
13 KiB
TypeScript
314 lines
13 KiB
TypeScript
export type DailyTaskEvent = {
|
|
metric: string;
|
|
amount?: number;
|
|
width?: number;
|
|
height?: number;
|
|
colorId?: number;
|
|
propType?: string;
|
|
};
|
|
|
|
/** 新版日任务统一进度入口。任务仍按现有架构由客户端累计并批量上传。 */
|
|
export default class DailyTaskProgressTracker {
|
|
private static saveTimer: any = null;
|
|
private static onlineStartedAt = 0;
|
|
private static onlineTimer: any = null;
|
|
private static pendingOnlineSeconds = 0;
|
|
private static unsavedOnlineRecovery: { dayKey: string, values: { [taskId: string]: number } } = null;
|
|
private static firstAttemptValid = true;
|
|
private static usedPropInLevel = false;
|
|
private static pendingClearEvents: { [key: string]: DailyTaskEvent } = {};
|
|
private static debugPaused = false;
|
|
private static waitingForPeriod = false;
|
|
private static pendingPeriodEvents: DailyTaskEvent[] = [];
|
|
|
|
static setDebugPaused(paused: boolean): void {
|
|
this.debugPaused = paused;
|
|
if (this.onlineStartedAt) this.onlineStartedAt = Date.now();
|
|
if (paused) this.pendingClearEvents = {};
|
|
}
|
|
|
|
static changePeriod(discardPending = false): void {
|
|
this.unsavedOnlineRecovery = null;
|
|
if (discardPending) {
|
|
this.pendingOnlineSeconds = 0;
|
|
this.pendingPeriodEvents = [];
|
|
this.waitingForPeriod = false;
|
|
}
|
|
// 等待新周期时已清理过旧消除事件,保留之后新产生的事件。
|
|
if (!this.waitingForPeriod) this.pendingClearEvents = {};
|
|
}
|
|
|
|
static confirmSaved(data: any): void {
|
|
const recovery = this.unsavedOnlineRecovery;
|
|
if (!recovery || data.dayKey !== recovery.dayKey) return;
|
|
if (Object.keys(recovery.values).every(id => {
|
|
const task = data.dailyTasks.find(task => task.taskId === id);
|
|
return task && (task.state === 2 || Number(task.value) >= recovery.values[id]);
|
|
})) this.unsavedOnlineRecovery = null;
|
|
}
|
|
|
|
static beginLevel(): void {
|
|
this.firstAttemptValid = true;
|
|
this.usedPropInLevel = false;
|
|
this.pendingClearEvents = {};
|
|
}
|
|
|
|
static invalidateAttempt(): void {
|
|
this.firstAttemptValid = false;
|
|
}
|
|
|
|
static recordPropUsed(propType: string): void {
|
|
this.usedPropInLevel = true;
|
|
this.record({ metric: "use_prop" });
|
|
this.record({ metric: "use_specific_prop", propType });
|
|
}
|
|
|
|
static recordShare(): void {
|
|
this.record({ metric: "share" });
|
|
this.flushNow();
|
|
}
|
|
|
|
static recordLevelWon(): void {
|
|
this.commitClearProgress();
|
|
this.record({ metric: "pass_levels" });
|
|
this.record({ metric: "win_streak" });
|
|
if (this.firstAttemptValid) this.record({ metric: "first_try_pass" });
|
|
if (!this.usedPropInLevel) this.record({ metric: "no_prop_pass" });
|
|
}
|
|
|
|
/** 任务结构版本的统一入口;升级协议时集中调整此处的兼容规则。 */
|
|
static isValidTaskData(data: any): boolean {
|
|
return !!data && data.version === 2 && Array.isArray(data.dailyTasks);
|
|
}
|
|
|
|
static setTaskData(data: any): void {
|
|
if (!this.isValidTaskData(data)) return;
|
|
let changed = false;
|
|
const recovery = this.unsavedOnlineRecovery;
|
|
if (recovery && recovery.dayKey === data.dayKey) {
|
|
// 补计保存成功前,后续任务读取的旧响应不能撤销本次补计。
|
|
data.dailyTasks.forEach((task: any) => {
|
|
const value = Math.min(Number(task.target) || 0, recovery.values[task.taskId] || 0);
|
|
if (task.metric !== "online_seconds" || task.state === 2 || value <= (Number(task.value) || 0)) return;
|
|
task.value = value;
|
|
if (value >= task.target) task.state = 1;
|
|
changed = true;
|
|
});
|
|
} else if (recovery) {
|
|
this.unsavedOnlineRecovery = null;
|
|
}
|
|
cc.fx.GameConfig.GM_INFO.tasks = data;
|
|
if (this.isExpired()) return;
|
|
if (this.waitingForPeriod) {
|
|
this.waitingForPeriod = false;
|
|
const events = this.pendingPeriodEvents;
|
|
this.pendingPeriodEvents = [];
|
|
events.forEach(event => { if (this.applyEvent(data, event)) changed = true; });
|
|
}
|
|
// 只补计加载前已经结算的整秒;先清空,重复接收同一份任务数据也不会重复补计。
|
|
const seconds = this.pendingOnlineSeconds;
|
|
this.pendingOnlineSeconds = 0;
|
|
if (seconds > 0 && this.applyEvent(data, { metric: "online_seconds", amount: seconds })) {
|
|
const values = {};
|
|
data.dailyTasks.forEach((task: any) => {
|
|
if (task.metric === "online_seconds" && task.state !== 2) values[task.taskId] = task.value;
|
|
});
|
|
this.unsavedOnlineRecovery = { dayKey: data.dayKey, values };
|
|
changed = true;
|
|
}
|
|
if (changed) this.scheduleSave();
|
|
}
|
|
|
|
static getTaskData(): any {
|
|
const tasks: any = cc.fx.GameConfig.GM_INFO.tasks;
|
|
return this.isValidTaskData(tasks) ? tasks : null;
|
|
}
|
|
|
|
static record(event: DailyTaskEvent): void {
|
|
if (this.debugPaused) return;
|
|
if (cc.fx.GameConfig.GM_INFO.otherLevel != 0 && this.isLevelMetric(event.metric)) return;
|
|
if (this.isExpired()) {
|
|
this.waitForPeriod();
|
|
if (this.isClearMetric(event.metric)) this.queueClearProgress(event);
|
|
else this.pendingPeriodEvents.push({ ...event });
|
|
cc.fx.GameTool.getDailyQuestsInfo(() => {});
|
|
return;
|
|
}
|
|
const data = this.getTaskData();
|
|
if (!data) {
|
|
if (event.metric === "online_seconds") {
|
|
this.pendingOnlineSeconds += Math.max(0, Number(event.amount == null ? 1 : event.amount) || 0);
|
|
}
|
|
return;
|
|
}
|
|
if (this.isClearMetric(event.metric)) {
|
|
if (data.dailyTasks.some((task: any) => task.state !== 2 && this.matches(task, event))) {
|
|
this.queueClearProgress(event);
|
|
}
|
|
return;
|
|
}
|
|
if (this.applyEvent(data, event)) this.scheduleSave();
|
|
}
|
|
|
|
private static applyEvent(data: any, event: DailyTaskEvent): boolean {
|
|
let changed = false;
|
|
data.dailyTasks.forEach((task: any) => {
|
|
if (task.state === 2 || !this.matches(task, event)) return;
|
|
const amount = Math.max(0, Number(event.amount == null ? 1 : event.amount) || 0);
|
|
const next = Math.min(Number(task.target) || 0, (Number(task.value) || 0) + amount);
|
|
if (next === task.value) return;
|
|
task.value = next;
|
|
if (task.value >= task.target) {
|
|
task.state = 1;
|
|
if (cc.fx.GameTool && cc.fx.GameTool.shushu_Track) {
|
|
cc.fx.GameTool.shushu_Track("daily_task", { id: task.taskId, status: "complete" });
|
|
}
|
|
}
|
|
changed = true;
|
|
});
|
|
return changed;
|
|
}
|
|
|
|
static resetWinStreak(): void {
|
|
if (this.debugPaused) return;
|
|
this.pendingClearEvents = {};
|
|
const data = this.getTaskData();
|
|
if (!data) return;
|
|
let changed = false;
|
|
data.dailyTasks.forEach((task: any) => {
|
|
if (task.metric === "win_streak" && task.state !== 2 && task.value !== 0) {
|
|
task.value = 0;
|
|
task.state = 0;
|
|
if (cc.fx.GameTool.markDailyTaskReset) cc.fx.GameTool.markDailyTaskReset(task.taskId);
|
|
changed = true;
|
|
}
|
|
});
|
|
if (changed) this.scheduleSave();
|
|
}
|
|
|
|
private static queueClearProgress(event: DailyTaskEvent): void {
|
|
const key = `${event.metric}:${event.width || ""}:${event.height || ""}:${event.colorId || ""}`;
|
|
const amount = Math.max(0, Number(event.amount == null ? 1 : event.amount) || 0);
|
|
const pending = this.pendingClearEvents[key];
|
|
if (pending) pending.amount = (Number(pending.amount) || 0) + amount;
|
|
else this.pendingClearEvents[key] = { ...event, amount };
|
|
}
|
|
|
|
private static commitClearProgress(): void {
|
|
if (this.debugPaused) return;
|
|
if (this.isExpired()) {
|
|
this.waitForPeriod();
|
|
this.pendingPeriodEvents.push(...Object.keys(this.pendingClearEvents).map(key => this.pendingClearEvents[key]));
|
|
this.pendingClearEvents = {};
|
|
cc.fx.GameTool.getDailyQuestsInfo(() => {});
|
|
return;
|
|
}
|
|
const data = this.getTaskData();
|
|
const events = Object.keys(this.pendingClearEvents).map(key => this.pendingClearEvents[key]);
|
|
this.pendingClearEvents = {};
|
|
if (!data) return;
|
|
let changed = false;
|
|
events.forEach(event => {
|
|
if (this.applyEvent(data, event)) changed = true;
|
|
});
|
|
if (changed) this.scheduleSave();
|
|
}
|
|
|
|
static startOnline(): void {
|
|
if (this.onlineStartedAt) return;
|
|
this.onlineStartedAt = Date.now();
|
|
if (!this.onlineTimer) {
|
|
this.onlineTimer = setInterval(() => this.flushOnline(), 30000);
|
|
}
|
|
}
|
|
|
|
static stopOnline(): void {
|
|
this.flushOnline();
|
|
if (this.onlineTimer) clearInterval(this.onlineTimer);
|
|
this.onlineTimer = null;
|
|
this.onlineStartedAt = 0;
|
|
}
|
|
|
|
static flushNow(save = true): void {
|
|
if (this.debugPaused) return;
|
|
this.flushOnline();
|
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
this.saveTimer = null;
|
|
if (save) this.save();
|
|
}
|
|
|
|
private static flushOnline(): void {
|
|
if (!this.onlineStartedAt) return;
|
|
const now = Date.now();
|
|
let seconds = Math.floor((now - this.onlineStartedAt) / 1000);
|
|
if (seconds <= 0) return;
|
|
this.onlineStartedAt += seconds * 1000;
|
|
if (this.isExpired()) {
|
|
// 旧日期已经结束的时间不能补进新日期;只缓冲零点之后的部分。
|
|
const data = this.getTaskData();
|
|
const serverNow = cc.fx.GameTool.getDailyTaskServerTime();
|
|
const elapsedAfterReset = Math.max(0, Math.floor((serverNow - Number(data.dayEndsAt)) / 1000));
|
|
seconds = Math.min(seconds, elapsedAfterReset);
|
|
}
|
|
if (seconds <= 0) return;
|
|
this.record({ metric: "online_seconds", amount: seconds });
|
|
}
|
|
|
|
private static matches(task: any, event: DailyTaskEvent): boolean {
|
|
if (task.metric !== event.metric) return false;
|
|
const params = task.params || {};
|
|
if (event.metric === "clear_size") {
|
|
const same = Number(event.width) === Number(params.width) && Number(event.height) === Number(params.height);
|
|
const rotated = Number(event.width) === Number(params.height) && Number(event.height) === Number(params.width);
|
|
return same || rotated;
|
|
}
|
|
if (event.metric === "clear_color") return Number(event.colorId) === Number(params.colorId);
|
|
if (event.metric === "use_specific_prop") return event.propType === params.propType;
|
|
return true;
|
|
}
|
|
|
|
private static isLevelMetric(metric: string): boolean {
|
|
return ["clear_blocks", "clear_size", "clear_color", "pass_levels", "no_prop_pass", "first_try_pass", "win_streak"].indexOf(metric) >= 0;
|
|
}
|
|
|
|
private static isClearMetric(metric: string): boolean {
|
|
return ["clear_blocks", "clear_size", "clear_color"].indexOf(metric) >= 0;
|
|
}
|
|
|
|
private static scheduleSave(): void {
|
|
if (cc.fx.GameTool && cc.fx.GameTool.markDailyTasksDirty) cc.fx.GameTool.markDailyTasksDirty();
|
|
if (this.saveTimer) return;
|
|
this.saveTimer = setTimeout(() => {
|
|
this.saveTimer = null;
|
|
this.save();
|
|
}, 1000);
|
|
}
|
|
|
|
private static save(): void {
|
|
if (this.debugPaused) return;
|
|
if (!this.getTaskData() || !cc.fx.GameTool || !cc.fx.GameTool.setDailyQuestInfo) return;
|
|
const recovery = this.unsavedOnlineRecovery;
|
|
cc.fx.GameTool.setDailyQuestInfo((response: any) => {
|
|
if (response && response.code === 1) {
|
|
if (this.unsavedOnlineRecovery === recovery) {
|
|
this.unsavedOnlineRecovery = null;
|
|
} else if (this.unsavedOnlineRecovery) {
|
|
// 同步期间又出现补计时,保留这批尚未确认的时间。
|
|
const data = this.getTaskData();
|
|
if (data && data.dayKey === this.unsavedOnlineRecovery.dayKey) this.setTaskData(data);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private static isExpired(): boolean {
|
|
return !!(cc.fx.GameTool && cc.fx.GameTool.isDailyTaskCacheExpired && cc.fx.GameTool.isDailyTaskCacheExpired());
|
|
}
|
|
|
|
private static waitForPeriod(): void {
|
|
if (this.waitingForPeriod) return;
|
|
this.waitingForPeriod = true;
|
|
this.pendingClearEvents = {};
|
|
}
|
|
}
|