fix: cooldown serveur 2s/8s + loot dans transaction (élimine deadlock)
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, BadRequestException, ConflictException } from '@nestjs/common';
|
import { Injectable, BadRequestException, ConflictException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { Character } from '../character/entities/character.entity';
|
import { Character } from '../character/entities/character.entity';
|
||||||
import { Monster } from '../monster/monster.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 VICTORY_HP_REGEN_RATIO = 0.1; // +10% hpMax à la victoire
|
||||||
const DEFEAT_GOLD_LOSS_RATIO = 0.05; // perte 5% or à la défaite
|
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()
|
@Injectable()
|
||||||
export class CombatService {
|
export class CombatService {
|
||||||
|
private readonly cooldowns = new Map<string, number>(); // userId → timestamp
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Character)
|
@InjectRepository(Character)
|
||||||
private readonly characterRepository: Repository<Character>,
|
private readonly characterRepository: Repository<Character>,
|
||||||
@@ -69,7 +87,23 @@ export class CombatService {
|
|||||||
private readonly dataSource: DataSource,
|
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) {
|
async startCombat(dto: StartCombatDto, user: User) {
|
||||||
|
this.checkCooldown(user.id);
|
||||||
|
|
||||||
// Charger le monstre (hors transaction — lecture seule)
|
// Charger le monstre (hors transaction — lecture seule)
|
||||||
const monster = await this.monsterService.findOne(dto.monsterId);
|
const monster = await this.monsterService.findOne(dto.monsterId);
|
||||||
|
|
||||||
@@ -202,7 +236,7 @@ export class CombatService {
|
|||||||
if (result.winner === 'player' && monster.dropMaterialId) {
|
if (result.winner === 'player' && monster.dropMaterialId) {
|
||||||
const { dropRate, dropQty } = computeDropRate(character.level, monster.minLevel, monster.maxLevel);
|
const { dropRate, dropQty } = computeDropRate(character.level, monster.minLevel, monster.maxLevel);
|
||||||
if (Math.random() < dropRate) {
|
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 };
|
lootMaterial = { name: 'matériau', quantity: dropQty };
|
||||||
lootedMaterialId = monster.dropMaterialId;
|
lootedMaterialId = monster.dropMaterialId;
|
||||||
}
|
}
|
||||||
@@ -284,10 +318,12 @@ export class CombatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setCooldown(user.id, COOLDOWN_SINGLE_MS);
|
||||||
return txResult.response;
|
return txResult.response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async startMultiCombat(dto: StartCombatDto, user: User, count: number) {
|
async startMultiCombat(dto: StartCombatDto, user: User, count: number) {
|
||||||
|
this.checkCooldown(user.id);
|
||||||
const monster = await this.monsterService.findOne(dto.monsterId);
|
const monster = await this.monsterService.findOne(dto.monsterId);
|
||||||
|
|
||||||
const txResult = await this.dataSource.transaction(async (manager) => {
|
const txResult = await this.dataSource.transaction(async (manager) => {
|
||||||
@@ -368,7 +404,7 @@ export class CombatService {
|
|||||||
if (monster.dropMaterialId) {
|
if (monster.dropMaterialId) {
|
||||||
const { dropRate, dropQty } = computeDropRate(character.level, monster.minLevel, monster.maxLevel);
|
const { dropRate, dropQty } = computeDropRate(character.level, monster.minLevel, monster.maxLevel);
|
||||||
if (Math.random() < dropRate) {
|
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 });
|
totals.loot.push({ name: 'matériau', quantity: dropQty });
|
||||||
lootedMaterialIds.push(monster.dropMaterialId);
|
lootedMaterialIds.push(monster.dropMaterialId);
|
||||||
}
|
}
|
||||||
@@ -428,6 +464,7 @@ export class CombatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setCooldown(user.id, COOLDOWN_MULTI_MS);
|
||||||
return {
|
return {
|
||||||
mode: 'multi',
|
mode: 'multi',
|
||||||
count: txResult.combatsDone,
|
count: txResult.combatsDone,
|
||||||
|
|||||||
Reference in New Issue
Block a user