forked from xiangwang25/whale-town-end-v2
feat: deploy adventure wallet progression
This commit is contained in:
102
src/business/player/adventure.service.ts
Normal file
102
src/business/player/adventure.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { UserWallets } from '../../core/db/user_wallets/user_wallets.entity';
|
||||
import chapters from './adventure_chapters.json';
|
||||
|
||||
const QUESTIONS = [
|
||||
{ text: 'Python 中列表通常使用哪一种括号表示?', options: ['{}', '[]', '()', '<>'], answer: 1 },
|
||||
{ text: 'Python 中哪个关键字用于定义函数?', options: ['class', 'for', 'def', 'return'], answer: 2 },
|
||||
{ text: 'Python 中 len([1, 2, 3]) 的结果是什么?', options: ['3', '2', '1', '0'], answer: 0 },
|
||||
{ text: 'Python 中哪个值表示逻辑真?', options: ['None', 'False', '0', 'True'], answer: 3 },
|
||||
];
|
||||
export interface AdventureCommand { action: string; encounter?: string; session?: string; turn?: number; choice?: number; chapter?: string; answers?: number[]; }
|
||||
|
||||
@Injectable()
|
||||
export class AdventureService {
|
||||
constructor(@Inject('IUserWalletsService') private readonly wallets: {
|
||||
mutateAdventure<T>(id: bigint, operation: (wallet: UserWallets) => T): Promise<T>;
|
||||
}) {}
|
||||
|
||||
execute(userId: bigint, command: AdventureCommand) {
|
||||
return this.wallets.mutateAdventure(userId, wallet => {
|
||||
const s = wallet.adventure ||= { level: 1, experience: 0, health: 10, chapters: {}, defeated: {}, sequence: 0, battle: null };
|
||||
const maxHealth = () => 10 + (s.level - 1) * 5;
|
||||
const spend = (amount: number) => {
|
||||
if (wallet.balance < amount) throw new BadRequestException('鲸币余额不足');
|
||||
wallet.balance -= amount;
|
||||
};
|
||||
let correct: boolean | undefined;
|
||||
const active = s.battle?.status === 'active';
|
||||
if (active && ['rest', 'recover', 'enroll', 'complete'].includes(command.action)) throw new BadRequestException('请先结束战斗');
|
||||
switch (command.action) {
|
||||
case 'snapshot': break;
|
||||
case 'start': {
|
||||
if (!command.encounter || !/^(starting_encounter|-?\d+_-?\d+)$/.test(command.encounter)) throw new BadRequestException('无效遭遇');
|
||||
if (active) {
|
||||
if (s.battle.encounter !== command.encounter) throw new BadRequestException('已有进行中的战斗');
|
||||
break;
|
||||
}
|
||||
if (s.defeated[command.encounter]) throw new BadRequestException('该怪物已被击败');
|
||||
if (s.health <= 0) throw new BadRequestException('请返回营地恢复');
|
||||
s.battle = { id: randomUUID(), encounter: command.encounter, turn: 0, hearts: 3, status: 'active', last: null };
|
||||
break;
|
||||
}
|
||||
case 'answer': {
|
||||
const b = s.battle;
|
||||
if (!b || command.session !== b.id) throw new BadRequestException('战斗不存在');
|
||||
if (b.last && command.turn === b.last.turn) {
|
||||
if (command.choice !== b.last.choice) throw new BadRequestException('本回合已结算');
|
||||
correct = b.last.correct;
|
||||
break;
|
||||
}
|
||||
if (b.status !== 'active' || command.turn !== b.turn || !Number.isInteger(command.choice) || command.choice < 0 || command.choice > 3) throw new BadRequestException('回合或答案无效');
|
||||
correct = command.choice === QUESTIONS[b.turn % QUESTIONS.length].answer;
|
||||
b.last = { turn: b.turn, choice: command.choice, correct };
|
||||
b.turn++;
|
||||
if (correct) b.hearts--; else s.health = Math.max(0, s.health - 10);
|
||||
if (b.hearts === 0) {
|
||||
b.status = 'victory';
|
||||
s.defeated[b.encounter] = true;
|
||||
wallet.balance += 10;
|
||||
} else if (!s.health) b.status = 'defeat';
|
||||
break;
|
||||
}
|
||||
case 'escape':
|
||||
if (!s.battle || s.battle.id !== command.session) throw new BadRequestException('战斗不存在');
|
||||
if (active) s.battle.status = 'escaped';
|
||||
break;
|
||||
case 'rest': s.health = maxHealth(); break;
|
||||
case 'recover':
|
||||
if (s.health < maxHealth()) { spend(5); s.health = Math.min(maxHealth(), s.health + 30); }
|
||||
break;
|
||||
case 'enroll':
|
||||
case 'complete': {
|
||||
const chapter = (chapters as Record<string, { answers: number[] }>)[command.chapter];
|
||||
if (!chapter) throw new BadRequestException('章节不存在');
|
||||
if (command.action === 'enroll') {
|
||||
if (!s.chapters[command.chapter]) { spend(20); s.chapters[command.chapter] = 'enrolled'; }
|
||||
} else {
|
||||
if (!s.chapters[command.chapter]) throw new BadRequestException('请先报名');
|
||||
if (s.chapters[command.chapter] === 'completed') break;
|
||||
if (!Array.isArray(command.answers) || command.answers.length !== chapter.answers.length || chapter.answers.some((answer, i) => command.answers[i] !== answer)) throw new BadRequestException('请完成章节练习');
|
||||
s.chapters[command.chapter] = 'completed';
|
||||
if (s.level < 5) s.experience += 20;
|
||||
while (s.level < 5 && s.experience >= 20 * (s.level + 1)) {
|
||||
s.experience -= 20 * (s.level + 1); s.level++; s.health += 5;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: throw new BadRequestException('未知操作');
|
||||
}
|
||||
s.sequence++;
|
||||
const b = s.battle;
|
||||
const question = QUESTIONS[(b?.turn || 0) % QUESTIONS.length];
|
||||
return { wallet: { user_id: userId.toString(), balance: wallet.balance, currency: 'whale_coin' },
|
||||
progress: { level: s.level, experience: s.experience, health: s.health, max_health: maxHealth(), chapters: s.chapters, defeated: s.defeated },
|
||||
battle: b ? { id: b.id, encounter: b.encounter, turn: b.turn, hearts: b.hearts, status: b.status, question: { text: question.text, options: question.options } } : null,
|
||||
correct,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user