Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 27s
Le combat x5 émettait kill_any mais pas kill_monster — les quêtes ciblant un monstre spécifique ne progressaient pas en batch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
510 lines
22 KiB
TypeScript
510 lines
22 KiB
TypeScript
import { Injectable, BadRequestException, ConflictException } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/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';
|
||
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 { ItemService } from '../item/item.service';
|
||
import { MaterialService } from '../material/material.service';
|
||
import { CommunityService } from '../community/community.service';
|
||
import {
|
||
resolveCombat,
|
||
applyXpGain,
|
||
xpRequiredForLevel,
|
||
CombatantStats,
|
||
} from './combat.engine';
|
||
|
||
const COMBAT_ENDURANCE_COST = 5;
|
||
|
||
/**
|
||
* Drop rate variable basé sur la difficulté relative monstre vs joueur.
|
||
* Monstre facile = moins de drop, monstre difficile = plus de drop + quantité.
|
||
* Boss de zone (maxLevel ≥ 9 et spread ≥ 3) = 80% + 2-3 drops.
|
||
*/
|
||
function computeDropRate(
|
||
playerLevel: number,
|
||
monsterMinLevel: number,
|
||
monsterMaxLevel: number,
|
||
): { dropRate: number; dropQty: number } {
|
||
const monsterAvgLevel = (monsterMinLevel + monsterMaxLevel) / 2;
|
||
const diff = monsterAvgLevel - playerLevel;
|
||
const isBoss = (monsterMaxLevel - monsterMinLevel) >= 3 && monsterMaxLevel >= 9;
|
||
|
||
if (isBoss) {
|
||
return { dropRate: 0.80, dropQty: 2 + (Math.random() < 0.5 ? 1 : 0) }; // 2-3
|
||
}
|
||
if (diff >= 2) {
|
||
return { dropRate: 0.60, dropQty: 1 + (Math.random() < 0.3 ? 1 : 0) }; // 1-2
|
||
}
|
||
if (diff >= 0) {
|
||
return { dropRate: 0.50, dropQty: 1 + (Math.random() < 0.2 ? 1 : 0) }; // 1-2
|
||
}
|
||
if (diff >= -2) {
|
||
return { dropRate: 0.40, dropQty: 1 };
|
||
}
|
||
// Très facile (level >> monstre)
|
||
return { dropRate: 0.25, dropQty: 1 };
|
||
}
|
||
const DEFEAT_ENDURANCE_PENALTY = 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<string, number>(); // userId → timestamp
|
||
|
||
constructor(
|
||
@InjectRepository(Character)
|
||
private readonly characterRepository: Repository<Character>,
|
||
@InjectRepository(CombatLog)
|
||
private readonly combatLogRepository: Repository<CombatLog>,
|
||
private readonly monsterService: MonsterService,
|
||
private readonly itemService: ItemService,
|
||
private readonly materialService: MaterialService,
|
||
private readonly communityService: CommunityService,
|
||
private readonly eventEmitter: EventEmitter2,
|
||
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);
|
||
|
||
// Transaction isolée — empêche les combats simultanés sur le même perso
|
||
const txResult = await this.dataSource.transaction(async (manager) => {
|
||
// SELECT ... FOR UPDATE — verrouille le personnage
|
||
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é');
|
||
|
||
// Calculer l'endurance actuelle (lazy pattern)
|
||
const elapsedMinutes = (Date.now() - character.lastEnduranceTs.getTime()) / 60_000;
|
||
const recharge = Math.floor(elapsedMinutes / 3);
|
||
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');
|
||
}
|
||
|
||
// Charger l'équipement actif du personnage
|
||
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;
|
||
|
||
// Item stat bonuses
|
||
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);
|
||
|
||
// Construire les stats des combattants
|
||
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,
|
||
};
|
||
|
||
// 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;
|
||
character.totalGoldEarned = Number(character.totalGoldEarned ?? 0) + 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 le personnage (dans la transaction)
|
||
character.hpCurrent = newHp;
|
||
character.enduranceSaved = newEnduranceSaved;
|
||
character.lastEnduranceTs = new Date();
|
||
await manager.save(character);
|
||
|
||
// Apply XP boost from community (dans la transaction)
|
||
if (result.winner === 'player') {
|
||
const xpBoost = await this.communityService.getActiveMultiplier('xp_boost');
|
||
if (xpBoost > 1.0) {
|
||
const bonusXp = Math.floor(result.xpEarned * (xpBoost - 1));
|
||
if (bonusXp > 0) {
|
||
const boosted = applyXpGain(character.level, character.xp, bonusXp);
|
||
character.xp = boosted.newXp;
|
||
character.level = boosted.newLevel;
|
||
character.statPoints = (character.statPoints ?? 0) + boosted.statPointsGained;
|
||
await manager.save(character);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Loot matériaux — drop rate variable par difficulté relative
|
||
let lootMaterial: { name: string; quantity: number } | null = null;
|
||
let lootedMaterialId: string | null = null;
|
||
if (result.winner === 'player' && monster.dropMaterialId) {
|
||
const { dropRate, dropQty } = computeDropRate(character.level, monster.minLevel, monster.maxLevel);
|
||
if (Math.random() < dropRate) {
|
||
await addMaterialInTx(manager, character.id, monster.dropMaterialId, dropQty);
|
||
lootMaterial = { name: 'matériau', quantity: dropQty };
|
||
lootedMaterialId = monster.dropMaterialId;
|
||
}
|
||
}
|
||
|
||
// 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,
|
||
lootMaterialId: lootedMaterialId,
|
||
lootQuantity: lootMaterial?.quantity ?? 0,
|
||
});
|
||
await manager.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.`);
|
||
}
|
||
|
||
if (lootMaterial) {
|
||
summaryParts.push(`Loot : ${lootMaterial.quantity} matériau${lootMaterial.quantity > 1 ? 'x' : ''} obtenu${lootMaterial.quantity > 1 ? 's' : ''} !`);
|
||
}
|
||
|
||
return {
|
||
characterId: character.id,
|
||
lootedMaterialId,
|
||
response: {
|
||
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,
|
||
loot: lootMaterial,
|
||
},
|
||
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 émis APRÈS la transaction (fire-and-forget)
|
||
if (txResult.response.winner === 'player') {
|
||
const cid = txResult.characterId;
|
||
this.eventEmitter.emit('achievement.check', { characterId: cid, type: 'combat_wins', increment: 1 });
|
||
this.eventEmitter.emit('achievement.check', { characterId: cid, type: 'level_reached', increment: 0, absolute: txResult.response.character.level });
|
||
this.eventEmitter.emit('achievement.check', { characterId: cid, type: 'gold_accumulated', increment: 0, absolute: txResult.response.rewards.gold });
|
||
this.eventEmitter.emit('community.contribute', { characterId: cid, type: 'total_monsters_killed', increment: 1 });
|
||
this.eventEmitter.emit('community.contribute', { characterId: cid, type: 'total_gold_earned', increment: txResult.response.rewards.gold });
|
||
this.eventEmitter.emit('quest.progress', { characterId: cid, type: 'kill_any', increment: 1, zone: monster.zone });
|
||
this.eventEmitter.emit('quest.progress', { characterId: cid, type: 'kill_monster', targetId: monster.id, increment: 1, zone: monster.zone });
|
||
if (txResult.lootedMaterialId) {
|
||
this.eventEmitter.emit('quest.progress', { characterId: cid, type: 'gather_material', targetId: txResult.lootedMaterialId, increment: 1 });
|
||
}
|
||
}
|
||
|
||
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) => {
|
||
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;
|
||
|
||
let combatLootMatId: string | null = null;
|
||
let combatLootQty = 0;
|
||
|
||
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.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 addMaterialInTx(manager, character.id, monster.dropMaterialId, dropQty);
|
||
totals.loot.push({ name: 'matériau', quantity: dropQty });
|
||
lootedMaterialIds.push(monster.dropMaterialId);
|
||
combatLootMatId = monster.dropMaterialId;
|
||
combatLootQty = dropQty;
|
||
}
|
||
}
|
||
} 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 += goldLost;
|
||
}
|
||
|
||
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,
|
||
lootMaterialId: combatLootMatId, lootQuantity: combatLootQty,
|
||
}));
|
||
|
||
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 });
|
||
this.eventEmitter.emit('quest.progress', { characterId: cid, type: 'kill_monster', targetId: monster.id, 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 });
|
||
}
|
||
}
|
||
|
||
this.setCooldown(user.id, COOLDOWN_MULTI_MS);
|
||
return {
|
||
mode: 'multi',
|
||
count: txResult.combatsDone,
|
||
totals: txResult.totals,
|
||
character: txResult.character,
|
||
};
|
||
}
|
||
|
||
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,
|
||
lootQuantity: true,
|
||
createdAt: true,
|
||
monster: { id: true, name: true, minLevel: true, maxLevel: true } as any,
|
||
},
|
||
});
|
||
}
|
||
}
|