diff --git a/src/combat/combat.service.ts b/src/combat/combat.service.ts index 7cdcdc9..d3a9bfe 100644 --- a/src/combat/combat.service.ts +++ b/src/combat/combat.service.ts @@ -288,36 +288,151 @@ export class CombatService { } async startMultiCombat(dto: StartCombatDto, user: User, count: number) { - const results: any[] = []; - const totals = { wins: 0, losses: 0, xp: 0, gold: 0, goldLost: 0, loot: [] as { name: string; quantity: number }[], levelsGained: 0 }; + const monster = await this.monsterService.findOne(dto.monsterId); + + const txResult = await this.dataSource.transaction(async (manager) => { + const character = await manager + .getRepository(Character) + .createQueryBuilder('c') + .setLock('pessimistic_write') + .where('c.user_id = :userId', { userId: user.id }) + .getOne(); + + if (!character) throw new BadRequestException('Aucun personnage trouvé'); + + // Équipement (une seule fois) + const equipped = await this.itemService.getEquippedItems(character.id); + const FORGE_BONUS_PER_LEVEL = 2; + const weaponAttack = equipped.weapon + ? equipped.weapon.item.attackBonus + equipped.weapon.forgeLevel * FORGE_BONUS_PER_LEVEL + : 0; + const armorDefense = equipped.armor + ? equipped.armor.item.defenseBonus + equipped.armor.forgeLevel * FORGE_BONUS_PER_LEVEL + : 0; + const itemForceBonus = (equipped.weapon?.item.forceBonus ?? 0) + (equipped.armor?.item.forceBonus ?? 0); + const itemAgiliteBonus = (equipped.weapon?.item.agiliteBonus ?? 0) + (equipped.armor?.item.agiliteBonus ?? 0); + const itemIntelligenceBonus = (equipped.weapon?.item.intelligenceBonus ?? 0) + (equipped.armor?.item.intelligenceBonus ?? 0); + const itemChanceBonus = (equipped.weapon?.item.chanceBonus ?? 0) + (equipped.armor?.item.chanceBonus ?? 0); + + const totals = { wins: 0, losses: 0, xp: 0, gold: 0, goldLost: 0, loot: [] as { name: string; quantity: number }[], levelsGained: 0 }; + const lootedMaterialIds: string[] = []; + let combatsDone = 0; + + for (let i = 0; i < count; i++) { + // Endurance lazy calc + const elapsed = (Date.now() - character.lastEnduranceTs.getTime()) / 60_000; + const recharge = Math.floor(elapsed / 3); + const enduranceCurrent = Math.min(character.enduranceSaved + recharge, character.enduranceMax); + + if (enduranceCurrent < COMBAT_ENDURANCE_COST || character.hpCurrent <= 0) break; + + const playerStats: CombatantStats = { + name: character.name, + hpCurrent: character.hpCurrent, + hpMax: character.hpMax, + force: character.force + itemForceBonus, + agilite: character.agilite + itemAgiliteBonus, + intelligence: character.intelligence + itemIntelligenceBonus, + chance: character.chance + itemChanceBonus, + attack: weaponAttack, + defense: armorDefense, + attackType: dto.attackType, + }; + + const monsterStats: CombatantStats = { + name: monster.name, hpCurrent: monster.hp, hpMax: monster.hp, + force: 0, agilite: 0, intelligence: 0, chance: 0, + attack: monster.attack, defense: monster.defense, attackType: monster.attackType, + }; + + const result = resolveCombat(playerStats, monsterStats, monster.xpReward, monster.goldMin, monster.goldMax); + combatsDone++; + + let newEnduranceSaved = enduranceCurrent - COMBAT_ENDURANCE_COST; - for (let i = 0; i < count; i++) { - try { - const result = await this.startCombat(dto, user); - results.push(result); if (result.winner === 'player') { + const levelUp = applyXpGain(character.level, character.xp, result.xpEarned); + character.xp = levelUp.newXp; + character.level = levelUp.newLevel; + character.statPoints = (character.statPoints ?? 0) + levelUp.statPointsGained; + character.gold += result.goldEarned; + character.totalGoldEarned = Number(character.totalGoldEarned ?? 0) + result.goldEarned; + character.hpCurrent = Math.min(character.hpMax, character.hpCurrent + Math.floor(character.hpMax * VICTORY_HP_REGEN_RATIO)); + totals.wins++; - totals.xp += result.rewards.xp; - totals.gold += result.rewards.gold; - if (result.rewards.levelUp) totals.levelsGained++; - if (result.rewards.loot) totals.loot.push(result.rewards.loot); + totals.xp += result.xpEarned; + totals.gold += result.goldEarned; + if (levelUp.levelsGained > 0) totals.levelsGained += levelUp.levelsGained; + + // Loot + if (monster.dropMaterialId) { + const { dropRate, dropQty } = computeDropRate(character.level, monster.minLevel, monster.maxLevel); + if (Math.random() < dropRate) { + await this.materialService.addMaterial(character.id, monster.dropMaterialId, dropQty); + totals.loot.push({ name: 'matériau', quantity: dropQty }); + lootedMaterialIds.push(monster.dropMaterialId); + } + } } else { + newEnduranceSaved = Math.max(0, newEnduranceSaved - DEFEAT_ENDURANCE_PENALTY); + character.hpCurrent = Math.max(1, Math.floor(character.hpMax * DEFEAT_HP_RATIO)); + const goldLost = Math.floor(character.gold * DEFEAT_GOLD_LOSS_RATIO); + character.gold = Math.max(0, character.gold - goldLost); totals.losses++; - totals.goldLost += result.rewards.goldLost ?? 0; - break; // Défaite = arrêt de la série + totals.goldLost += goldLost; } - } catch { - break; // Endurance insuffisante ou autre erreur = arrêt + + character.enduranceSaved = newEnduranceSaved; + character.lastEnduranceTs = new Date(); + + // Log combat + await manager.save(manager.getRepository(CombatLog).create({ + characterId: character.id, monsterId: monster.id, + winner: result.winner, totalRounds: result.totalRounds, + roundsData: result.rounds, xpEarned: result.xpEarned, + goldEarned: result.goldEarned, levelUp: false, + })); + + if (result.winner !== 'player') break; // Arrêt sur défaite + } + + // Save final character state + await manager.save(character); + + return { + characterId: character.id, + lootedMaterialIds, + combatsDone, + totals, + character: { + level: character.level, xp: character.xp, + xpToNextLevel: xpRequiredForLevel(character.level), + gold: character.gold, hpCurrent: character.hpCurrent, + hpMax: character.hpMax, enduranceCurrent: character.enduranceSaved, + enduranceMax: character.enduranceMax, statPoints: character.statPoints ?? 0, + }, + }; + }); + + // Events groupés APRÈS la transaction (une seule fois) + const cid = txResult.characterId; + if (txResult.totals.wins > 0) { + this.eventEmitter.emit('achievement.check', { characterId: cid, type: 'combat_wins', increment: txResult.totals.wins }); + this.eventEmitter.emit('achievement.check', { characterId: cid, type: 'level_reached', increment: 0, absolute: txResult.character.level }); + this.eventEmitter.emit('achievement.check', { characterId: cid, type: 'gold_accumulated', increment: 0, absolute: txResult.character.gold }); + this.eventEmitter.emit('community.contribute', { characterId: cid, type: 'total_monsters_killed', increment: txResult.totals.wins }); + this.eventEmitter.emit('community.contribute', { characterId: cid, type: 'total_gold_earned', increment: txResult.totals.gold }); + this.eventEmitter.emit('quest.progress', { characterId: cid, type: 'kill_any', increment: txResult.totals.wins, zone: monster.zone }); + for (const matId of txResult.lootedMaterialIds) { + this.eventEmitter.emit('quest.progress', { characterId: cid, type: 'gather_material', targetId: matId, increment: 1 }); } } - const lastResult = results[results.length - 1]; return { mode: 'multi', - count: results.length, - totals, - lastResult, - character: lastResult?.character, + count: txResult.combatsDone, + totals: txResult.totals, + character: txResult.character, }; }