mirror of
https://github.com/thepeacockproject/Peacock
synced 2024-11-22 22:12:45 +01:00
Add support for XP progression (#86)
* Fixed issue where restarting Peacock would require to first go offline again * Added proper player progression * Fixed most issues with the mission end screen * Added final tweaks to scoring * Update global challenges * Added near-complete support for Freelancer Added support for unlocking shortcuts Cleaned up a bunch of magic values in relation to XP and levels --------- Co-authored-by: moonysolari <changyiding@126.com>
This commit is contained in:
parent
c7d676d0af
commit
ba9b799abe
@ -32,10 +32,11 @@ import type {
|
|||||||
} from "../types/types"
|
} from "../types/types"
|
||||||
import { getUserData, writeUserData } from "../databaseHandler"
|
import { getUserData, writeUserData } from "../databaseHandler"
|
||||||
|
|
||||||
import { Controller } from "../controller"
|
import { controller, Controller } from "../controller"
|
||||||
import {
|
import {
|
||||||
generateCompletionData,
|
generateCompletionData,
|
||||||
generateUserCentric,
|
generateUserCentric,
|
||||||
|
getSubLocationByName,
|
||||||
getSubLocationFromContract,
|
getSubLocationFromContract,
|
||||||
} from "../contracts/dataGen"
|
} from "../contracts/dataGen"
|
||||||
import { log, LogLevel } from "../loggingInterop"
|
import { log, LogLevel } from "../loggingInterop"
|
||||||
@ -48,11 +49,21 @@ import {
|
|||||||
HandleEventOptions,
|
HandleEventOptions,
|
||||||
} from "@peacockproject/statemachine-parser"
|
} from "@peacockproject/statemachine-parser"
|
||||||
import { SavedChallengeGroup } from "../types/challenges"
|
import { SavedChallengeGroup } from "../types/challenges"
|
||||||
import { fastClone } from "../utils"
|
import {
|
||||||
|
clampValue,
|
||||||
|
DEFAULT_MASTERY_MAXLEVEL,
|
||||||
|
evergreenLevelForXp,
|
||||||
|
fastClone,
|
||||||
|
getMaxProfileLevel,
|
||||||
|
levelForXp,
|
||||||
|
xpRequiredForEvergreenLevel,
|
||||||
|
xpRequiredForLevel,
|
||||||
|
} from "../utils"
|
||||||
import {
|
import {
|
||||||
ChallengeFilterOptions,
|
ChallengeFilterOptions,
|
||||||
ChallengeFilterType,
|
ChallengeFilterType,
|
||||||
filterChallenge,
|
filterChallenge,
|
||||||
|
inclusionDataCheck,
|
||||||
mergeSavedChallengeGroups,
|
mergeSavedChallengeGroups,
|
||||||
} from "./challengeHelpers"
|
} from "./challengeHelpers"
|
||||||
import assert from "assert"
|
import assert from "assert"
|
||||||
@ -324,6 +335,11 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
let challenges: [string, RegistryChallenge[]][] = []
|
let challenges: [string, RegistryChallenge[]][] = []
|
||||||
|
|
||||||
for (const groupId of this.groups.get(location).keys()) {
|
for (const groupId of this.groups.get(location).keys()) {
|
||||||
|
// if this is the global group, skip it.
|
||||||
|
if (groupId === "global") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const groupContents = this.getGroupContentByIdLoc(groupId, location)
|
const groupContents = this.getGroupContentByIdLoc(groupId, location)
|
||||||
if (groupContents) {
|
if (groupContents) {
|
||||||
let groupChallenges: RegistryChallenge[] | string[] = [
|
let groupChallenges: RegistryChallenge[] | string[] = [
|
||||||
@ -429,12 +445,42 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const contractJson = this.controller.resolveContract(contractId)
|
||||||
|
|
||||||
|
if (contractJson.Metadata.Type === "evergreen") {
|
||||||
|
session.evergreen = {
|
||||||
|
payout: 0,
|
||||||
|
scoringScreenEndState: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Add this to getChallengesForContract without breaking the rest of Peacock?
|
||||||
|
challengeGroups["global"] = this.getGroupByIdLoc(
|
||||||
|
"global",
|
||||||
|
"GLOBAL",
|
||||||
|
).Challenges.filter((val) =>
|
||||||
|
inclusionDataCheck(val.InclusionData, contractJson),
|
||||||
|
)
|
||||||
|
|
||||||
const profile = getUserData(session.userId, session.gameVersion)
|
const profile = getUserData(session.userId, session.gameVersion)
|
||||||
|
|
||||||
for (const group of Object.keys(challengeGroups)) {
|
for (const group of Object.keys(challengeGroups)) {
|
||||||
for (const challenge of challengeGroups[group]) {
|
for (const challenge of challengeGroups[group]) {
|
||||||
const isDone = this.fastGetIsCompleted(profile, challenge.Id)
|
const isDone = this.fastGetIsCompleted(profile, challenge.Id)
|
||||||
|
|
||||||
|
if (
|
||||||
|
challenge.Definition.Scope === "profile" ||
|
||||||
|
challenge.Definition.Scope === "hit"
|
||||||
|
) {
|
||||||
|
profile.Extensions.ChallengeProgression[challenge.Id] ??= {
|
||||||
|
Ticked: false,
|
||||||
|
Completed: false,
|
||||||
|
State:
|
||||||
|
(<ChallengeDefinitionLike>challenge?.Definition)
|
||||||
|
?.Context || {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For challenges with scopes being "profile" or "hit",
|
// For challenges with scopes being "profile" or "hit",
|
||||||
// update challenge progression with the user's progression data
|
// update challenge progression with the user's progression data
|
||||||
const ctx =
|
const ctx =
|
||||||
@ -451,6 +497,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
context: ctx,
|
context: ctx,
|
||||||
state: isDone ? "Success" : "Start",
|
state: isDone ? "Success" : "Start",
|
||||||
timers: [],
|
timers: [],
|
||||||
|
timesCompleted: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -488,6 +535,8 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
currentState: data.state,
|
currentState: data.state,
|
||||||
timers: data.timers,
|
timers: data.timers,
|
||||||
timestamp: event.Timestamp,
|
timestamp: event.Timestamp,
|
||||||
|
//logger: (category, message) =>
|
||||||
|
// log(LogLevel.DEBUG, `[${category}] ${message}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousState = data.state
|
const previousState = data.state
|
||||||
@ -499,6 +548,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
event.Value,
|
event.Value,
|
||||||
options,
|
options,
|
||||||
)
|
)
|
||||||
|
|
||||||
// For challenges with scopes being "profile" or "hit",
|
// For challenges with scopes being "profile" or "hit",
|
||||||
// save challenge progression to the user's progression data
|
// save challenge progression to the user's progression data
|
||||||
if (
|
if (
|
||||||
@ -519,6 +569,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
|
|
||||||
if (previousState !== "Success" && result.state === "Success") {
|
if (previousState !== "Success" && result.state === "Success") {
|
||||||
this.onChallengeCompleted(
|
this.onChallengeCompleted(
|
||||||
|
session,
|
||||||
session.userId,
|
session.userId,
|
||||||
session.gameVersion,
|
session.gameVersion,
|
||||||
challenge,
|
challenge,
|
||||||
@ -991,6 +1042,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
* @param gameVersion The game version.
|
* @param gameVersion The game version.
|
||||||
*/
|
*/
|
||||||
public tryToCompleteChallenge(
|
public tryToCompleteChallenge(
|
||||||
|
session: ContractSession,
|
||||||
challengeId: string,
|
challengeId: string,
|
||||||
userData: UserProfile,
|
userData: UserProfile,
|
||||||
parentId: string,
|
parentId: string,
|
||||||
@ -1037,6 +1089,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.onChallengeCompleted(
|
this.onChallengeCompleted(
|
||||||
|
session,
|
||||||
userData.Id,
|
userData.Id,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
this.getChallengeById(challengeId),
|
this.getChallengeById(challengeId),
|
||||||
@ -1045,6 +1098,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onChallengeCompleted(
|
private onChallengeCompleted(
|
||||||
|
session: ContractSession,
|
||||||
userId: string,
|
userId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
challenge: RegistryChallenge,
|
challenge: RegistryChallenge,
|
||||||
@ -1061,15 +1115,35 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
|
|
||||||
const userData = getUserData(userId, gameVersion)
|
const userData = getUserData(userId, gameVersion)
|
||||||
|
|
||||||
userData.Extensions.ChallengeProgression ??= {}
|
//ASSUMED: Challenges that are not global should always be completed
|
||||||
|
if (!challenge.Tags.includes("global")) {
|
||||||
|
userData.Extensions.ChallengeProgression ??= {}
|
||||||
|
|
||||||
userData.Extensions.ChallengeProgression[challenge.Id] ??= {
|
userData.Extensions.ChallengeProgression[challenge.Id] ??= {
|
||||||
State: {},
|
State: {},
|
||||||
Completed: false,
|
Completed: false,
|
||||||
Ticked: false,
|
Ticked: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
userData.Extensions.ChallengeProgression[challenge.Id].Completed =
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
userData.Extensions.ChallengeProgression[challenge.Id].Completed = true
|
//Always count the number of completions
|
||||||
|
session.challengeContexts[challenge.Id].timesCompleted++
|
||||||
|
|
||||||
|
//If we have a Definition-scope with a Repeatable, we may want to restart it.
|
||||||
|
//TODO: Figure out what Base/Delta means. For now if Repeatable is set, we restart the challenge.
|
||||||
|
if (challenge.Definition.Repeatable) {
|
||||||
|
session.challengeContexts[challenge.Id].state = "Start"
|
||||||
|
}
|
||||||
|
|
||||||
|
//NOTE: Official will always grant XP to both Location Mastery and the Player Profile
|
||||||
|
const totalXp =
|
||||||
|
(challenge.Xp || 0) + (challenge.Rewards?.MasteryXP || 0)
|
||||||
|
|
||||||
|
this.grantLocationMasteryXp(totalXp, session, userData)
|
||||||
|
this.grantUserXp(totalXp, session, userData)
|
||||||
|
|
||||||
writeUserData(userId, gameVersion)
|
writeUserData(userId, gameVersion)
|
||||||
|
|
||||||
@ -1078,6 +1152,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
// Check if completing this challenge also completes any dependency trees depending on it
|
// Check if completing this challenge also completes any dependency trees depending on it
|
||||||
for (const depTreeId of this._dependencyTree.keys()) {
|
for (const depTreeId of this._dependencyTree.keys()) {
|
||||||
this.tryToCompleteChallenge(
|
this.tryToCompleteChallenge(
|
||||||
|
session,
|
||||||
depTreeId,
|
depTreeId,
|
||||||
userData,
|
userData,
|
||||||
challenge.Id,
|
challenge.Id,
|
||||||
@ -1085,4 +1160,87 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grantLocationMasteryXp(
|
||||||
|
masteryXp: number,
|
||||||
|
contractSession: ContractSession,
|
||||||
|
userProfile: UserProfile,
|
||||||
|
): boolean {
|
||||||
|
const contract = controller.resolveContract(contractSession.contractId)
|
||||||
|
|
||||||
|
if (!contract) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const subLocation = getSubLocationByName(
|
||||||
|
contract.Metadata.Location,
|
||||||
|
contractSession.gameVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
const parentLocationId = subLocation
|
||||||
|
? subLocation.Properties?.ParentLocation
|
||||||
|
: contract.Metadata.Location
|
||||||
|
|
||||||
|
if (!parentLocationId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const masteryData =
|
||||||
|
this.controller.masteryService.getMasteryPackage(parentLocationId)
|
||||||
|
|
||||||
|
if (!masteryData) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentLocationIdLowerCase = parentLocationId.toLocaleLowerCase()
|
||||||
|
|
||||||
|
userProfile.Extensions.progression.Locations[
|
||||||
|
parentLocationIdLowerCase
|
||||||
|
] ??= {
|
||||||
|
Xp: 0,
|
||||||
|
Level: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationData =
|
||||||
|
userProfile.Extensions.progression.Locations[
|
||||||
|
parentLocationIdLowerCase
|
||||||
|
]
|
||||||
|
|
||||||
|
const maxLevel = masteryData.MaxLevel || DEFAULT_MASTERY_MAXLEVEL
|
||||||
|
|
||||||
|
locationData.Xp = clampValue(
|
||||||
|
locationData.Xp + masteryXp,
|
||||||
|
0,
|
||||||
|
contract.Metadata.Type !== "evergreen"
|
||||||
|
? xpRequiredForLevel(maxLevel)
|
||||||
|
: xpRequiredForEvergreenLevel(maxLevel),
|
||||||
|
)
|
||||||
|
|
||||||
|
locationData.Level = clampValue(
|
||||||
|
contract.Metadata.Type !== "evergreen"
|
||||||
|
? levelForXp(locationData.Xp)
|
||||||
|
: evergreenLevelForXp(locationData.Xp),
|
||||||
|
1,
|
||||||
|
maxLevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
grantUserXp(
|
||||||
|
xp: number,
|
||||||
|
contractSession: ContractSession,
|
||||||
|
userProfile: UserProfile,
|
||||||
|
): boolean {
|
||||||
|
const profileData = userProfile.Extensions.progression.PlayerProfileXP
|
||||||
|
|
||||||
|
profileData.Total += xp
|
||||||
|
profileData.ProfileLevel = clampValue(
|
||||||
|
levelForXp(profileData.Total),
|
||||||
|
1,
|
||||||
|
getMaxProfileLevel(contractSession.gameVersion),
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,12 @@ import {
|
|||||||
MasteryPackage,
|
MasteryPackage,
|
||||||
} from "../types/mastery"
|
} from "../types/mastery"
|
||||||
import { CompletionData, GameVersion, Unlockable } from "../types/types"
|
import { CompletionData, GameVersion, Unlockable } from "../types/types"
|
||||||
import { xpRequiredForLevel } from "../utils"
|
import {
|
||||||
|
clampValue,
|
||||||
|
DEFAULT_MASTERY_MAXLEVEL,
|
||||||
|
xpRequiredForLevel,
|
||||||
|
XP_PER_LEVEL,
|
||||||
|
} from "../utils"
|
||||||
|
|
||||||
export class MasteryService {
|
export class MasteryService {
|
||||||
private masteryData: Map<string, MasteryPackage> = new Map()
|
private masteryData: Map<string, MasteryPackage> = new Map()
|
||||||
@ -81,13 +86,13 @@ export class MasteryService {
|
|||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
userId: string,
|
userId: string,
|
||||||
): CompletionData {
|
): CompletionData {
|
||||||
if (!this.masteryData.has(locationParentId)) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
//Get the mastery data
|
//Get the mastery data
|
||||||
const masteryData: MasteryPackage =
|
const masteryData: MasteryPackage =
|
||||||
this.masteryData.get(locationParentId)
|
this.getMasteryPackage(locationParentId)
|
||||||
|
|
||||||
|
if (!masteryData) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
//Get the user profile
|
//Get the user profile
|
||||||
const userProfile = getUserData(userId, gameVersion)
|
const userProfile = getUserData(userId, gameVersion)
|
||||||
@ -107,11 +112,12 @@ export class MasteryService {
|
|||||||
lowerCaseLocationParentId
|
lowerCaseLocationParentId
|
||||||
]
|
]
|
||||||
|
|
||||||
const maxLevel = masteryData.MaxLevel || 20
|
const maxLevel = masteryData.MaxLevel || DEFAULT_MASTERY_MAXLEVEL
|
||||||
|
|
||||||
const nextLevel: number = Math.max(
|
const nextLevel: number = clampValue(
|
||||||
0,
|
locationData.Level + 1,
|
||||||
Math.min(locationData.Level + 1, maxLevel),
|
1,
|
||||||
|
maxLevel,
|
||||||
)
|
)
|
||||||
const nextLevelXp: number = xpRequiredForLevel(nextLevel)
|
const nextLevelXp: number = xpRequiredForLevel(nextLevel)
|
||||||
|
|
||||||
@ -119,7 +125,8 @@ export class MasteryService {
|
|||||||
Level: locationData.Level,
|
Level: locationData.Level,
|
||||||
MaxLevel: maxLevel,
|
MaxLevel: maxLevel,
|
||||||
XP: locationData.Xp,
|
XP: locationData.Xp,
|
||||||
Completion: locationData.Xp / nextLevelXp,
|
Completion:
|
||||||
|
(XP_PER_LEVEL - (nextLevelXp - locationData.Xp)) / XP_PER_LEVEL,
|
||||||
XpLeft: nextLevelXp - locationData.Xp,
|
XpLeft: nextLevelXp - locationData.Xp,
|
||||||
Id: masteryData.Id,
|
Id: masteryData.Id,
|
||||||
SubLocationId: subLocationId,
|
SubLocationId: subLocationId,
|
||||||
@ -129,20 +136,24 @@ export class MasteryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMasteryPackage(locationParentId: string): MasteryPackage {
|
||||||
|
if (!this.masteryData.has(locationParentId)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.masteryData.get(locationParentId)
|
||||||
|
}
|
||||||
|
|
||||||
private getMasteryData(
|
private getMasteryData(
|
||||||
locationParentId: string,
|
locationParentId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
userId: string,
|
userId: string,
|
||||||
): MasteryData[] {
|
): MasteryData[] {
|
||||||
if (!this.masteryData.has(locationParentId)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
//Get the mastery data
|
//Get the mastery data
|
||||||
const masteryData: MasteryPackage =
|
const masteryData: MasteryPackage =
|
||||||
this.masteryData.get(locationParentId)
|
this.getMasteryPackage(locationParentId)
|
||||||
|
|
||||||
if (masteryData.Drops.length === 0) {
|
if (!masteryData || masteryData.Drops.length === 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,11 @@ import { controller, preserveContracts } from "../controller"
|
|||||||
import { createLocationsData } from "../menus/destinations"
|
import { createLocationsData } from "../menus/destinations"
|
||||||
import { userAuths } from "../officialServerAuth"
|
import { userAuths } from "../officialServerAuth"
|
||||||
import { log, LogLevel } from "../loggingInterop"
|
import { log, LogLevel } from "../loggingInterop"
|
||||||
import { getRemoteService, contractCreationTutorialId } from "../utils"
|
import {
|
||||||
|
getRemoteService,
|
||||||
|
contractCreationTutorialId,
|
||||||
|
getMaxProfileLevel,
|
||||||
|
} from "../utils"
|
||||||
|
|
||||||
export function contractsModeHome(req: RequestWithJwt, res: Response): void {
|
export function contractsModeHome(req: RequestWithJwt, res: Response): void {
|
||||||
const contractsHomeTemplate = getConfig("ContractsTemplate", false)
|
const contractsHomeTemplate = getConfig("ContractsTemplate", false)
|
||||||
@ -58,7 +62,7 @@ export function contractsModeHome(req: RequestWithJwt, res: Response): void {
|
|||||||
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
||||||
Level: userData.Extensions.progression.PlayerProfileXP
|
Level: userData.Extensions.progression.PlayerProfileXP
|
||||||
.ProfileLevel,
|
.ProfileLevel,
|
||||||
MaxLevel: 7500,
|
MaxLevel: getMaxProfileLevel(req.gameVersion),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
Campaign,
|
Campaign,
|
||||||
ClientToServerEvent,
|
ClientToServerEvent,
|
||||||
|
CompiledChallengeRuntimeData,
|
||||||
ContractSession,
|
ContractSession,
|
||||||
GameVersion,
|
GameVersion,
|
||||||
GenSingleMissionFunc,
|
GenSingleMissionFunc,
|
||||||
@ -36,6 +37,7 @@ import type {
|
|||||||
MissionManifest,
|
MissionManifest,
|
||||||
PeacockLocationsData,
|
PeacockLocationsData,
|
||||||
PlayNextGetCampaignsHookReturn,
|
PlayNextGetCampaignsHookReturn,
|
||||||
|
RegistryChallenge,
|
||||||
RequestWithJwt,
|
RequestWithJwt,
|
||||||
S2CEventWithTimestamp,
|
S2CEventWithTimestamp,
|
||||||
SMFLastDeploy,
|
SMFLastDeploy,
|
||||||
@ -81,7 +83,7 @@ import { createContext, Script } from "vm"
|
|||||||
import { ChallengeService } from "./candle/challengeService"
|
import { ChallengeService } from "./candle/challengeService"
|
||||||
import { getFlag } from "./flags"
|
import { getFlag } from "./flags"
|
||||||
import { unpack } from "msgpackr"
|
import { unpack } from "msgpackr"
|
||||||
import { ChallengePackage } from "./types/challenges"
|
import { ChallengePackage, SavedChallengeGroup } from "./types/challenges"
|
||||||
import { promisify } from "util"
|
import { promisify } from "util"
|
||||||
import { brotliDecompress } from "zlib"
|
import { brotliDecompress } from "zlib"
|
||||||
import assert from "assert"
|
import assert from "assert"
|
||||||
@ -920,6 +922,42 @@ export class Controller {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//Get all global challenges and register a simplified version of them
|
||||||
|
{
|
||||||
|
const globalChallenges: RegistryChallenge[] = (
|
||||||
|
getConfig(
|
||||||
|
"GlobalChallenges",
|
||||||
|
true,
|
||||||
|
) as CompiledChallengeRuntimeData[]
|
||||||
|
).map((e) => {
|
||||||
|
const tags = e.Challenge.Tags || []
|
||||||
|
tags.push("global")
|
||||||
|
|
||||||
|
//NOTE: Treat all other fields as undefined
|
||||||
|
return <RegistryChallenge>{
|
||||||
|
Id: e.Challenge.Id,
|
||||||
|
Tags: tags,
|
||||||
|
Name: e.Challenge.Name,
|
||||||
|
ImageName: e.Challenge.ImageName,
|
||||||
|
Description: e.Challenge.Description,
|
||||||
|
Definition: e.Challenge.Definition,
|
||||||
|
Xp: e.Challenge.Xp,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this._handleChallengeResources({
|
||||||
|
groups: [
|
||||||
|
<SavedChallengeGroup>{
|
||||||
|
CategoryId: "global",
|
||||||
|
Challenges: globalChallenges,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
Location: "GLOBAL",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Load mastery resources
|
// Load mastery resources
|
||||||
const masteryDirectory = join(
|
const masteryDirectory = join(
|
||||||
PEACOCK_DEV ? process.cwd() : __dirname,
|
PEACOCK_DEV ? process.cwd() : __dirname,
|
||||||
|
@ -104,9 +104,41 @@ export function getUserData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//NOTE: ProfileLevel always starts at 1
|
||||||
|
if (data?.Extensions?.progression?.PlayerProfileXP?.ProfileLevel === 0) {
|
||||||
|
data.Extensions.progression.PlayerProfileXP.ProfileLevel = 1
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only attempt to load a user's profile if it hasn't been loaded yet
|
||||||
|
*
|
||||||
|
* @param userId The user's ID.
|
||||||
|
* @param gameVersion The game's version.
|
||||||
|
*/
|
||||||
|
export async function cheapLoadUserData(
|
||||||
|
userId: string,
|
||||||
|
gameVersion: GameVersion,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!userId || !gameVersion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userProfile = asyncGuard.getProfile(`${userId}.${gameVersion}`)
|
||||||
|
|
||||||
|
if (userProfile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadUserData(userId, gameVersion)
|
||||||
|
} catch (e) {
|
||||||
|
log(LogLevel.DEBUG, "Unable to load profile information.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a user's profile data.
|
* Loads a user's profile data.
|
||||||
*
|
*
|
||||||
|
@ -31,7 +31,7 @@ import {
|
|||||||
Seconds,
|
Seconds,
|
||||||
ServerToClientEvent,
|
ServerToClientEvent,
|
||||||
} from "./types/types"
|
} from "./types/types"
|
||||||
import { contractTypes, extractToken, ServerVer } from "./utils"
|
import { contractTypes, extractToken, gameDifficulty, ServerVer } from "./utils"
|
||||||
import { json as jsonMiddleware } from "body-parser"
|
import { json as jsonMiddleware } from "body-parser"
|
||||||
import { log, LogLevel } from "./loggingInterop"
|
import { log, LogLevel } from "./loggingInterop"
|
||||||
import { getUserData, writeUserData } from "./databaseHandler"
|
import { getUserData, writeUserData } from "./databaseHandler"
|
||||||
@ -47,6 +47,7 @@ import {
|
|||||||
AmbientChangedC2SEvent,
|
AmbientChangedC2SEvent,
|
||||||
BodyHiddenC2SEvent,
|
BodyHiddenC2SEvent,
|
||||||
ContractStartC2SEvent,
|
ContractStartC2SEvent,
|
||||||
|
Evergreen_Payout_DataC2SEvent,
|
||||||
HeroSpawn_LocationC2SEvent,
|
HeroSpawn_LocationC2SEvent,
|
||||||
ItemDroppedC2SEvent,
|
ItemDroppedC2SEvent,
|
||||||
ItemPickedUpC2SEvent,
|
ItemPickedUpC2SEvent,
|
||||||
@ -471,7 +472,25 @@ function saveEvents(
|
|||||||
const processed: string[] = []
|
const processed: string[] = []
|
||||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
const session = contractSessions.get(event.ContractSessionId)
|
let session = contractSessions.get(event.ContractSessionId)
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
log(
|
||||||
|
LogLevel.WARN,
|
||||||
|
"Creating a fake session to avoid problems... scoring will not work!",
|
||||||
|
)
|
||||||
|
|
||||||
|
newSession(
|
||||||
|
event.ContractSessionId,
|
||||||
|
event.ContractId,
|
||||||
|
req.jwt.unique_name,
|
||||||
|
gameDifficulty.normal,
|
||||||
|
req.gameVersion,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
session = contractSessions.get(event.ContractSessionId)
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!session ||
|
!session ||
|
||||||
@ -562,6 +581,18 @@ function saveEvents(
|
|||||||
|
|
||||||
if (handleMultiplayerEvent(event, session)) {
|
if (handleMultiplayerEvent(event, session)) {
|
||||||
processed.push(event.Name)
|
processed.push(event.Name)
|
||||||
|
response.push(process.hrtime.bigint().toString())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.Name.startsWith("ScoringScreenEndState_")) {
|
||||||
|
session.evergreen.scoringScreenEndState = event.Name
|
||||||
|
|
||||||
|
processed.push(event.Name)
|
||||||
|
response.push(process.hrtime.bigint().toString())
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.Name) {
|
switch (event.Name) {
|
||||||
@ -760,6 +791,11 @@ function saveEvents(
|
|||||||
contract.Metadata.CpdId,
|
contract.Metadata.CpdId,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
case "Evergreen_Payout_Data":
|
||||||
|
session.evergreen.payout = (<Evergreen_Payout_DataC2SEvent>(
|
||||||
|
event
|
||||||
|
)).Value.Total_Payout
|
||||||
|
break
|
||||||
// Sinkhole events we don't care about
|
// Sinkhole events we don't care about
|
||||||
case "ItemPickedUp":
|
case "ItemPickedUp":
|
||||||
log(
|
log(
|
||||||
|
@ -51,5 +51,11 @@ export async function getCpd(
|
|||||||
return defaultCPD
|
return defaultCPD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//NOTE: Update the EvergreenLevel with the latest Mastery Level
|
||||||
|
//TODO: Get rid of hard-coded values
|
||||||
|
userData.Extensions.CPD[cpdID]["EvergreenLevel"] =
|
||||||
|
userData.Extensions.progression.Locations["location_parent_snug"]
|
||||||
|
?.Level || 1
|
||||||
|
|
||||||
return userData.Extensions.CPD[cpdID]
|
return userData.Extensions.CPD[cpdID]
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,7 @@ import { multiplayerRouter } from "./multiplayer/multiplayerService"
|
|||||||
import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData"
|
import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData"
|
||||||
import { pack, unpack } from "msgpackr"
|
import { pack, unpack } from "msgpackr"
|
||||||
import { liveSplitManager } from "./livesplit/liveSplitManager"
|
import { liveSplitManager } from "./livesplit/liveSplitManager"
|
||||||
|
import { cheapLoadUserData } from "./databaseHandler"
|
||||||
|
|
||||||
// welcome to the bleeding edge
|
// welcome to the bleeding edge
|
||||||
setFlagsFromString("--harmony")
|
setFlagsFromString("--harmony")
|
||||||
@ -356,7 +357,18 @@ app.use(
|
|||||||
|
|
||||||
next()
|
next()
|
||||||
}),
|
}),
|
||||||
)
|
).use(async (req: RequestWithJwt, _res, next): Promise<void> => {
|
||||||
|
if (!req.jwt) {
|
||||||
|
next()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the userdata is always loaded if a proper jwt token is available
|
||||||
|
await cheapLoadUserData(req.jwt.unique_name, req.gameVersion)
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
function generateBlobConfig(req: RequestWithJwt) {
|
function generateBlobConfig(req: RequestWithJwt) {
|
||||||
return {
|
return {
|
||||||
|
@ -20,6 +20,7 @@ import { Response, Router } from "express"
|
|||||||
import {
|
import {
|
||||||
contractCreationTutorialId,
|
contractCreationTutorialId,
|
||||||
gameDifficulty,
|
gameDifficulty,
|
||||||
|
getMaxProfileLevel,
|
||||||
PEACOCKVERSTRING,
|
PEACOCKVERSTRING,
|
||||||
unlockOrderComparer,
|
unlockOrderComparer,
|
||||||
uuidRegex,
|
uuidRegex,
|
||||||
@ -322,7 +323,7 @@ menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
|
|||||||
XP: userdata.Extensions.progression.PlayerProfileXP.Total,
|
XP: userdata.Extensions.progression.PlayerProfileXP.Total,
|
||||||
Level: userdata.Extensions.progression.PlayerProfileXP
|
Level: userdata.Extensions.progression.PlayerProfileXP
|
||||||
.ProfileLevel,
|
.ProfileLevel,
|
||||||
MaxLevel: 7500,
|
MaxLevel: getMaxProfileLevel(req.gameVersion),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -1837,7 +1838,7 @@ menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
|
|||||||
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
||||||
Level: userData.Extensions.progression.PlayerProfileXP
|
Level: userData.Extensions.progression.PlayerProfileXP
|
||||||
.ProfileLevel,
|
.ProfileLevel,
|
||||||
MaxLevel: 7500,
|
MaxLevel: getMaxProfileLevel(req.gameVersion),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -35,6 +35,7 @@ import { getUserData, writeUserData } from "../databaseHandler"
|
|||||||
import {
|
import {
|
||||||
fastClone,
|
fastClone,
|
||||||
getDefaultSuitFor,
|
getDefaultSuitFor,
|
||||||
|
getMaxProfileLevel,
|
||||||
nilUuid,
|
nilUuid,
|
||||||
unlockOrderComparer,
|
unlockOrderComparer,
|
||||||
} from "../utils"
|
} from "../utils"
|
||||||
@ -471,7 +472,7 @@ export async function planningView(
|
|||||||
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
||||||
Level: userData.Extensions.progression.PlayerProfileXP
|
Level: userData.Extensions.progression.PlayerProfileXP
|
||||||
.ProfileLevel,
|
.ProfileLevel,
|
||||||
MaxLevel: 7500,
|
MaxLevel: getMaxProfileLevel(req.gameVersion),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -18,7 +18,13 @@
|
|||||||
|
|
||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { castUserProfile, nilUuid, uuidRegex } from "./utils"
|
import {
|
||||||
|
castUserProfile,
|
||||||
|
getMaxProfileLevel,
|
||||||
|
nilUuid,
|
||||||
|
uuidRegex,
|
||||||
|
XP_PER_LEVEL,
|
||||||
|
} from "./utils"
|
||||||
import { json as jsonMiddleware } from "body-parser"
|
import { json as jsonMiddleware } from "body-parser"
|
||||||
import { getPlatformEntitlements } from "./platformEntitlements"
|
import { getPlatformEntitlements } from "./platformEntitlements"
|
||||||
import { contractSessions, newSession } from "./eventHandler"
|
import { contractSessions, newSession } from "./eventHandler"
|
||||||
@ -542,36 +548,20 @@ profileRouter.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const challenge of challenges) {
|
for (const challenge of challenges) {
|
||||||
// TODO: Add actual support for shortcut challenges
|
challenge.Progression = Object.assign(
|
||||||
if (challenge.Challenge.Tags?.includes("shortcut")) {
|
{
|
||||||
challenge.Progression = {
|
|
||||||
ChallengeId: challenge.Challenge.Id,
|
ChallengeId: challenge.Challenge.Id,
|
||||||
ProfileId: req.jwt.unique_name,
|
ProfileId: req.jwt.unique_name,
|
||||||
Completed: true,
|
Completed: false,
|
||||||
Ticked: true,
|
State: {},
|
||||||
State: {
|
ETag: `W/"datetime'${encodeURIComponent(
|
||||||
CurrentState: "Success",
|
new Date().toISOString(),
|
||||||
},
|
)}'"`,
|
||||||
// @ts-expect-error typescript hates dates
|
CompletedAt: null,
|
||||||
CompletedAt: new Date(new Date() - 10).toISOString(),
|
|
||||||
MustBeSaved: false,
|
MustBeSaved: false,
|
||||||
}
|
},
|
||||||
} else {
|
challenge.Progression,
|
||||||
challenge.Progression = Object.assign(
|
)
|
||||||
{
|
|
||||||
ChallengeId: challenge.Challenge.Id,
|
|
||||||
ProfileId: req.jwt.unique_name,
|
|
||||||
Completed: false,
|
|
||||||
State: {},
|
|
||||||
ETag: `W/"datetime'${encodeURIComponent(
|
|
||||||
new Date().toISOString(),
|
|
||||||
)}'"`,
|
|
||||||
CompletedAt: null,
|
|
||||||
MustBeSaved: false,
|
|
||||||
},
|
|
||||||
challenge.Progression,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(challenges)
|
res.json(challenges)
|
||||||
@ -595,8 +585,8 @@ profileRouter.post(
|
|||||||
Location: [0],
|
Location: [0],
|
||||||
PlayerProfile: {
|
PlayerProfile: {
|
||||||
Version: 1,
|
Version: 1,
|
||||||
XpPerLevel: 6000,
|
XpPerLevel: XP_PER_LEVEL,
|
||||||
MaxLevel: 7500,
|
MaxLevel: getMaxProfileLevel(req.gameVersion),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -817,6 +807,13 @@ async function loadSession(
|
|||||||
}
|
}
|
||||||
// Update challenge progression with the user's latest progression data
|
// Update challenge progression with the user's latest progression data
|
||||||
for (const cid in sessionData.challengeContexts) {
|
for (const cid in sessionData.challengeContexts) {
|
||||||
|
// Make sure the ChallengeProgression is available, otherwise loading might fail!
|
||||||
|
userData.Extensions.ChallengeProgression[cid] ??= {
|
||||||
|
State: {},
|
||||||
|
Completed: false,
|
||||||
|
Ticked: false,
|
||||||
|
}
|
||||||
|
|
||||||
const scope =
|
const scope =
|
||||||
controller.challengeService.getChallengeById(cid).Definition.Scope
|
controller.challengeService.getChallengeById(cid).Definition.Scope
|
||||||
if (
|
if (
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -40,10 +40,15 @@ export interface SavedChallenge {
|
|||||||
ParentLocationId: string
|
ParentLocationId: string
|
||||||
Type: "Hit" | string
|
Type: "Hit" | string
|
||||||
RuntimeType: "contract" | string
|
RuntimeType: "contract" | string
|
||||||
|
Xp: number
|
||||||
XpModifier?: unknown
|
XpModifier?: unknown
|
||||||
DifficultyLevels: string[]
|
DifficultyLevels: string[]
|
||||||
Definition: MissionManifestObjective["Definition"] & {
|
Definition: MissionManifestObjective["Definition"] & {
|
||||||
Scope: ContextScopedStorageLocation
|
Scope: ContextScopedStorageLocation
|
||||||
|
Repeatable?: {
|
||||||
|
Base: number
|
||||||
|
Delta: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Tags: string[]
|
Tags: string[]
|
||||||
InclusionData?: InclusionData
|
InclusionData?: InclusionData
|
||||||
|
@ -253,3 +253,7 @@ export type Dart_HitC2SEvent = ClientToServerEvent<{
|
|||||||
ActorType: number
|
ActorType: number
|
||||||
Sedative: "" | string
|
Sedative: "" | string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
export type Evergreen_Payout_DataC2SEvent = ClientToServerEvent<{
|
||||||
|
Total_Payout: number
|
||||||
|
}>
|
||||||
|
127
components/types/score.ts
Normal file
127
components/types/score.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { Playstyle, ScoringBonus, ScoringHeadline } from "./scoring"
|
||||||
|
import { CompletionData, Seconds, Unlockable } from "./types"
|
||||||
|
|
||||||
|
export interface CalculateXpResult {
|
||||||
|
completedChallenges: MissionEndChallenge[]
|
||||||
|
xp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculateScoreResult {
|
||||||
|
stars: number
|
||||||
|
scoringHeadlines: ScoringHeadline[]
|
||||||
|
awardedBonuses: ScoringBonus[]
|
||||||
|
failedBonuses: ScoringBonus[]
|
||||||
|
achievedMasteries: MissionEndAchievedMastery[]
|
||||||
|
silentAssassin: boolean
|
||||||
|
score: number
|
||||||
|
scoreWithBonus: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionEndChallenge {
|
||||||
|
ChallengeId: string
|
||||||
|
ChallengeTags: string[]
|
||||||
|
ChallengeName: string
|
||||||
|
ChallengeImageUrl: string
|
||||||
|
ChallengeDescription: string
|
||||||
|
XPGain: number
|
||||||
|
IsGlobal: boolean
|
||||||
|
IsActionReward: boolean
|
||||||
|
Drops: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionEndDrop {
|
||||||
|
Unlockable: Unlockable
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionEndAchievedMastery {
|
||||||
|
score: number
|
||||||
|
RatioParts: number
|
||||||
|
RatioTotal: number
|
||||||
|
Id: string
|
||||||
|
BaseScore: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionEndEvergreen {
|
||||||
|
Payout: number
|
||||||
|
EndStateEventName?: string
|
||||||
|
PayoutsCompleted: MissionEndEvergreenPayout[]
|
||||||
|
PayoutsFailed: MissionEndEvergreenPayout[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionEndEvergreenPayout {
|
||||||
|
Name: string
|
||||||
|
Payout: number
|
||||||
|
IsPrestige: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionEndResponse {
|
||||||
|
MissionReward: {
|
||||||
|
LocationProgression: {
|
||||||
|
LevelInfo: number[]
|
||||||
|
XP: number
|
||||||
|
Level: number
|
||||||
|
Completion: number
|
||||||
|
XPGain: number
|
||||||
|
HideProgression: boolean
|
||||||
|
}
|
||||||
|
ProfileProgression: {
|
||||||
|
LevelInfo: number[]
|
||||||
|
LevelInfoOffset: number
|
||||||
|
XP: number
|
||||||
|
Level: number
|
||||||
|
XPGain: number
|
||||||
|
}
|
||||||
|
Challenges: MissionEndChallenge[]
|
||||||
|
Drops: MissionEndDrop[]
|
||||||
|
OpportunityRewards: unknown[] //?
|
||||||
|
CompletionData: CompletionData
|
||||||
|
ChallengeCompletion: {
|
||||||
|
ChallengesCount: number
|
||||||
|
CompletedChallengesCount: number
|
||||||
|
}
|
||||||
|
ContractChallengeCompletion: {
|
||||||
|
ChallengesCount: number
|
||||||
|
CompletedChallengesCount: number
|
||||||
|
}
|
||||||
|
OpportunityStatistics: {
|
||||||
|
Count: number
|
||||||
|
Completed: number
|
||||||
|
}
|
||||||
|
LocationCompletionPercent: number
|
||||||
|
}
|
||||||
|
ScoreOverview: {
|
||||||
|
XP: number
|
||||||
|
Level: number
|
||||||
|
Completion: number
|
||||||
|
XPGain: number
|
||||||
|
ChallengesCompleted: number
|
||||||
|
LocationHideProgression: boolean
|
||||||
|
ProdileId1?: string
|
||||||
|
stars: number
|
||||||
|
ScoreDetails: {
|
||||||
|
Headlines: ScoringHeadline[]
|
||||||
|
}
|
||||||
|
ContractScore: {
|
||||||
|
Total: number
|
||||||
|
AchievedMasteries: MissionEndAchievedMastery[]
|
||||||
|
AwardedBonuses: ScoringBonus[]
|
||||||
|
TotalNoMultipliers: number
|
||||||
|
TimeUsedSecs: Seconds
|
||||||
|
StarCount: number
|
||||||
|
FailedBonuses: ScoringBonus[]
|
||||||
|
SilentAssassin: boolean
|
||||||
|
}
|
||||||
|
SilentAssassin: boolean
|
||||||
|
NewRank: number
|
||||||
|
RankCount: number
|
||||||
|
Rank: number
|
||||||
|
FriendsRankCount: number
|
||||||
|
FriendsRank: number
|
||||||
|
IsPartOfTopScores: boolean
|
||||||
|
PlayStyle?: Playstyle
|
||||||
|
IsNewBestScore: boolean
|
||||||
|
IsNewBestTime: boolean
|
||||||
|
IsNewBestStars: boolean
|
||||||
|
Evergreen?: MissionEndEvergreen
|
||||||
|
}
|
||||||
|
}
|
@ -262,8 +262,18 @@ export interface ContractSession {
|
|||||||
context: unknown
|
context: unknown
|
||||||
state: string
|
state: string
|
||||||
timers: Timer[]
|
timers: Timer[]
|
||||||
|
timesCompleted: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Session Evergreen details.
|
||||||
|
*
|
||||||
|
* @since v6.0.0
|
||||||
|
*/
|
||||||
|
evergreen?: {
|
||||||
|
payout: number
|
||||||
|
scoringScreenEndState: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,8 +123,69 @@ export function extractToken(
|
|||||||
next?.("router")
|
next?.("router")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MASTERY_MAXLEVEL = 20
|
||||||
|
export const XP_PER_LEVEL = 6000
|
||||||
|
|
||||||
|
export function getMaxProfileLevel(gameVersion: GameVersion): number {
|
||||||
|
if (gameVersion === "h3") {
|
||||||
|
return 7500
|
||||||
|
}
|
||||||
|
|
||||||
|
return 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the level for the given XP based on XP_PER_LEVEL.
|
||||||
|
* Minimum level returned is 1.
|
||||||
|
*/
|
||||||
|
export function levelForXp(xp: number): number {
|
||||||
|
return Math.floor(xp / XP_PER_LEVEL) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the required XP for the given level based on XP_PER_LEVEL.
|
||||||
|
* Minimum XP returned is 0.
|
||||||
|
*/
|
||||||
export function xpRequiredForLevel(level: number): number {
|
export function xpRequiredForLevel(level: number): number {
|
||||||
return level * 6000 - 6000
|
return Math.max(0, (level - 1) * XP_PER_LEVEL)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EVERGREEN_LEVEL_INFO: number[] = [
|
||||||
|
0, 5000, 10000, 17000, 24000, 31000, 38000, 45000, 52000, 61000, 70000,
|
||||||
|
79000, 88000, 97000, 106000, 115000, 124000, 133000, 142000, 154000, 166000,
|
||||||
|
178000, 190000, 202000, 214000, 226000, 238000, 250000, 262000, 280000,
|
||||||
|
298000, 316000, 334000, 352000, 370000, 388000, 406000, 424000, 442000,
|
||||||
|
468000, 494000, 520000, 546000, 572000, 598000, 624000, 650000, 676000,
|
||||||
|
702000, 736000, 770000, 804000, 838000, 872000, 906000, 940000, 974000,
|
||||||
|
1008000, 1042000, 1082000, 1122000, 1162000, 1202000, 1242000, 1282000,
|
||||||
|
1322000, 1362000, 1402000, 1442000, 1492000, 1542000, 1592000, 1642000,
|
||||||
|
1692000, 1742000, 1792000, 1842000, 1892000, 1942000, 2002000, 2062000,
|
||||||
|
2122000, 2182000, 2242000, 2302000, 2362000, 2422000, 2482000, 2542000,
|
||||||
|
2692000, 2842000, 2992000, 3142000, 3292000, 3442000, 3592000, 3742000,
|
||||||
|
3892000, 4042000, 4192000,
|
||||||
|
]
|
||||||
|
|
||||||
|
export function evergreenLevelForXp(xp: number): number {
|
||||||
|
for (let i = 1; i < EVERGREEN_LEVEL_INFO.length; i++) {
|
||||||
|
if (xp >= EVERGREEN_LEVEL_INFO[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xpRequiredForEvergreenLevel(level: number): number {
|
||||||
|
return EVERGREEN_LEVEL_INFO[level - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps the given value between a minimum and maximum value
|
||||||
|
*/
|
||||||
|
export function clampValue(value: number, min: number, max: number) {
|
||||||
|
return Math.max(min, Math.min(value, max))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function castUserProfile(profile: UserProfile): UserProfile {
|
export function castUserProfile(profile: UserProfile): UserProfile {
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user