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, @InjectRepository(CombatLog) private readonly combatLogRepository: Repository, 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, }, }); } }