forked from xiangwang25/whale-town-end-v2
feat: add task reward economy APIs
This commit is contained in:
189
src/business/tasks/task_catalog.ts
Normal file
189
src/business/tasks/task_catalog.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user