All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 35s
- Fix vitalité: HP initial = 100 + (vitalité-1)×10 - Arme de départ: Bâton de Roseau équipé à la création - Rebalance forge: niv3 200, niv4 400, niv5 700 (−30%) - Confirmation avant vente d'item (confirm dialog) - Fix forge costs dupliqués (shop sellback + inventaire)
138 lines
4.4 KiB
TypeScript
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: 200,
|
|
4: 400,
|
|
5: 700,
|
|
};
|
|
|
|
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 / 3);
|
|
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.`,
|
|
};
|
|
});
|
|
}
|
|
}
|