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:
37
src/profile/profile.controller.ts
Normal file
37
src/profile/profile.controller.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Controller, Get, Put, Body, Req, UseGuards, BadRequestException } from '@nestjs/common';
|
||||
import { ProfileService } from './profile.service';
|
||||
import { AuthGuard } from '../auth/guards/auth.guard';
|
||||
import { Request } from 'express';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Character } from '../character/entities/character.entity';
|
||||
|
||||
@Controller('api/profile')
|
||||
export class ProfileController {
|
||||
constructor(
|
||||
private readonly profileService: ProfileService,
|
||||
@InjectRepository(Character)
|
||||
private readonly characterRepo: Repository<Character>,
|
||||
) {}
|
||||
|
||||
@Get('me')
|
||||
@UseGuards(AuthGuard)
|
||||
async getProfile(@Req() req: Request) {
|
||||
const character = await this.getCharacter(req);
|
||||
return this.profileService.getProfile(character.id);
|
||||
}
|
||||
|
||||
@Put('title')
|
||||
@UseGuards(AuthGuard)
|
||||
async setTitle(@Body('title') title: string, @Req() req: Request) {
|
||||
const character = await this.getCharacter(req);
|
||||
return this.profileService.setActiveTitle(character.id, title);
|
||||
}
|
||||
|
||||
private async getCharacter(req: Request): Promise<Character> {
|
||||
const user = (req as any).user;
|
||||
const character = await this.characterRepo.findOne({ where: { userId: user.id } });
|
||||
if (!character) throw new BadRequestException('Aucun personnage trouvé');
|
||||
return character;
|
||||
}
|
||||
}
|
||||
18
src/profile/profile.module.ts
Normal file
18
src/profile/profile.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ProfileService } from './profile.service';
|
||||
import { ProfileController } from './profile.controller';
|
||||
import { Character } from '../character/entities/character.entity';
|
||||
import { PlayerAchievement } from '../achievement/player-achievement.entity';
|
||||
import { HallOfFame } from '../halloffame/hall-of-fame.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Character, PlayerAchievement, HallOfFame]),
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [ProfileController],
|
||||
providers: [ProfileService],
|
||||
})
|
||||
export class ProfileModule {}
|
||||
82
src/profile/profile.service.ts
Normal file
82
src/profile/profile.service.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Character } from '../character/entities/character.entity';
|
||||
import { PlayerAchievement } from '../achievement/player-achievement.entity';
|
||||
import { HallOfFame } from '../halloffame/hall-of-fame.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ProfileService {
|
||||
constructor(
|
||||
@InjectRepository(Character)
|
||||
private readonly characterRepo: Repository<Character>,
|
||||
@InjectRepository(PlayerAchievement)
|
||||
private readonly playerAchievementRepo: Repository<PlayerAchievement>,
|
||||
@InjectRepository(HallOfFame)
|
||||
private readonly hofRepo: Repository<HallOfFame>,
|
||||
) {}
|
||||
|
||||
async getProfile(characterId: string) {
|
||||
const character = await this.characterRepo.findOne({ where: { id: characterId } });
|
||||
if (!character) throw new BadRequestException('Personnage introuvable');
|
||||
|
||||
// Achievement stats
|
||||
const achievements = await this.playerAchievementRepo.find({
|
||||
where: { characterId },
|
||||
relations: ['achievement'],
|
||||
});
|
||||
const unlocked = achievements.filter((a) => a.unlocked);
|
||||
|
||||
// Total achievements count
|
||||
const totalCount = await this.playerAchievementRepo.manager
|
||||
.getRepository('Achievement')
|
||||
.count();
|
||||
|
||||
// Badges from Hall of Fame
|
||||
const badges = await this.hofRepo.find({
|
||||
where: { characterId },
|
||||
order: { period: 'DESC' },
|
||||
});
|
||||
|
||||
return {
|
||||
name: character.name,
|
||||
level: character.level,
|
||||
xp: character.xp,
|
||||
gold: character.gold,
|
||||
activeTitle: character.activeTitle,
|
||||
stats: {
|
||||
force: character.force,
|
||||
agilite: character.agilite,
|
||||
intelligence: character.intelligence,
|
||||
chance: character.chance,
|
||||
vitalite: character.vitalite,
|
||||
},
|
||||
achievements: {
|
||||
unlocked: unlocked.length,
|
||||
total: totalCount,
|
||||
percentage: totalCount > 0 ? Math.floor((unlocked.length / totalCount) * 100) : 0,
|
||||
},
|
||||
badges: badges.map((b) => ({ badge: b.badge, period: b.period, rank: b.rank })),
|
||||
};
|
||||
}
|
||||
|
||||
async setActiveTitle(characterId: string, title: string) {
|
||||
// Verify the player has unlocked this title
|
||||
if (title) {
|
||||
const hasTitle = await this.playerAchievementRepo
|
||||
.createQueryBuilder('pa')
|
||||
.innerJoin('pa.achievement', 'a')
|
||||
.where('pa.character_id = :characterId', { characterId })
|
||||
.andWhere('pa.unlocked = true')
|
||||
.andWhere('a.reward_title = :title', { title })
|
||||
.getCount();
|
||||
|
||||
if (!hasTitle) {
|
||||
throw new BadRequestException('Titre non débloqué');
|
||||
}
|
||||
}
|
||||
|
||||
await this.characterRepo.update(characterId, { activeTitle: title || null });
|
||||
return { activeTitle: title || null };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user