-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(back,xpsys): move xp systems logic to xp-systems
- Loading branch information
Showing
8 changed files
with
328 additions
and
409 deletions.
There are no files selected for viewing
326 changes: 7 additions & 319 deletions
326
apps/backend/src/app/modules/xp-systems/xp-systems.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 |
---|---|---|
@@ -1,321 +1,9 @@ | ||
import { | ||
DtoFactory, | ||
UpdateXpSystemsDto, | ||
XpSystemsDto | ||
} from '@momentum/backend/dto'; | ||
import { | ||
CosXpParams, | ||
RankXpGain, | ||
RankXpParams, | ||
TrackType, | ||
XpSystems | ||
} from '@momentum/constants'; | ||
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; | ||
import { instanceToPlain } from 'class-transformer'; | ||
import { EXTENDED_PRISMA_SERVICE } from '../database/db.constants'; | ||
import { ExtendedPrismaService } from '../database/prisma.extension'; | ||
|
||
const DEFAULT_RANK_XP: RankXpParams = { | ||
top10: { | ||
WRPoints: 3000, | ||
rankPercentages: [1, 0.75, 0.68, 0.61, 0.57, 0.53, 0.505, 0.48, 0.455, 0.43] | ||
}, | ||
formula: { | ||
A: 50000, | ||
B: 49 | ||
}, | ||
groups: { | ||
maxGroups: 4, | ||
groupScaleFactors: [1, 1.5, 2, 2.5], | ||
groupExponents: [0.5, 0.56, 0.62, 0.68], | ||
groupMinSizes: [10, 45, 125, 250], | ||
groupPointPcts: [0.2, 0.13, 0.07, 0.03] | ||
} | ||
}; | ||
|
||
const DEFAULT_COS_XP: CosXpParams = { | ||
levels: { | ||
maxLevels: 500, | ||
startingValue: 0, | ||
linearScaleBaseIncrease: 1000, | ||
linearScaleInterval: 10, | ||
linearScaleIntervalMultiplier: 1, | ||
staticScaleStart: 101, | ||
staticScaleBaseMultiplier: 1.5, | ||
staticScaleInterval: 25, | ||
staticScaleIntervalMultiplier: 0.5 | ||
}, | ||
completions: { | ||
unique: { | ||
tierScale: { | ||
linear: 2500, | ||
staged: 2500 | ||
} | ||
}, | ||
repeat: { | ||
tierScale: { | ||
linear: 20, | ||
staged: 40, | ||
stages: 5, | ||
bonus: 40 | ||
} | ||
} | ||
} | ||
}; | ||
import { Injectable } from '@nestjs/common'; | ||
import { XpSystems } from '@momentum/xp-systems'; | ||
|
||
/* | ||
* This logic is all in a shared library now, we just need an extended class | ||
* with @Injectable for DI. | ||
*/ | ||
@Injectable() | ||
export class XpSystemsService implements OnModuleInit { | ||
constructor( | ||
@Inject(EXTENDED_PRISMA_SERVICE) private readonly db: ExtendedPrismaService | ||
) {} | ||
|
||
private readonly logger = new Logger('XP Systems'); | ||
|
||
private _cosXpParams: CosXpParams; | ||
private _rankXpParams: RankXpParams; | ||
private xpInLevels: number[]; | ||
private xpForLevels: number[]; | ||
|
||
public get xpParams(): XpSystems { | ||
return { | ||
cosXP: this._cosXpParams, | ||
rankXP: this._rankXpParams | ||
}; | ||
} | ||
|
||
public get rankXpParams(): RankXpParams { | ||
return this._rankXpParams; | ||
} | ||
|
||
public get cosXpParams(): CosXpParams { | ||
return this._cosXpParams; | ||
} | ||
|
||
async onModuleInit() { | ||
const params = await this.db.xpSystems.findUnique({ | ||
where: { id: 1 } | ||
}); | ||
|
||
if (params) { | ||
this._rankXpParams = params.rankXP as RankXpParams; | ||
this._cosXpParams = params.cosXP as CosXpParams; | ||
} else { | ||
this.logger.log('Initialising empty XP parameters with defaults'); | ||
|
||
if ((await this.db.xpSystems.count()) > 0) { | ||
// The only time this can ever really happen is during tests, that's no | ||
// big deal, just warn | ||
this.logger.warn( | ||
"Tried to init XP systems, but the table wasn't empty!" | ||
); | ||
return; | ||
} | ||
|
||
await this.db.xpSystems.create({ | ||
data: { id: 1, rankXP: DEFAULT_RANK_XP, cosXP: DEFAULT_COS_XP } | ||
}); | ||
|
||
this._rankXpParams = DEFAULT_RANK_XP; | ||
this._cosXpParams = DEFAULT_COS_XP; | ||
} | ||
|
||
this.generateLevelsArrays(); | ||
|
||
this.logger.log('Initialised XP systems!'); | ||
} | ||
|
||
getCosmeticXpInLevel(level: number): number { | ||
const levels = this._cosXpParams.levels; | ||
|
||
if (!levels || level < 0 || level > levels.maxLevels) return -1; | ||
|
||
if (level < levels.staticScaleStart) { | ||
return ( | ||
levels.startingValue + | ||
levels.linearScaleBaseIncrease * | ||
level * | ||
(levels.linearScaleIntervalMultiplier * | ||
Math.ceil(level / levels.linearScaleInterval)) | ||
); | ||
} else { | ||
return ( | ||
levels.linearScaleBaseIncrease * | ||
(levels.staticScaleStart - 1) * | ||
(levels.linearScaleIntervalMultiplier * | ||
Math.ceil( | ||
(levels.staticScaleStart - 1) / levels.linearScaleInterval | ||
)) * | ||
(level >= levels.staticScaleStart + levels.staticScaleInterval | ||
? levels.staticScaleBaseMultiplier + | ||
Math.floor( | ||
(level - levels.staticScaleStart) / levels.staticScaleInterval | ||
) * | ||
levels.staticScaleIntervalMultiplier | ||
: levels.staticScaleBaseMultiplier) | ||
); | ||
} | ||
} | ||
|
||
getCosmeticXpForLevel(level: number): number { | ||
if ( | ||
!this._cosXpParams || | ||
level < 0 || | ||
level > this._cosXpParams.levels.maxLevels | ||
) | ||
return -1; | ||
return this.xpForLevels[level]; | ||
} | ||
|
||
getCosmeticXpForCompletion( | ||
tier: number, | ||
type: TrackType, | ||
isLinear: boolean, | ||
isUnique: boolean | ||
): number { | ||
let xp: number; | ||
const initialScale = XpSystemsService.getInitialScale(tier); | ||
|
||
if (type === TrackType.BONUS) { | ||
// This needs to probably change (0.9.0+) | ||
const baseBonus = Math.ceil( | ||
(this._cosXpParams.completions.unique.tierScale.linear * | ||
XpSystemsService.getInitialScale(3) + | ||
this._cosXpParams.completions.unique.tierScale.linear * | ||
XpSystemsService.getInitialScale(4)) / | ||
2 | ||
); | ||
xp = isUnique | ||
? baseBonus | ||
: Math.ceil( | ||
baseBonus / this._cosXpParams.completions.repeat.tierScale.bonus | ||
); | ||
} else { | ||
const baseXP = | ||
(isLinear | ||
? this._cosXpParams.completions.unique.tierScale.linear | ||
: this._cosXpParams.completions.unique.tierScale.staged) * | ||
initialScale; | ||
if (type === TrackType.STAGE) { | ||
// Unique counts as a repeat | ||
xp = Math.ceil( | ||
baseXP / | ||
this._cosXpParams.completions.repeat.tierScale.staged / | ||
this._cosXpParams.completions.repeat.tierScale.stages | ||
); | ||
} else { | ||
xp = isUnique | ||
? baseXP | ||
: Math.ceil( | ||
baseXP / | ||
(isLinear | ||
? this._cosXpParams.completions.repeat.tierScale.linear | ||
: this._cosXpParams.completions.repeat.tierScale.staged) | ||
); | ||
} | ||
} | ||
|
||
return xp; | ||
} | ||
|
||
getRankXpForRank(rank: number, completions: number): RankXpGain { | ||
const rankGain: RankXpGain = { | ||
rankXP: 0, | ||
group: { | ||
groupXP: 0, | ||
groupNum: -1 | ||
}, | ||
formula: 0, | ||
top10: 0 | ||
}; | ||
|
||
// Regardless of run, we want to calculate formula points | ||
const formulaPoints = Math.ceil( | ||
this._rankXpParams.formula.A / (rank + this._rankXpParams.formula.B) | ||
); | ||
rankGain.formula = formulaPoints; | ||
rankGain.rankXP += formulaPoints; | ||
|
||
// Calculate Top10 points if in there | ||
if (rank <= 10) { | ||
const top10Points = Math.ceil( | ||
this._rankXpParams.top10.rankPercentages[rank - 1] * | ||
this._rankXpParams.top10.WRPoints | ||
); | ||
rankGain.top10 = top10Points; | ||
rankGain.rankXP += top10Points; | ||
} else { | ||
// Otherwise we calculate group points depending on group location | ||
|
||
// Going to have to calculate groupSizes dynamically | ||
const groupSizes = []; | ||
for (let i = 0; i < this._rankXpParams.groups.maxGroups; i++) { | ||
groupSizes[i] = Math.max( | ||
this._rankXpParams.groups.groupScaleFactors[i] * | ||
completions ** this._rankXpParams.groups.groupExponents[i], | ||
this._rankXpParams.groups.groupMinSizes[i] | ||
); | ||
} | ||
|
||
let rankOffset = 11; | ||
for (let i = 0; i < this._rankXpParams.groups.maxGroups; i++) { | ||
if (rank < rankOffset + groupSizes[i]) { | ||
const groupPoints = Math.ceil( | ||
this._rankXpParams.top10.WRPoints * | ||
this._rankXpParams.groups.groupPointPcts[i] | ||
); | ||
rankGain.group.groupNum = i + 1; | ||
rankGain.group.groupXP = groupPoints; | ||
rankGain.rankXP += groupPoints; | ||
break; | ||
} else { | ||
rankOffset += groupSizes[i]; | ||
} | ||
} | ||
} | ||
|
||
return rankGain; | ||
} | ||
|
||
public async get(): Promise<XpSystemsDto> { | ||
return DtoFactory(XpSystemsDto, { | ||
cosXP: this._cosXpParams, | ||
rankXP: this._rankXpParams | ||
}); | ||
} | ||
|
||
public async update(body: UpdateXpSystemsDto): Promise<void> { | ||
const cosXP = instanceToPlain(body.cosXP) as CosXpParams; | ||
const rankXP = instanceToPlain(body.rankXP) as RankXpParams; | ||
await this.db.xpSystems.update({ | ||
where: { id: 1 }, | ||
data: { cosXP, rankXP } | ||
}); | ||
|
||
this._cosXpParams = cosXP; | ||
this._rankXpParams = rankXP; | ||
} | ||
|
||
private generateLevelsArrays() { | ||
this.xpInLevels = [0]; | ||
this.xpForLevels = [0, 0]; | ||
|
||
for (let i = 0; i < this._cosXpParams.levels.maxLevels; i++) { | ||
this.xpInLevels[i] = this.getCosmeticXpInLevel(i); | ||
|
||
if (i > 0) | ||
this.xpForLevels[i] = this.xpForLevels[i - 1] + this.xpInLevels[i]; | ||
} | ||
} | ||
|
||
private static getInitialScale(tier: number, type = 0) { | ||
switch (type) { | ||
case 0: | ||
default: | ||
return tier ** 2 - tier + 10; | ||
case 1: | ||
return tier ** 2; | ||
case 2: | ||
return tier ** 2 + 5; | ||
} | ||
} | ||
} | ||
export class XpSystemsService extends XpSystems {} |
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
Oops, something went wrong.