Files
whale-town-end-v2/src/business/tasks/task_progress_memory.service.ts
2026-07-22 00:45:59 +08:00

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}`;
}
}