Files
TetaRdPG/src/combat/combat.service.ts
Tetardtek 6d1230d16a feat: Sprint 2 — moteur de combat PvE TetaRdPG
Moteur combat stateless (POST /api/combat/start résout le combat complet).
Formules GDD : Mêlée/Distance/Magie × 1.5, critique (5% + Chance×0.2%), esquive (5% + Chance×0.1%).
5 monstres seedés (Têtard Vase → Golem de Boue, level 1–9).
Level up : XP → seuil atteint → level++, +5 statPoints.
Persiste combat_logs (jsonb rounds). Validé : victoire, défaite, 401, 400, 404.
2026-03-15 06:10:06 +01:00

196 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Character } from '../character/entities/character.entity';
import { Monster } from '../monster/monster.entity';
import { MonsterService } from '../monster/monster.service';
import { CombatLog } from './combat-log.entity';
import { StartCombatDto } from './dto/start-combat.dto';
import { User } from '../user/user.entity';
import {
resolveCombat,
applyXpGain,
CombatantStats,
} from './combat.engine';
const COMBAT_ENDURANCE_COST = 10;
const DEFEAT_ENDURANCE_PENALTY = 50;
const DEFEAT_HP_RATIO = 0.2; // 20% hpMax à la défaite
const VICTORY_HP_REGEN_RATIO = 0.1; // +10% hpMax à la victoire
const DEFEAT_GOLD_LOSS_RATIO = 0.05; // perte 5% or à la défaite
@Injectable()
export class CombatService {
constructor(
@InjectRepository(Character)
private readonly characterRepository: Repository<Character>,
@InjectRepository(CombatLog)
private readonly combatLogRepository: Repository<CombatLog>,
private readonly monsterService: MonsterService,
) {}
async startCombat(dto: StartCombatDto, user: User) {
// Charger le personnage
const character = await this.characterRepository.findOne({
where: { userId: user.id },
});
if (!character) throw new BadRequestException('Aucun personnage trouvé');
// Charger le monstre
const monster = await this.monsterService.findOne(dto.monsterId);
// Calculer l'endurance actuelle (lazy pattern)
const elapsedMinutes = (Date.now() - character.lastEnduranceTs.getTime()) / 60_000;
const recharge = Math.floor(elapsedMinutes / 6);
const enduranceCurrent = Math.min(character.enduranceSaved + recharge, character.enduranceMax);
if (enduranceCurrent < COMBAT_ENDURANCE_COST) {
throw new BadRequestException(
`Endurance insuffisante (${enduranceCurrent}/${COMBAT_ENDURANCE_COST} requis)`,
);
}
if (character.hpCurrent <= 0) {
throw new BadRequestException('Votre personnage est KO — récupérez d\'abord vos PV');
}
// Construire les stats des combattants
const playerStats: CombatantStats = {
name: character.name,
hpCurrent: character.hpCurrent,
hpMax: character.hpMax,
force: character.force,
agilite: character.agilite,
intelligence: character.intelligence,
chance: character.chance,
attack: 0, // pas d'arme Sprint 2
defense: 0, // pas d'armure Sprint 2
attackType: dto.attackType,
};
const monsterStats: CombatantStats = {
name: monster.name,
hpCurrent: monster.hp,
hpMax: monster.hp,
force: 0,
agilite: 0,
intelligence: 0,
chance: 0, // pas de crit/esquive pour les monstres Sprint 2
attack: monster.attack,
defense: monster.defense,
attackType: monster.attackType,
};
// Résolution combat
const result = resolveCombat(
playerStats,
monsterStats,
monster.xpReward,
monster.goldMin,
monster.goldMax,
);
// Appliquer les effets post-combat sur le personnage
let newHp = character.hpCurrent;
let newEnduranceSaved = enduranceCurrent - COMBAT_ENDURANCE_COST;
let goldLost = 0;
let levelUpData = { levelsGained: 0, statPointsGained: 0, newLevel: character.level, newXp: character.xp };
if (result.winner === 'player') {
// Victoire : XP + Or + récup 10% PV
levelUpData = applyXpGain(character.level, character.xp, result.xpEarned);
character.xp = levelUpData.newXp;
character.level = levelUpData.newLevel;
character.statPoints = (character.statPoints ?? 0) + levelUpData.statPointsGained;
character.gold += result.goldEarned;
newHp = Math.min(character.hpMax, character.hpCurrent + Math.floor(character.hpMax * VICTORY_HP_REGEN_RATIO));
} else {
// Défaite : retour auberge + pénalités
newEnduranceSaved = Math.max(0, newEnduranceSaved - DEFEAT_ENDURANCE_PENALTY);
newHp = Math.max(1, Math.floor(character.hpMax * DEFEAT_HP_RATIO));
goldLost = Math.floor(character.gold * DEFEAT_GOLD_LOSS_RATIO);
character.gold = Math.max(0, character.gold - goldLost);
}
// Sauvegarder l'endurance (lazy reset)
character.hpCurrent = newHp;
character.enduranceSaved = newEnduranceSaved;
character.lastEnduranceTs = new Date();
await this.characterRepository.save(character);
// Persister le log
const combatLog = this.combatLogRepository.create({
characterId: character.id,
monsterId: monster.id,
winner: result.winner,
totalRounds: result.totalRounds,
roundsData: result.rounds,
xpEarned: result.xpEarned,
goldEarned: result.goldEarned,
levelUp: levelUpData.levelsGained > 0,
});
await this.combatLogRepository.save(combatLog);
// Construire la réponse
const summaryParts: string[] = [];
if (result.winner === 'player') {
summaryParts.push(`Victoire en ${result.totalRounds} tours !`);
summaryParts.push(`+${result.xpEarned} XP, +${result.goldEarned} Or.`);
if (levelUpData.levelsGained > 0) {
summaryParts.push(`LEVEL UP ! Niveau ${levelUpData.newLevel} atteint. +${levelUpData.statPointsGained} points de stats.`);
}
} else {
summaryParts.push(`Défaite au tour ${result.totalRounds}. Retour à l'auberge.`);
if (goldLost > 0) summaryParts.push(`${goldLost} Or perdu.`);
}
return {
winner: result.winner,
rounds: result.rounds,
summary: summaryParts.join(' '),
rewards: {
xp: result.xpEarned,
gold: result.goldEarned,
goldLost,
levelUp: levelUpData.levelsGained > 0,
newLevel: levelUpData.newLevel,
statPointsGained: levelUpData.statPointsGained,
},
character: {
level: character.level,
xp: character.xp,
gold: character.gold,
hpCurrent: character.hpCurrent,
hpMax: character.hpMax,
enduranceCurrent: character.enduranceSaved, // déjà le nouveau enduranceSaved post-combat
enduranceMax: character.enduranceMax,
statPoints: character.statPoints ?? 0,
},
};
}
async getHistory(user: User) {
const character = await this.characterRepository.findOne({
where: { userId: user.id },
});
if (!character) throw new BadRequestException('Aucun personnage trouvé');
return this.combatLogRepository.find({
where: { characterId: character.id },
order: { createdAt: 'DESC' },
take: 20,
relations: ['monster'],
select: {
id: true,
winner: true,
totalRounds: true,
xpEarned: true,
goldEarned: true,
levelUp: true,
createdAt: true,
monster: { id: true, name: true, minLevel: true, maxLevel: true } as any,
},
});
}
}