feat(sprint4): achievements, community goals, hall of fame, profile
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 37s
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 37s
4 modules: achievement (15 succès, 5 catégories, 3 paliers), community (objectifs collectifs + boosts globaux), halloffame (classement mensuel), profile (titre actif + badges + % progression). Event-driven: combat/forge/craft émettent des events via @nestjs/event-emitter. Character entity: +activeTitle, +totalGoldEarned. Seeds: 15 achievements + 3 community goals.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
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';
|
||||
@@ -9,6 +10,7 @@ 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,
|
||||
@@ -31,6 +33,8 @@ export class CombatService {
|
||||
private readonly monsterService: MonsterService,
|
||||
private readonly itemService: ItemService,
|
||||
private readonly materialService: MaterialService,
|
||||
private readonly communityService: CommunityService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
async startCombat(dto: StartCombatDto, user: User) {
|
||||
@@ -126,12 +130,63 @@ export class CombatService {
|
||||
character.gold = Math.max(0, character.gold - goldLost);
|
||||
}
|
||||
|
||||
// Track total gold earned (for achievements)
|
||||
if (result.winner === 'player') {
|
||||
character.totalGoldEarned = Number(character.totalGoldEarned ?? 0) + result.goldEarned;
|
||||
}
|
||||
|
||||
// Sauvegarder l'endurance (lazy reset)
|
||||
character.hpCurrent = newHp;
|
||||
character.enduranceSaved = newEnduranceSaved;
|
||||
character.lastEnduranceTs = new Date();
|
||||
await this.characterRepository.save(character);
|
||||
|
||||
// Emit achievement & community events
|
||||
if (result.winner === 'player') {
|
||||
this.eventEmitter.emit('achievement.check', {
|
||||
characterId: character.id,
|
||||
type: 'combat_wins',
|
||||
increment: 1,
|
||||
});
|
||||
this.eventEmitter.emit('achievement.check', {
|
||||
characterId: character.id,
|
||||
type: 'level_reached',
|
||||
increment: 0,
|
||||
absolute: character.level,
|
||||
});
|
||||
this.eventEmitter.emit('achievement.check', {
|
||||
characterId: character.id,
|
||||
type: 'gold_accumulated',
|
||||
increment: 0,
|
||||
absolute: Number(character.totalGoldEarned),
|
||||
});
|
||||
this.eventEmitter.emit('community.contribute', {
|
||||
characterId: character.id,
|
||||
type: 'total_monsters_killed',
|
||||
increment: 1,
|
||||
});
|
||||
this.eventEmitter.emit('community.contribute', {
|
||||
characterId: character.id,
|
||||
type: 'total_gold_earned',
|
||||
increment: result.goldEarned,
|
||||
});
|
||||
}
|
||||
|
||||
// Apply XP boost from community
|
||||
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 this.characterRepository.save(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loot matériaux — 40% de chance après victoire si le monstre a un drop_material_id
|
||||
let lootMaterial: { name: string; quantity: number } | null = null;
|
||||
if (result.winner === 'player' && monster.dropMaterialId && Math.random() < 0.4) {
|
||||
|
||||
Reference in New Issue
Block a user