import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { CharacterItem } from '../item/character-item.entity'; import { Character } from '../character/entities/character.entity'; import { User } from '../user/user.entity'; const MAX_FORGE_LEVEL = 5; const FORGE_BONUS_PER_LEVEL = 2; // Coût en or par niveau cible const FORGE_GOLD_COST: Record = { 1: 50, 2: 100, 3: 250, 4: 500, 5: 1000, }; const FORGE_ENDURANCE_COST = 10; // Risque d'échec par niveau cible (GDD exact) const FORGE_FAIL_CHANCE: Record = { 1: 0, 2: 0, 3: 0.20, 4: 0.30, 5: 0.40, }; @Injectable() export class ForgeService { constructor( @InjectRepository(CharacterItem) private readonly charItemRepository: Repository, @InjectRepository(Character) private readonly characterRepository: Repository, private readonly eventEmitter: EventEmitter2, private readonly dataSource: DataSource, ) {} async upgradeItem(charItemId: string, user: User) { return this.dataSource.transaction(async (manager) => { // Lock le personnage const char = await manager .getRepository(Character) .createQueryBuilder('c') .setLock('pessimistic_write') .where('c.user_id = :userId', { userId: user.id }) .getOne(); if (!char) throw new BadRequestException('Aucun personnage trouvé'); // Lock l'item const charItem = await manager .getRepository(CharacterItem) .createQueryBuilder('ci') .setLock('pessimistic_write') .leftJoinAndSelect('ci.item', 'item') .where('ci.id = :id', { id: charItemId }) .andWhere('ci.character_id = :cid', { cid: char.id }) .getOne(); if (!charItem) throw new NotFoundException('Item non trouvé dans l\'inventaire'); if (charItem.forgeLevel >= MAX_FORGE_LEVEL) { throw new BadRequestException(`Niveau de forge maximum atteint (${MAX_FORGE_LEVEL})`); } const targetLevel = charItem.forgeLevel + 1; const goldCost = FORGE_GOLD_COST[targetLevel] ?? 0; // Vérifier endurance const elapsedMinutes = (Date.now() - char.lastEnduranceTs.getTime()) / 60_000; const recharge = Math.floor(elapsedMinutes / 6); const enduranceCurrent = Math.min(char.enduranceSaved + recharge, char.enduranceMax); if (enduranceCurrent < FORGE_ENDURANCE_COST) { throw new BadRequestException( `Endurance insuffisante (${enduranceCurrent}/${FORGE_ENDURANCE_COST} requis)`, ); } // Vérifier or if (char.gold < goldCost) { throw new BadRequestException( `Or insuffisant (${char.gold}/${goldCost} requis)`, ); } // Déduire les coûts (même en cas d'échec) char.gold -= goldCost; char.enduranceSaved = enduranceCurrent - FORGE_ENDURANCE_COST; char.lastEnduranceTs = new Date(); const failChance = FORGE_FAIL_CHANCE[targetLevel] ?? 0; const success = Math.random() >= failChance; if (success) { charItem.forgeLevel = targetLevel; await manager.save(charItem); this.eventEmitter.emit('achievement.check', { characterId: char.id, type: 'forge_upgrades', increment: 1, }); this.eventEmitter.emit('community.contribute', { characterId: char.id, type: 'total_forge_upgrades', increment: 1, }); this.eventEmitter.emit('quest.progress', { characterId: char.id, type: 'forge_item', increment: 1, }); } await manager.save(char); const statLabel = charItem.item.type === 'weapon' ? `+${FORGE_BONUS_PER_LEVEL} ATK` : `+${FORGE_BONUS_PER_LEVEL} DEF`; if (success) { return { success: true, forgeLevel: charItem.forgeLevel, item: charItem.item.name, goldSpent: goldCost, message: `Forge réussie ! ${charItem.item.name} [+${charItem.forgeLevel}] (${statLabel}). -${goldCost} Or.`, }; } return { success: false, forgeLevel: charItem.forgeLevel, item: charItem.item.name, goldSpent: goldCost, message: `Échec de forge ! ${charItem.item.name} reste au niveau [+${charItem.forgeLevel}]. -${goldCost} Or perdus.`, }; }); } }