Skip to content

Commit

Permalink
refactor(back,xpsys): move xp systems logic to xp-systems
Browse files Browse the repository at this point in the history
  • Loading branch information
tsa96 committed Jan 6, 2024
1 parent d06a60a commit 1e1c75b
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 409 deletions.
326 changes: 7 additions & 319 deletions apps/backend/src/app/modules/xp-systems/xp-systems.service.ts
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 {}
1 change: 0 additions & 1 deletion libs/constants/src/types/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,4 @@ export * from './socials/twitch-stream.model';
export * from './stats/base-stats.model';
export * from './stats/run-stats.model';
export * from './report/report.model';
export * from './xp-systems/xp-systems.model';
export * from './paged-response.model';
Loading

0 comments on commit 1e1c75b

Please sign in to comment.