Added distinction between Mastery XP and Action XP

Added extended Profile Profile to main menu
Added support for Payout objectives on the score screen
Added flag for unlocking all shortcuts
Added flag for unlocking all Freelancer masteries
Added flag to allow Peacock to be restarted when the game is running and connected
Fixed issue where playstyle wasn't show properly
This commit is contained in:
Lennard Fonteijn 2023-03-18 22:53:14 +01:00 committed by Reece Dunham
parent ba9b799abe
commit 4031779a91
10 changed files with 471 additions and 74 deletions

View File

@ -1139,11 +1139,12 @@ export class ChallengeService extends ChallengeRegistry {
}
//NOTE: Official will always grant XP to both Location Mastery and the Player Profile
const totalXp =
(challenge.Xp || 0) + (challenge.Rewards?.MasteryXP || 0)
const actionXp = challenge.Xp || 0
const masteryXp = challenge.Rewards?.MasteryXP || 0
const xp = actionXp + masteryXp
this.grantLocationMasteryXp(totalXp, session, userData)
this.grantUserXp(totalXp, session, userData)
this.grantLocationMasteryXp(masteryXp, actionXp, session, userData)
this.grantUserXp(xp, session, userData)
writeUserData(userId, gameVersion)
@ -1163,6 +1164,7 @@ export class ChallengeService extends ChallengeRegistry {
grantLocationMasteryXp(
masteryXp: number,
actionXp: number,
contractSession: ContractSession,
userProfile: UserProfile,
): boolean {
@ -1194,6 +1196,7 @@ export class ChallengeService extends ChallengeRegistry {
const parentLocationIdLowerCase = parentLocationId.toLocaleLowerCase()
//Update the Location data
userProfile.Extensions.progression.Locations[
parentLocationIdLowerCase
] ??= {
@ -1209,7 +1212,7 @@ export class ChallengeService extends ChallengeRegistry {
const maxLevel = masteryData.MaxLevel || DEFAULT_MASTERY_MAXLEVEL
locationData.Xp = clampValue(
locationData.Xp + masteryXp,
locationData.Xp + masteryXp + actionXp,
0,
contract.Metadata.Type !== "evergreen"
? xpRequiredForLevel(maxLevel)
@ -1224,9 +1227,43 @@ export class ChallengeService extends ChallengeRegistry {
maxLevel,
)
//Update the SubLocation data
const profileData = userProfile.Extensions.progression.PlayerProfileXP
let foundSubLocation = profileData.Sublocations.find(
(e) => e.Location === parentLocationId,
)
if (!foundSubLocation) {
foundSubLocation = {
Location: parentLocationId,
Xp: 0,
ActionXp: 0,
}
profileData.Sublocations.push(foundSubLocation)
}
foundSubLocation.Xp = clampValue(
foundSubLocation.Xp + masteryXp,
0,
contract.Metadata.Type !== "evergreen"
? xpRequiredForLevel(maxLevel)
: xpRequiredForEvergreenLevel(maxLevel),
)
foundSubLocation.ActionXp += actionXp
//Update the EvergreenLevel with the latest Mastery Level
if (contract.Metadata.Type === "evergreen") {
userProfile.Extensions.CPD[contract.Metadata.CpdId][
"EvergreenLevel"
] = locationData.Level
}
return true
}
//TODO: Combine with grantLocationMasteryXp?
grantUserXp(
xp: number,
contractSession: ContractSession,

View File

@ -19,6 +19,7 @@
import { getUserData, writeUserData } from "./databaseHandler"
import { getConfig } from "./configSwizzleManager"
import { ContractProgressionData } from "./types/types"
import { getFlag } from "./flags"
export async function setCpd(
data: ContractProgressionData,
@ -51,11 +52,11 @@ export async function getCpd(
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
//NOTE: Override the EvergreenLevel with the latest Mastery Level
if (getFlag("gameplayUnlockAllFreelancerMasteries")) {
//TODO: Get rid of hardcoded values
userData.Extensions.CPD[cpdID]["EvergreenLevel"] = 100
}
return userData.Extensions.CPD[cpdID]
}

View File

@ -89,10 +89,22 @@ const defaultFlags: Flags = {
desc: "[Development - Workspace required] Toggle loading of plugins with a .ts/.cts extension inside the /plugins folder",
default: false,
},
developmentAllowRuntimeRestart: {
desc: "[Development] When set to true, it will be possible to restart Peacock while the game is running and connected.",
default: false,
},
legacyContractDownloader: {
desc: "When set to true, the official servers will be used for contract downloading in H3, which only works for the platform you are playing on. When false, the HITMAPS servers will be used instead. Note that this option only pertains to H3. Official servers will be used for H1 and H2 regardless of the value of this option.",
default: false,
},
gameplayUnlockAllShortcuts: {
desc: "When set to true, all shortcuts will always be unlocked.",
default: false,
},
gameplayUnlockAllFreelancerMasteries: {
desc: "When set to true, all Freelancer unlocks will always be available.",
default: false,
},
}
const OLD_FLAGS_FILE = "flags.json5"

View File

@ -357,18 +357,22 @@ app.use(
next()
}),
).use(async (req: RequestWithJwt, _res, next): Promise<void> => {
if (!req.jwt) {
)
if (getFlag("developmentAllowRuntimeRestart")) {
app.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()
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) {
return {

View File

@ -1801,6 +1801,7 @@ menuDataRouter.post(
createLoadSaveMiddleware("SaveMenuTemplate"),
)
//TODO: Add statistics
menuDataRouter.get("/PlayerProfile", (req: RequestWithJwt, res) => {
const playerProfilePage = getConfig<PlayerProfileView>(
"PlayerProfilePage",
@ -1813,6 +1814,28 @@ menuDataRouter.get("/PlayerProfile", (req: RequestWithJwt, res) => {
playerProfilePage.data.PlayerProfileXp.Level =
userProfile.Extensions.progression.PlayerProfileXP.ProfileLevel
const subLocationMap = new Map(
userProfile.Extensions.progression.PlayerProfileXP.Sublocations.map(
(obj) => [obj.Location, obj],
),
)
playerProfilePage.data.PlayerProfileXp.Seasons.forEach((e) =>
e.Locations.forEach((f) => {
const subLocationData = subLocationMap.get(f.LocationId)
f.Xp = subLocationData?.Xp || 0
f.ActionXp = subLocationData?.ActionXp || 0
if (f.LocationProgression) {
f.LocationProgression.Level =
userProfile.Extensions.progression.Locations[
f.LocationId.toLocaleLowerCase()
]?.Level || 1
}
}),
)
res.json(playerProfilePage)
})

View File

@ -547,21 +547,41 @@ profileRouter.post(
})
}
const unlockAllShortcuts = getFlag("gameplayUnlockAllShortcuts")
for (const challenge of challenges) {
challenge.Progression = Object.assign(
{
if (
unlockAllShortcuts &&
challenge.Challenge.Tags?.includes("shortcut")
) {
challenge.Progression = {
ChallengeId: challenge.Challenge.Id,
ProfileId: req.jwt.unique_name,
Completed: false,
State: {},
ETag: `W/"datetime'${encodeURIComponent(
new Date().toISOString(),
)}'"`,
CompletedAt: null,
Completed: true,
Ticked: true,
State: {
CurrentState: "Success",
},
// @ts-expect-error typescript hates dates
CompletedAt: new Date(new Date() - 10).toISOString(),
MustBeSaved: false,
},
challenge.Progression,
)
}
} else {
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)
@ -582,6 +602,7 @@ profileRouter.post(
),
},
LevelsDefinition: {
//TODO: Add Evergreen LevelInfo here?
Location: [0],
PlayerProfile: {
Version: 1,

View File

@ -35,6 +35,7 @@ import { getConfig } from "./configSwizzleManager"
import { _theLastYardbirdScpc, controller } from "./controller"
import type {
ContractSession,
GameChanger,
GameVersion,
MissionManifest,
MissionManifestObjective,
@ -76,6 +77,7 @@ import { MasteryData } from "./types/mastery"
* @param session The contract session.
* @returns The play-styles, ranked from best fit to worst fit.
*/
//TODO: This could use an update with more playstyles
export function calculatePlaystyle(
session: Partial<{ kills: Set<RatingKill> }>,
): Playstyle[] {
@ -728,11 +730,89 @@ export async function missionEnd(
const profileLevelInfoOffset = oldPlayerProfileLevel - 1
//Time
const timeTotal: Seconds =
(sessionDetails.timerEnd as number) -
(sessionDetails.timerStart as number)
//Playstyle
const calculatedPlaystyles = calculatePlaystyle(sessionDetails)
let playstyle =
calculatedPlaystyles[0].Score !== 0
? calculatedPlaystyles[0]
: undefined
//Calculate score and summary
const calculateScoreResult = calculateScore(
req.gameVersion,
req.query.contractSessionId,
sessionDetails,
contractData,
timeTotal,
)
//Evergreen
const evergreenData: MissionEndEvergreen = <MissionEndEvergreen>{}
const evergreenData: MissionEndEvergreen = <MissionEndEvergreen>{
PayoutsCompleted: [],
PayoutsFailed: [],
}
if (contractData.Metadata.Type === "evergreen") {
evergreenData.Payout = sessionDetails.evergreen.payout
const gameChangerProperties = getConfig<Record<string, GameChanger>>(
"EvergreenGameChangerProperties",
true,
)
let totalPayout = 0
//ASSUMPTION: All payout objectives have a "condition"-category objective
//and a "secondary"-category objective with a "MyPayout" in the context.
Object.keys(gameChangerProperties).forEach((e) => {
const gameChanger = gameChangerProperties[e]
const conditionObjective = gameChanger.Objectives.find(
(e) => e.Category === "condition",
)
const secondaryObjective = gameChanger.Objectives.find(
(e) =>
e.Category === "secondary" &&
e.Definition.Context["MyPayout"],
)
if (
conditionObjective &&
secondaryObjective &&
sessionDetails.objectiveStates.get(conditionObjective.Id) ===
"Success"
) {
const payoutObjective = {
Name: gameChanger.Name,
Payout: parseInt(
sessionDetails.objectiveContexts.get(
secondaryObjective.Id,
)["MyPayout"] || 0,
),
IsPrestige: gameChanger.IsPrestigeObjective || false,
}
if (
sessionDetails.objectiveStates.get(
secondaryObjective.Id,
) === "Success"
) {
totalPayout += payoutObjective.Payout
evergreenData.PayoutsCompleted.push(payoutObjective)
} else {
evergreenData.PayoutsFailed.push(payoutObjective)
}
}
})
logDebug("Payout", sessionDetails.evergreen.payout, totalPayout)
evergreenData.Payout = totalPayout
evergreenData.EndStateEventName =
sessionDetails.evergreen.scoringScreenEndState
@ -760,23 +840,17 @@ export async function missionEnd(
)
newLocationLevel = locationProgressionData.Level
//Debug
logDebug(
sessionDetails.failedObjectives,
sessionDetails.completedObjectives,
sessionDetails.objectiveContexts,
sessionDetails.objectiveStates,
)
//Override the silent assassin rank
if (calculateScoreResult.silentAssassin) {
playstyle = {
Id: "595f6ff1-85bf-4e4f-a9ee-76038a455648",
Name: "UI_PLAYSTYLE_ICA_STEALTH_ASSASSIN",
Type: "STEALTH_ASSASSIN",
Score: 0,
}
}
evergreenData.PayoutsCompleted = [
{
Name: "UI_CONTRACT_EVERGREEN_ELIMINATIONPAYOUT_TITLE",
Payout: 0,
IsPrestige: false,
},
]
evergreenData.PayoutsFailed = []
calculateScoreResult.silentAssassin = false
}
//Drops
@ -799,26 +873,6 @@ export async function missionEnd(
})
}
//Time
const timeTotal: Seconds =
(sessionDetails.timerEnd as number) -
(sessionDetails.timerStart as number)
//Playstyle
const calculatedPlaystyles = calculatePlaystyle(sessionDetails)
const playstyle =
calculatedPlaystyles[0].Score !== 0 ? calculatePlaystyle[0] : undefined
//Calculate score and summary
const calculateScoreResult = calculateScore(
req.gameVersion,
req.query.contractSessionId,
sessionDetails,
contractData,
timeTotal,
)
//Setup the result
const result: MissionEndResponse = {
MissionReward: {
@ -975,8 +1029,6 @@ export async function missionEnd(
}
//#endregion
logDebug(result)
res.json({
template:
req.gameVersion === "scpc"

View File

@ -379,6 +379,18 @@ export interface PlayerProfileView {
PlayerProfileXp: {
Total: number
Level: number
Seasons: {
Number: number
Locations: {
LocationId: string
Xp: number
ActionXp: number
LocationProgression?: {
Level: number
MaxLevel: number
}
}[]
}[]
}
}
}
@ -425,6 +437,11 @@ export interface UserProfile {
* The total amount of XP a user has obtained.
*/
Total: number
Sublocations: {
Location: string
Xp: number
ActionXp: number
}[]
}
Locations: {
[location: string]: {

View File

@ -139,7 +139,7 @@ export function getMaxProfileLevel(gameVersion: GameVersion): number {
* Minimum level returned is 1.
*/
export function levelForXp(xp: number): number {
return Math.floor(xp / XP_PER_LEVEL) + 1
return Math.min(1, Math.floor(xp / XP_PER_LEVEL) + 1)
}
/**
@ -150,6 +150,7 @@ export function xpRequiredForLevel(level: number): number {
return Math.max(0, (level - 1) * XP_PER_LEVEL)
}
//TODO: Determine some mathematical function
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,

View File

@ -6,9 +6,238 @@
"Total": 0,
"Level": 1,
"Seasons": [
{
"Number": 1,
"Locations": [
{
"LocationId": "LOCATION_PARENT_ICA_FACILITY",
"Xp": 0,
"ActionXp": 0
},
{
"LocationId": "LOCATION_PARENT_PARIS",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_COASTALTOWN",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_MARRAKECH",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_BANGKOK",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_COLORADO",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_HOKKAIDO",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
}
]
},
{
"Number": 2,
"Locations": [
{
"LocationId": "LOCATION_PARENT_NEWZEALAND",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 5
}
},
{
"LocationId": "LOCATION_PARENT_MIAMI",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_COLOMBIA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_MUMBAI",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_NORTHAMERICA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_NORTHSEA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_GREEDY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_OPULENT",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_AUSTRIA",
"Xp": 0,
"ActionXp": 0
},
{
"LocationId": "LOCATION_PARENT_SALTY",
"Xp": 0,
"ActionXp": 0
},
{
"LocationId": "LOCATION_PARENT_CAGED",
"Xp": 0,
"ActionXp": 0
}
]
},
{
"Number": 3,
"Locations": []
"Locations": [
{
"LocationId": "LOCATION_PARENT_GOLDEN",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_ANCESTRAL",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_EDGY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_WET",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_ELEGANT",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_TRAPPED",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 5
}
},
{
"LocationId": "LOCATION_PARENT_ROCKY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_SNUG",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 100
}
}
]
}
]
}