171 lines
6.6 KiB
TypeScript
171 lines
6.6 KiB
TypeScript
import {
|
|
TASK_COLOR_IDS,
|
|
DAILY_TASK_POOLS,
|
|
WEEKLY_REWARDS,
|
|
} from "@/dailyTaskConfig";
|
|
import type { DailyTask, DailyWeeklyTaskData } from "@/dailyTaskConfig";
|
|
|
|
const DAY_MS = 86400000;
|
|
const SHANGHAI_OFFSET_MS = 8 * 3600000;
|
|
|
|
export function getDayKey(nowMs = Date.now()): string {
|
|
return new Date(nowMs + SHANGHAI_OFFSET_MS).toISOString().slice(0, 10);
|
|
}
|
|
|
|
export function getDayEndsAt(nowMs = Date.now()): number {
|
|
return Date.parse(`${getDayKey(nowMs)}T00:00:00+08:00`) + DAY_MS;
|
|
}
|
|
|
|
export function getWeekKey(nowMs = Date.now()): string {
|
|
const shifted = new Date(nowMs + SHANGHAI_OFFSET_MS);
|
|
const day = shifted.getUTCDay() || 7;
|
|
return new Date(shifted.getTime() - (day - 1) * DAY_MS).toISOString().slice(0, 10);
|
|
}
|
|
|
|
export function getWeekEndsAt(nowMs = Date.now()): number {
|
|
const weekKey = getWeekKey(nowMs);
|
|
return Date.parse(`${weekKey}T00:00:00+08:00`) + 7 * DAY_MS;
|
|
}
|
|
|
|
function hash(input: string): number {
|
|
let value = 2166136261;
|
|
for (let i = 0; i < input.length; i++) {
|
|
value ^= input.charCodeAt(i);
|
|
value = Math.imul(value, 16777619);
|
|
}
|
|
// 打散低位,避免任务选择与二选一奖励使用相同奇偶位而互相关联。
|
|
value = Math.imul(value ^ (value >>> 16), 0x85ebca6b);
|
|
value = Math.imul(value ^ (value >>> 13), 0xc2b2ae35);
|
|
return (value ^ (value >>> 16)) >>> 0;
|
|
}
|
|
|
|
function selectDailyTasks(uid: string, dayKey: string, level: number): DailyTask[] {
|
|
const usedMetrics = new Set<string>();
|
|
return [2, 3, 4, 5, 6].map((stars, slot) => {
|
|
const candidates = DAILY_TASK_POOLS[stars].filter(candidate =>
|
|
(candidate.minLevel || 1) <= level && !usedMetrics.has(candidate.metric));
|
|
if (candidates.length === 0) throw new Error(`每日任务 ${stars} 星池没有可用的独立 metric`);
|
|
const selected = candidates[hash(`${uid}:${dayKey}:${stars}`) % candidates.length];
|
|
usedMetrics.add(selected.metric);
|
|
return {
|
|
slot,
|
|
taskId: selected.taskId,
|
|
trackingId: selected.trackingId,
|
|
metric: selected.metric,
|
|
target: selected.target,
|
|
value: selected.metric === "login" ? 1 : 0,
|
|
stars,
|
|
state: selected.metric === "login" ? 1 : 0,
|
|
locked: false,
|
|
rewards: [{ ...selected.rewardOptions[hash(`${uid}:${dayKey}:${selected.taskId}:reward`) % selected.rewardOptions.length] }],
|
|
...(selected.metric === "clear_color"
|
|
? { params: { colorId: TASK_COLOR_IDS[hash(`${uid}:${dayKey}:${selected.taskId}:color`) % TASK_COLOR_IDS.length] } }
|
|
: selected.params ? { params: { ...selected.params } } : {}),
|
|
} as DailyTask;
|
|
});
|
|
}
|
|
|
|
export function refreshTaskData(
|
|
stored: unknown,
|
|
uid: string,
|
|
level: number,
|
|
nowMs = Date.now(),
|
|
): { data: DailyWeeklyTaskData; changed: boolean } {
|
|
const dayKey = getDayKey(nowMs);
|
|
const weekKey = getWeekKey(nowMs);
|
|
const dayEndsAt = getDayEndsAt(nowMs);
|
|
const weekEndsAt = getWeekEndsAt(nowMs);
|
|
const parsed = typeof stored === "string" ? safeParse(stored) : stored;
|
|
const valid = parsed && (parsed as DailyWeeklyTaskData).version === 2
|
|
&& Array.isArray((parsed as DailyWeeklyTaskData).dailyTasks)
|
|
&& (parsed as DailyWeeklyTaskData).dailyTasks.length === 5
|
|
&& Array.isArray((parsed as DailyWeeklyTaskData).weeklyRewards);
|
|
const old = valid ? parsed as DailyWeeklyTaskData : null;
|
|
const newWeek = !old || old.weekKey !== weekKey;
|
|
const newDay = !old || old.dayKey !== dayKey;
|
|
|
|
if (!newDay && !newWeek) {
|
|
const changed = old.dayEndsAt !== dayEndsAt || old.weekEndsAt !== weekEndsAt
|
|
|| old.dailyTasks.some(task => task.locked !== false);
|
|
old.dayEndsAt = dayEndsAt;
|
|
old.weekEndsAt = weekEndsAt;
|
|
old.serverNow = nowMs;
|
|
refreshDailyStates(old.dailyTasks);
|
|
applyWeeklyStates(old);
|
|
return { data: old, changed };
|
|
}
|
|
|
|
const previousTaskIds = old ? old.dailyTasks.map(task => task.taskId) : [];
|
|
const data: DailyWeeklyTaskData = {
|
|
version: 2,
|
|
dayKey,
|
|
weekKey,
|
|
dayEndsAt,
|
|
weekEndsAt,
|
|
serverNow: nowMs,
|
|
dailyTasks: selectDailyTasks(uid, dayKey, Math.max(1, level)),
|
|
weeklyStars: newWeek ? 0 : Number(old?.weeklyStars) || 0,
|
|
weeklyRewards: newWeek
|
|
? WEEKLY_REWARDS.map(reward => ({ ...reward, rewards: reward.rewards.map(item => ({ ...item })) }))
|
|
: old!.weeklyRewards,
|
|
previousTaskIds,
|
|
};
|
|
applyWeeklyStates(data);
|
|
return { data, changed: true };
|
|
}
|
|
|
|
export function mergeClientProgress(data: DailyWeeklyTaskData, submitted: unknown): void {
|
|
const parsed = typeof submitted === "string" ? safeParse(submitted) : submitted;
|
|
const submittedTasks = Array.isArray((parsed as any)?.dailyTasks)
|
|
? (parsed as any).dailyTasks
|
|
: [];
|
|
const byId = new Map(submittedTasks.map((task: any) => [task.taskId, task]));
|
|
data.dailyTasks.forEach(task => {
|
|
const incoming: any = byId.get(task.taskId);
|
|
if (!incoming) return;
|
|
const value = Number(incoming.value);
|
|
if (Number.isFinite(value)) task.value = Math.max(0, Math.min(task.target, value));
|
|
if (task.state !== 2) task.state = task.value >= task.target ? 1 : 0;
|
|
});
|
|
refreshDailyStates(data.dailyTasks);
|
|
}
|
|
|
|
export function claimDaily(data: DailyWeeklyTaskData, taskId: string) {
|
|
const task = data.dailyTasks.find(item => item.taskId === taskId);
|
|
if (!task) return { ok: false, msg: "任务不存在" };
|
|
if (task.state === 2) return { ok: false, msg: "不能重复领取奖励" };
|
|
if (task.value < task.target || task.state !== 1) return { ok: false, msg: "请先完成任务" };
|
|
task.state = 2;
|
|
data.weeklyStars = Math.min(140, data.weeklyStars + task.stars);
|
|
refreshDailyStates(data.dailyTasks);
|
|
applyWeeklyStates(data);
|
|
return { ok: true, rewards: task.rewards };
|
|
}
|
|
|
|
export function claimWeekly(data: DailyWeeklyTaskData, rewardId: string) {
|
|
const reward = data.weeklyRewards.find(item => item.rewardId === rewardId);
|
|
if (!reward) return { ok: false, msg: "周奖励不存在" };
|
|
if (reward.state === 2) return { ok: false, msg: "不能重复领取奖励" };
|
|
if (data.weeklyStars < reward.target) return { ok: false, msg: "星星数量不足" };
|
|
reward.state = 2;
|
|
return { ok: true, rewards: reward.rewards };
|
|
}
|
|
|
|
function refreshDailyStates(tasks: DailyTask[]): void {
|
|
tasks.forEach(task => {
|
|
// 保留字段兼容旧客户端,历史任务在读取时一并开放。
|
|
task.locked = false;
|
|
if (task.state !== 2 && task.value >= task.target) task.state = 1;
|
|
});
|
|
}
|
|
|
|
function applyWeeklyStates(data: DailyWeeklyTaskData): void {
|
|
data.weeklyRewards.forEach(reward => {
|
|
if (reward.state !== 2) reward.state = data.weeklyStars >= reward.target ? 1 : 0;
|
|
});
|
|
}
|
|
|
|
function safeParse(value: string): unknown {
|
|
try { return JSON.parse(value); } catch (_) { return null; }
|
|
}
|