-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/RUKAYAT-CODER/lyricsflip
- Loading branch information
Showing
8 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
backend/src/progression/controllers/progression.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
achievedAt: Date; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) | ||
experience: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
updatedAt: Date; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
145
backend/src/progression/services/progression.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |