Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Rukayat Zakariyau committed Feb 24, 2025
2 parents 7a5e4ef + be70c17 commit 60c3f7a
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 0 deletions.
21 changes: 21 additions & 0 deletions backend/src/progression/controllers/progression.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// src/progression/controllers/progression.controller.ts
import { Controller, Post, Body, Get, Param, UseGuards } from '@nestjs/common';
import { ProgressionService } from '../services/progression.service';
import { XpEventDto } from '../dtos/xp-event.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';

@Controller('progression')
@UseGuards(JwtAuthGuard)
export class ProgressionController {
constructor(private readonly progressionService: ProgressionService) {}

@Post('xp')
addXp(@Body() xpEvent: XpEventDto) {
return this.progressionService.addXp(xpEvent);
}

@Get(':userId')
getPlayerStats(@Param('userId') userId: string) {
return this.progressionService.getOrCreatePlayerStats(userId);
  }
}
24 changes: 24 additions & 0 deletions backend/src/progression/dtos/xp-event.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// src/progression/dtos/xp-event.dto.ts
import { IsString, IsNumber, IsEnum, IsOptional } from 'class-validator';

export enum XpSource {
GAME_WIN = 'GAME_WIN',
ACHIEVEMENT = 'ACHIEVEMENT',
QUEST = 'QUEST',
DAILY_BONUS = 'DAILY_BONUS',
}

export class XpEventDto {
@IsString()
userId: string;

@IsNumber()
amount: number;

@IsEnum(XpSource)
source: XpSource;

@IsOptional()
@IsString()
metadata?: string;
}
24 changes: 24 additions & 0 deletions backend/src/progression/entities/player-level.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// src/progression/entities/player-level.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn } from 'typeorm';
import { PlayerStats } from './player-stats.entity';

@Entity()
export class PlayerLevel {
@PrimaryGeneratedColumn('uuid')
id: string;

@ManyToOne(() => PlayerStats, stats => stats.levelHistory)
playerStats: PlayerStats;

@Column({ type: 'int' })
level: number;

@Column({ type: 'int' })
xpGained: number;

@Column({ type: 'int' })
totalXp: number;

@CreateDateColumn()
achievedAtDate;
}
21 changes: 21 additions & 0 deletions backend/src/progression/entities/player-skill.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// src/progression/entities/player-skill.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
import { PlayerStats } from './player-stats.entity';

@Entity()
export class PlayerSkill {
@PrimaryGeneratedColumn('uuid')
id: string;

@ManyToOne(() => PlayerStats, stats => stats.skills)
playerStats: PlayerStats;

@Column()
name: string;

@Column({ type: 'int', default: 0 })
level: number;

@Column({ type: 'int', default: 0 })
experiencenumber;
}
43 changes: 43 additions & 0 deletions backend/src/progression/entities/player-stats.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// src/progression/entities/player-stats.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { PlayerLevel } from './player-level.entity';
import { PlayerSkill } from './player-skill.entity';

@Entity()
export class PlayerStats {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
userId: string;

@Column({ type: 'int', default: 0 })
totalXp: number;

@Column({ type: 'int', default: 1 })
level: number;

@Column({ type: 'int', default: 0 })
skillRating: number;

@Column()
rank: string;

@Column('json')
achievements: string[];

@Column('json')
milestones: string[];

@OneToMany(() => PlayerLevel, level => level.playerStats)
levelHistory: PlayerLevel[];

@OneToMany(() => PlayerSkill, skill => skill.playerStats)
skills: PlayerSkill[];

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAtDate;
}
25 changes: 25 additions & 0 deletions backend/src/progression/gateways/progression.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// src/progression/gateways/progression.gateway.ts
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server } from 'socket.io';
import { OnEvent } from '@nestjs/event-emitter';

@WebSocketGateway()
export class ProgressionGateway {
@WebSocketServer()
server: Server;

@OnEvent('progression.xp.added')
handleXpAdded(payload: any) {
this.server.to(payload.userId).emit('xpGained', payload);
}

@OnEvent('progression.level.up')
handleLevelUp(payload: any) {
this.server.to(payload.userId).emit('levelUp', payload);
}

@OnEvent('progression.rank.changed')
handleRankChange(payload: any) {
this.server.to(payload.userId).emit('rankChanged', payload);
  }
}
19 changes: 19 additions & 0 deletions backend/src/progression/progression.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// src/progression/progression.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProgressionController } from './controllers/progression.controller';
import { ProgressionService } from './services/progression.service';
import { ProgressionGateway } from './gateways/progression.gateway';
import { PlayerStats } from './entities/player-stats.entity';
import { PlayerLevel } from './entities/player-level.entity';
import { PlayerSkill } from './entities/player-skill.entity';

@Module({
imports: [
TypeOrmModule.forFeature([PlayerStats, PlayerLevel, PlayerSkill]),
],
controllers: [ProgressionController],
providers: [ProgressionService, ProgressionGateway],
exports: [ProgressionService],
})
export class ProgressionModule {}
145 changes: 145 additions & 0 deletions backend/src/progression/services/progression.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// src/progression/services/progression.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PlayerStats } from '../entities/player-stats.entity';
import { PlayerLevel } from '../entities/player-level.entity';
import { XpEventDto } from '../dtos/xp-event.dto';

@Injectable()
export class ProgressionService {
constructor(
@InjectRepository(PlayerStats)
private playerStatsRepository: Repository<PlayerStats>,
@InjectRepository(PlayerLevel)
private playerLevelRepository: Repository<PlayerLevel>,
private eventEmitter: EventEmitter2,
) {}

async addXp(xpEvent: XpEventDto): Promise<PlayerStats> {
const playerStats = await this.getOrCreatePlayerStats(xpEvent.userId);
const oldLevel = playerStats.level;

playerStats.totalXp += xpEvent.amount;

// Calculate new level
const newLevel = this.calculateLevel(playerStats.totalXp);
if (newLevel > oldLevel) {
await this.handleLevelUp(playerStats, oldLevel, newLevel);
}

playerStats.level = newLevel;

// Update skill rating and rank
await this.updateSkillRating(playerStats);
await this.updateRank(playerStats);

const updatedStats = await this.playerStatsRepository.save(playerStats);

this.eventEmitter.emit('progression.xp.added', {
userId: xpEvent.userId,
amount: xpEvent.amount,
source: xpEvent.source,
newTotal: updatedStats.totalXp,
oldLevel,
newLevel: updatedStats.level,
});

return updatedStats;
}

private async handleLevelUp(
playerStats: PlayerStats,
oldLevel: number,
newLevel: number,
): Promise<void> {
// Record level up history
const levelUpEvent = this.playerLevelRepository.create({
playerStats,
level: newLevel,
xpGained: this.calculateLevelXp(newLevel) - this.calculateLevelXp(oldLevel),
totalXp: playerStats.totalXp,
});
await this.playerLevelRepository.save(levelUpEvent);

// Emit level up event
this.eventEmitter.emit('progression.level.up', {
userId: playerStats.userId,
oldLevel,
newLevel,
rewards: await this.calculateLevelUpRewards(newLevel),
});
}

private calculateLevel(totalXp: number): number {
// Simplified level calculation
return Math.floor(Math.sqrt(totalXp / 100)) + 1;
}

private calculateLevelXp(level: number): number {
// XP required for level
return Math.pow(level - 1, 2) * 100;
}

private async calculateLevelUpRewards(level: number): Promise<any> {
// Implement reward calculation based on level
return {
coins: level * 100,
items: [reward_${level}],
};
}

private async updateSkillRating(playerStats: PlayerStats): Promise<void> {
// Implement ELO or similar rating system
const baseRating = playerStats.level * 100;
const performanceBonus = Math.floor(Math.random() * 50); // Example
playerStats.skillRating = baseRating + performanceBonus;
}

private async updateRank(playerStats: PlayerStats): Promise<void> {
// Define rank thresholds
const ranks = [
{ name: 'Bronze', threshold: 0 },
{ name: 'Silver', threshold: 1000 },
{ name: 'Gold', threshold: 2000 },
{ name: 'Platinum', threshold: 3000 },
{ name: 'Diamond', threshold: 4000 },
];

// Find appropriate rank
const newRank = ranks
.reverse()
.find(rank => playerStats.skillRating >= rank.threshold);

if (newRank && newRank.name !== playerStats.rank) {
const oldRank = playerStats.rank;
playerStats.rank = newRank.name;

this.eventEmitter.emit('progression.rank.changed', {
userId: playerStats.userId,
oldRank,
newRank: newRank.name,
});
}
}

private async getOrCreatePlayerStats(userId: string): Promise<PlayerStats> {
let playerStats = await this.playerStatsRepository.findOne({
where: { userId },
relations: ['levelHistory', 'skills'],
});

if (!playerStats) {
playerStats = this.playerStatsRepository.create({
userId,
rank: 'Bronze',
achievements: [],
milestones: [],
});
await this.playerStatsRepository.save(playerStats);
}

return playerStats;
  }
}

0 comments on commit 60c3f7a

Please sign in to comment.