forked from xiangwang25/whale-town-end-v2
148 lines
5.6 KiB
TypeScript
148 lines
5.6 KiB
TypeScript
import { BadRequestException, ConflictException, Inject, Injectable } from '@nestjs/common';
|
|
import { PlayerWalletPayload } from '../player/player.types';
|
|
import {
|
|
getCurrentWeeklyCycle,
|
|
getTaskCycleKey,
|
|
getTaskDefinitions,
|
|
NEWBIE_CYCLE_KEY,
|
|
TaskActivityType,
|
|
TaskDefinition,
|
|
TaskProgressState,
|
|
WEEKLY_COMPLETION_BONUS,
|
|
WEEKLY_TASKS,
|
|
} from './task_catalog';
|
|
import { buildTaskBoard, TaskBoardPayload, TaskClaimResult, TaskProgressRow, TaskProgressStore } from './tasks.types';
|
|
|
|
interface IUserWalletsService {
|
|
earn(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<{ wallet: { balance: number } }>;
|
|
}
|
|
|
|
interface MemoryProgressRow extends TaskProgressRow {
|
|
user_id: bigint;
|
|
created_at: Date;
|
|
updated_at: Date;
|
|
}
|
|
|
|
@Injectable()
|
|
export class TaskProgressMemoryService implements TaskProgressStore {
|
|
private readonly rows = new Map<string, MemoryProgressRow>();
|
|
|
|
constructor(@Inject('IUserWalletsService') private readonly walletService: IUserWalletsService) {}
|
|
|
|
async getBoard(userId: bigint): Promise<TaskBoardPayload> {
|
|
const cycle = getCurrentWeeklyCycle();
|
|
const rows = this.ensureRows(userId, cycle.key);
|
|
this.syncWeeklyBonus(rows);
|
|
return buildTaskBoard(rows, cycle);
|
|
}
|
|
|
|
async recordActivity(userId: bigint, activity: TaskActivityType, targetId?: string): Promise<TaskBoardPayload> {
|
|
const cycle = getCurrentWeeklyCycle();
|
|
const rows = this.ensureRows(userId, cycle.key);
|
|
for (const definition of getTaskDefinitions()) {
|
|
if (definition.bonus || definition.activity !== activity) continue;
|
|
const row = this.findRow(rows, definition, cycle.key);
|
|
this.applyActivity(definition, row, targetId);
|
|
}
|
|
this.syncWeeklyBonus(rows);
|
|
return buildTaskBoard(rows, cycle);
|
|
}
|
|
|
|
async claim(userId: bigint, taskId: string): Promise<TaskClaimResult> {
|
|
const cycle = getCurrentWeeklyCycle();
|
|
const rows = this.ensureRows(userId, cycle.key);
|
|
this.syncWeeklyBonus(rows);
|
|
const definition = getTaskDefinitions().find((item) => item.id === taskId);
|
|
if (!definition) {
|
|
throw new BadRequestException('任务不存在');
|
|
}
|
|
const row = this.findRow(rows, definition, cycle.key);
|
|
if (!row.completed_at) {
|
|
throw new BadRequestException('任务尚未完成');
|
|
}
|
|
if (row.claimed_at) {
|
|
throw new ConflictException('任务奖励已领取');
|
|
}
|
|
const result = await this.walletService.earn(
|
|
userId,
|
|
definition.reward,
|
|
'task_reward',
|
|
`${getTaskCycleKey(definition, cycle)}:${definition.id}`,
|
|
`任务奖励:${definition.title}`,
|
|
);
|
|
row.claimed_at = new Date();
|
|
row.updated_at = new Date();
|
|
const wallet: PlayerWalletPayload = {
|
|
user_id: userId.toString(),
|
|
balance: result.wallet.balance,
|
|
currency: 'whale_coin',
|
|
};
|
|
return { board: buildTaskBoard(rows, cycle), wallet };
|
|
}
|
|
|
|
private ensureRows(userId: bigint, weeklyCycleKey: string): MemoryProgressRow[] {
|
|
for (const definition of getTaskDefinitions()) {
|
|
const cycleKey = definition.group === 'weekly' ? weeklyCycleKey : NEWBIE_CYCLE_KEY;
|
|
const key = this.rowKey(userId, definition.id, cycleKey);
|
|
if (!this.rows.has(key)) {
|
|
const now = new Date();
|
|
this.rows.set(key, {
|
|
user_id: userId,
|
|
task_id: definition.id,
|
|
cycle_key: cycleKey,
|
|
progress: 0,
|
|
activity_state: {},
|
|
completed_at: null,
|
|
claimed_at: null,
|
|
created_at: now,
|
|
updated_at: now,
|
|
});
|
|
}
|
|
}
|
|
return getTaskDefinitions().map((definition) => {
|
|
const cycleKey = definition.group === 'weekly' ? weeklyCycleKey : NEWBIE_CYCLE_KEY;
|
|
return this.rows.get(this.rowKey(userId, definition.id, cycleKey)) as MemoryProgressRow;
|
|
});
|
|
}
|
|
|
|
private findRow(rows: MemoryProgressRow[], definition: TaskDefinition, weeklyCycleKey: string): MemoryProgressRow {
|
|
const cycleKey = definition.group === 'weekly' ? weeklyCycleKey : NEWBIE_CYCLE_KEY;
|
|
const row = rows.find((item) => item.task_id === definition.id && item.cycle_key === cycleKey);
|
|
if (!row) throw new Error(`任务进度缺失: ${definition.id}`);
|
|
return row;
|
|
}
|
|
|
|
private applyActivity(definition: TaskDefinition, row: MemoryProgressRow, targetId?: string): void {
|
|
if (row.completed_at) return;
|
|
if (definition.allowed_targets && (!targetId || !definition.allowed_targets.includes(targetId))) return;
|
|
if (definition.progress_mode === 'unique_target') {
|
|
if (!targetId) return;
|
|
const state = row.activity_state as TaskProgressState;
|
|
const targets = Array.isArray(state.targets) ? state.targets.filter((item): item is string => typeof item === 'string') : [];
|
|
if (targets.includes(targetId)) return;
|
|
targets.push(targetId);
|
|
row.activity_state = { ...state, targets };
|
|
row.progress = Math.min(definition.target, targets.length);
|
|
} else {
|
|
row.progress = Math.min(definition.target, row.progress + 1);
|
|
}
|
|
if (row.progress >= definition.target) row.completed_at = new Date();
|
|
row.updated_at = new Date();
|
|
}
|
|
|
|
private syncWeeklyBonus(rows: MemoryProgressRow[]): void {
|
|
const bonus = rows.find((row) => row.task_id === WEEKLY_COMPLETION_BONUS.id);
|
|
if (!bonus || bonus.completed_at) return;
|
|
const complete = WEEKLY_TASKS.every((definition) => rows.some((row) => row.task_id === definition.id && row.completed_at));
|
|
if (complete) {
|
|
bonus.progress = 1;
|
|
bonus.completed_at = new Date();
|
|
bonus.updated_at = new Date();
|
|
}
|
|
}
|
|
|
|
private rowKey(userId: bigint, taskId: string, cycleKey: string): string {
|
|
return `${userId.toString()}:${taskId}:${cycleKey}`;
|
|
}
|
|
}
|