export const NEWBIE_CYCLE_KEY = 'newbie'; export const TASK_ACTIVITY_TYPES = [ 'guide_opened', 'notice_viewed', 'map_visited', 'course_board_opened', 'facility_interacted', 'public_message_sent', 'skin_purchased', ] as const; export type TaskActivityType = typeof TASK_ACTIVITY_TYPES[number]; export type TaskGroup = 'newbie' | 'weekly'; export type TaskProgressMode = 'count' | 'unique_target'; export interface TaskDefinition { id: string; group: TaskGroup; title: string; description: string; reward: number; target: number; activity?: TaskActivityType; progress_mode?: TaskProgressMode; allowed_targets?: string[]; optional?: boolean; bonus?: boolean; sort_order: number; } export interface WeeklyCycle { key: string; starts_at: string; ends_at: string; } export interface TaskProgressState { targets?: string[]; } export const NEWBIE_TASKS: TaskDefinition[] = [ { id: 'newbie_guide', group: 'newbie', title: '翻阅新人手册', description: '打开新人引导,了解鲸镇的基本操作。', reward: 40, target: 1, activity: 'guide_opened', sort_order: 10, }, { id: 'newbie_notice', group: 'newbie', title: '查看镇务公告', description: '在广场查看一次公告栏。', reward: 60, target: 1, activity: 'notice_viewed', sort_order: 20, }, { id: 'newbie_work_zone', group: 'newbie', title: '探索打工区', description: '前往打工区,看看小镇的工作与学习入口。', reward: 80, target: 1, activity: 'map_visited', allowed_targets: ['work_zone'], sort_order: 30, }, { id: 'newbie_course_board', group: 'newbie', title: '浏览课程板', description: '在打工区打开 Datawhale 课程看板。', reward: 120, target: 1, activity: 'course_board_opened', sort_order: 40, }, { id: 'newbie_first_skin', group: 'newbie', title: '选择你的形象', description: '在鲸鱼商城购买任意一款皮肤。此任务可跳过。', reward: 100, target: 1, activity: 'skin_purchased', optional: true, sort_order: 50, }, ]; export const WEEKLY_TASKS: TaskDefinition[] = [ { id: 'weekly_explore', group: 'weekly', title: '海风巡游', description: '探索两个不同的开放地图。', reward: 100, target: 2, activity: 'map_visited', progress_mode: 'unique_target', allowed_targets: ['square', 'work_zone', 'whale_cafe'], sort_order: 10, }, { id: 'weekly_course', group: 'weekly', title: '本周学习计划', description: '打开一次 Datawhale 课程看板。', reward: 100, target: 1, activity: 'course_board_opened', sort_order: 20, }, { id: 'weekly_interact', group: 'weekly', title: '和小镇打招呼', description: '与两个不同的公共设施或 NPC 互动。', reward: 100, target: 2, activity: 'facility_interacted', progress_mode: 'unique_target', allowed_targets: ['welcome_board', 'notice_board', 'npc'], sort_order: 30, }, { id: 'weekly_public_message', group: 'weekly', title: '分享此刻', description: '在公共频道成功发送一条消息。', reward: 100, target: 1, activity: 'public_message_sent', sort_order: 40, }, ]; export const WEEKLY_COMPLETION_BONUS: TaskDefinition = { id: 'weekly_completion_bonus', group: 'weekly', title: '本周任务书结算', description: '完成本周全部四项任务后领取额外奖励。', reward: 200, target: 1, bonus: true, sort_order: 90, }; export function getCurrentWeeklyCycle(now: Date = new Date()): WeeklyCycle { const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', }); const parts = Object.fromEntries(formatter.formatToParts(now) .filter((part) => part.type !== 'literal') .map((part) => [part.type, part.value])); const year = Number(parts.year); const month = Number(parts.month); const day = Number(parts.day); const chinaDateAsUtc = Date.UTC(year, month - 1, day); const weekday = new Date(chinaDateAsUtc).getUTCDay(); const daysSinceMonday = (weekday + 6) % 7; const mondayAsUtc = chinaDateAsUtc - daysSinceMonday * 24 * 60 * 60 * 1000; const monday = new Date(mondayAsUtc); const cycleDate = monday.toISOString().slice(0, 10); const startsAt = new Date(mondayAsUtc - 8 * 60 * 60 * 1000); const endsAt = new Date(startsAt.getTime() + 7 * 24 * 60 * 60 * 1000); return { key: `weekly:${cycleDate}`, starts_at: startsAt.toISOString(), ends_at: endsAt.toISOString(), }; } export function getTaskDefinitions(): TaskDefinition[] { return [...NEWBIE_TASKS, ...WEEKLY_TASKS, WEEKLY_COMPLETION_BONUS]; } export function getTaskCycleKey(definition: TaskDefinition, cycle: WeeklyCycle): string { return definition.group === 'weekly' ? cycle.key : NEWBIE_CYCLE_KEY; }