feat(sprint3): items + forge + craft + loot — équipement, artisanat lazy-calc, forge risque GDD

This commit is contained in:
2026-03-15 08:22:20 +01:00
parent 6d1230d16a
commit 23f7dd0f3c
25 changed files with 1169 additions and 2 deletions

View File

@@ -0,0 +1,16 @@
import { Controller, Post, Param, UseGuards, Req } from '@nestjs/common';
import { Request } from 'express';
import { ForgeService } from './forge.service';
import { AuthGuard } from '../auth/guards/auth.guard';
import { User } from '../user/user.entity';
@Controller('forge')
@UseGuards(AuthGuard)
export class ForgeController {
constructor(private readonly forgeService: ForgeService) {}
@Post('upgrade/:charItemId')
upgrade(@Param('charItemId') charItemId: string, @Req() req: Request & { user: User }) {
return this.forgeService.upgradeItem(charItemId, req.user);
}
}

14
src/forge/forge.module.ts Normal file
View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { ForgeService } from './forge.service';
import { ForgeController } from './forge.controller';
import { AuthModule } from '../auth/auth.module';
import { ItemModule } from '../item/item.module';
import { Character } from '../character/entities/character.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([Character]), AuthModule, ItemModule],
controllers: [ForgeController],
providers: [ForgeService],
})
export class ForgeModule {}

View File

@@ -0,0 +1,68 @@
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
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; // +2 attack (weapon) ou +2 defense (armor) par niveau affiché
// 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>,
) {}
async upgradeItem(charItemId: string, user: User) {
const char = await this.characterRepository.findOne({ where: { userId: user.id } });
if (!char) throw new BadRequestException('Aucun personnage trouvé');
const charItem = await this.charItemRepository.findOne({
where: { id: charItemId, characterId: char.id },
});
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 failChance = FORGE_FAIL_CHANCE[targetLevel] ?? 0;
const success = Math.random() >= failChance;
if (success) {
charItem.forgeLevel = targetLevel;
await this.charItemRepository.save(charItem);
const statLabel = charItem.item.type === 'weapon'
? `+${FORGE_BONUS_PER_LEVEL} ATK`
: `+${FORGE_BONUS_PER_LEVEL} DEF`;
return {
success: true,
forgeLevel: charItem.forgeLevel,
item: charItem.item.name,
message: `Forge réussie ! ${charItem.item.name} [+${charItem.forgeLevel}] (${statLabel}).`,
};
}
return {
success: false,
forgeLevel: charItem.forgeLevel,
item: charItem.item.name,
message: `Échec de forge ! ${charItem.item.name} reste au niveau [+${charItem.forgeLevel}].`,
};
}
}