diff --git a/src/combat/combat.service.ts b/src/combat/combat.service.ts index d3a9bfe..e62a585 100644 --- a/src/combat/combat.service.ts +++ b/src/combat/combat.service.ts @@ -1,6 +1,7 @@ import { Injectable, BadRequestException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { CharacterMaterial } from '../material/character-material.entity'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Character } from '../character/entities/character.entity'; import { Monster } from '../monster/monster.entity'; @@ -54,8 +55,25 @@ 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 +/** Ajouter un matériau dans la transaction courante (pas de connexion séparée). */ +async function addMaterialInTx(manager: EntityManager, characterId: string, materialId: string, quantity: number) { + const repo = manager.getRepository(CharacterMaterial); + let entry = await repo.findOne({ where: { characterId, materialId } }); + if (entry) { + entry.quantity += quantity; + } else { + entry = repo.create({ characterId, materialId, quantity }); + } + await repo.save(entry); +} + +const COOLDOWN_SINGLE_MS = 2_000; +const COOLDOWN_MULTI_MS = 8_000; + @Injectable() export class CombatService { + private readonly cooldowns = new Map(); // userId → timestamp + constructor( @InjectRepository(Character) private readonly characterRepository: Repository, @@ -69,7 +87,23 @@ export class CombatService { private readonly dataSource: DataSource, ) {} + private checkCooldown(userId: string): void { + const lastCombat = this.cooldowns.get(userId) ?? 0; + const remaining = lastCombat - Date.now(); + if (remaining > 0) { + throw new BadRequestException( + `Cooldown actif — attendez ${Math.ceil(remaining / 1000)}s`, + ); + } + } + + private setCooldown(userId: string, durationMs: number): void { + this.cooldowns.set(userId, Date.now() + durationMs); + } + async startCombat(dto: StartCombatDto, user: User) { + this.checkCooldown(user.id); + // Charger le monstre (hors transaction — lecture seule) const monster = await this.monsterService.findOne(dto.monsterId); @@ -202,7 +236,7 @@ export class CombatService { if (result.winner === 'player' && 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); + await addMaterialInTx(manager, character.id, monster.dropMaterialId, dropQty); lootMaterial = { name: 'matériau', quantity: dropQty }; lootedMaterialId = monster.dropMaterialId; } @@ -284,10 +318,12 @@ export class CombatService { } } + this.setCooldown(user.id, COOLDOWN_SINGLE_MS); return txResult.response; } async startMultiCombat(dto: StartCombatDto, user: User, count: number) { + this.checkCooldown(user.id); const monster = await this.monsterService.findOne(dto.monsterId); const txResult = await this.dataSource.transaction(async (manager) => { @@ -368,7 +404,7 @@ export class CombatService { 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); + await addMaterialInTx(manager, character.id, monster.dropMaterialId, dropQty); totals.loot.push({ name: 'matériau', quantity: dropQty }); lootedMaterialIds.push(monster.dropMaterialId); } @@ -428,6 +464,7 @@ export class CombatService { } } + this.setCooldown(user.id, COOLDOWN_MULTI_MS); return { mode: 'multi', count: txResult.combatsDone,