Files
TetaRdPG/src/forge/forge.service.ts
Tetardtek eafac3d8c7
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 31s
feat: endurance tickets — coûts visibles partout + budget dashboard
Combat: coût 5 affiché, compteur "X combats possibles", bouton disabled
Forge: coût 10 + or affiché (baissé de 15 à 10), bouton disabled
Dashboard: indicateur budget "X combats · Y forges · Z repos"
Repos: coût 10 affiché, disabled si insuffisant
2026-03-24 17:09:06 +01:00

138 lines
4.4 KiB
TypeScript

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<number, number> = {
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<number, number> = {
1: 0,
2: 0,
3: 0.20,
4: 0.30,
5: 0.40,
};
@Injectable()
export class ForgeService {
constructor(
@InjectRepository(CharacterItem)
private readonly charItemRepository: Repository<CharacterItem>,
@InjectRepository(Character)
private readonly characterRepository: Repository<Character>,
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.`,
};
});
}
}