From ba9b799abe225df6fe80c11e9d86cad9276f5b8b Mon Sep 17 00:00:00 2001 From: Lennard Fonteijn Date: Sat, 18 Mar 2023 18:35:26 +0100 Subject: [PATCH] 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 --- components/candle/challengeService.ts | 174 +- components/candle/masteryService.ts | 45 +- components/contracts/contractsModeRouting.ts | 8 +- components/controller.ts | 40 +- components/databaseHandler.ts | 32 + components/eventHandler.ts | 40 +- components/evergreen.ts | 6 + components/index.ts | 14 +- components/menuData.ts | 5 +- components/menus/planning.ts | 3 +- components/profileHandler.ts | 57 +- components/scoreHandler.ts | 957 +++++---- components/types/challenges.ts | 5 + components/types/events.ts | 4 + components/types/score.ts | 127 ++ components/types/types.ts | 10 + components/utils.ts | 63 +- static/GlobalChallenges.json | 1871 +++++------------- 18 files changed, 1684 insertions(+), 1777 deletions(-) create mode 100644 components/types/score.ts diff --git a/components/candle/challengeService.ts b/components/candle/challengeService.ts index f71e2428..dd5fe113 100644 --- a/components/candle/challengeService.ts +++ b/components/candle/challengeService.ts @@ -32,10 +32,11 @@ import type { } from "../types/types" import { getUserData, writeUserData } from "../databaseHandler" -import { Controller } from "../controller" +import { controller, Controller } from "../controller" import { generateCompletionData, generateUserCentric, + getSubLocationByName, getSubLocationFromContract, } from "../contracts/dataGen" import { log, LogLevel } from "../loggingInterop" @@ -48,11 +49,21 @@ import { HandleEventOptions, } from "@peacockproject/statemachine-parser" import { SavedChallengeGroup } from "../types/challenges" -import { fastClone } from "../utils" +import { + clampValue, + DEFAULT_MASTERY_MAXLEVEL, + evergreenLevelForXp, + fastClone, + getMaxProfileLevel, + levelForXp, + xpRequiredForEvergreenLevel, + xpRequiredForLevel, +} from "../utils" import { ChallengeFilterOptions, ChallengeFilterType, filterChallenge, + inclusionDataCheck, mergeSavedChallengeGroups, } from "./challengeHelpers" import assert from "assert" @@ -324,6 +335,11 @@ export class ChallengeService extends ChallengeRegistry { let challenges: [string, RegistryChallenge[]][] = [] 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) if (groupContents) { let groupChallenges: RegistryChallenge[] | string[] = [ @@ -429,12 +445,42 @@ export class ChallengeService extends ChallengeRegistry { 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) for (const group of Object.keys(challengeGroups)) { for (const challenge of challengeGroups[group]) { 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: + (challenge?.Definition) + ?.Context || {}, + } + } + // For challenges with scopes being "profile" or "hit", // update challenge progression with the user's progression data const ctx = @@ -451,6 +497,7 @@ export class ChallengeService extends ChallengeRegistry { context: ctx, state: isDone ? "Success" : "Start", timers: [], + timesCompleted: 0, } } } @@ -488,6 +535,8 @@ export class ChallengeService extends ChallengeRegistry { currentState: data.state, timers: data.timers, timestamp: event.Timestamp, + //logger: (category, message) => + // log(LogLevel.DEBUG, `[${category}] ${message}`), } const previousState = data.state @@ -499,6 +548,7 @@ export class ChallengeService extends ChallengeRegistry { event.Value, options, ) + // For challenges with scopes being "profile" or "hit", // save challenge progression to the user's progression data if ( @@ -519,6 +569,7 @@ export class ChallengeService extends ChallengeRegistry { if (previousState !== "Success" && result.state === "Success") { this.onChallengeCompleted( + session, session.userId, session.gameVersion, challenge, @@ -991,6 +1042,7 @@ export class ChallengeService extends ChallengeRegistry { * @param gameVersion The game version. */ public tryToCompleteChallenge( + session: ContractSession, challengeId: string, userData: UserProfile, parentId: string, @@ -1037,6 +1089,7 @@ export class ChallengeService extends ChallengeRegistry { } this.onChallengeCompleted( + session, userData.Id, gameVersion, this.getChallengeById(challengeId), @@ -1045,6 +1098,7 @@ export class ChallengeService extends ChallengeRegistry { } private onChallengeCompleted( + session: ContractSession, userId: string, gameVersion: GameVersion, challenge: RegistryChallenge, @@ -1061,15 +1115,35 @@ export class ChallengeService extends ChallengeRegistry { 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] ??= { - State: {}, - Completed: false, - Ticked: false, + userData.Extensions.ChallengeProgression[challenge.Id] ??= { + State: {}, + Completed: 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) @@ -1078,6 +1152,7 @@ export class ChallengeService extends ChallengeRegistry { // Check if completing this challenge also completes any dependency trees depending on it for (const depTreeId of this._dependencyTree.keys()) { this.tryToCompleteChallenge( + session, depTreeId, userData, 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 + } } diff --git a/components/candle/masteryService.ts b/components/candle/masteryService.ts index 31f1f2aa..54127d06 100644 --- a/components/candle/masteryService.ts +++ b/components/candle/masteryService.ts @@ -27,7 +27,12 @@ import { MasteryPackage, } from "../types/mastery" 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 { private masteryData: Map = new Map() @@ -81,13 +86,13 @@ export class MasteryService { gameVersion: GameVersion, userId: string, ): CompletionData { - if (!this.masteryData.has(locationParentId)) { - return undefined - } - //Get the mastery data const masteryData: MasteryPackage = - this.masteryData.get(locationParentId) + this.getMasteryPackage(locationParentId) + + if (!masteryData) { + return undefined + } //Get the user profile const userProfile = getUserData(userId, gameVersion) @@ -107,11 +112,12 @@ export class MasteryService { lowerCaseLocationParentId ] - const maxLevel = masteryData.MaxLevel || 20 + const maxLevel = masteryData.MaxLevel || DEFAULT_MASTERY_MAXLEVEL - const nextLevel: number = Math.max( - 0, - Math.min(locationData.Level + 1, maxLevel), + const nextLevel: number = clampValue( + locationData.Level + 1, + 1, + maxLevel, ) const nextLevelXp: number = xpRequiredForLevel(nextLevel) @@ -119,7 +125,8 @@ export class MasteryService { Level: locationData.Level, MaxLevel: maxLevel, XP: locationData.Xp, - Completion: locationData.Xp / nextLevelXp, + Completion: + (XP_PER_LEVEL - (nextLevelXp - locationData.Xp)) / XP_PER_LEVEL, XpLeft: nextLevelXp - locationData.Xp, Id: masteryData.Id, 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( locationParentId: string, gameVersion: GameVersion, userId: string, ): MasteryData[] { - if (!this.masteryData.has(locationParentId)) { - return [] - } - //Get the mastery data const masteryData: MasteryPackage = - this.masteryData.get(locationParentId) + this.getMasteryPackage(locationParentId) - if (masteryData.Drops.length === 0) { + if (!masteryData || masteryData.Drops.length === 0) { return [] } diff --git a/components/contracts/contractsModeRouting.ts b/components/contracts/contractsModeRouting.ts index 2a1026f7..eefb7206 100644 --- a/components/contracts/contractsModeRouting.ts +++ b/components/contracts/contractsModeRouting.ts @@ -29,7 +29,11 @@ import { controller, preserveContracts } from "../controller" import { createLocationsData } from "../menus/destinations" import { userAuths } from "../officialServerAuth" import { log, LogLevel } from "../loggingInterop" -import { getRemoteService, contractCreationTutorialId } from "../utils" +import { + getRemoteService, + contractCreationTutorialId, + getMaxProfileLevel, +} from "../utils" export function contractsModeHome(req: RequestWithJwt, res: Response): void { const contractsHomeTemplate = getConfig("ContractsTemplate", false) @@ -58,7 +62,7 @@ export function contractsModeHome(req: RequestWithJwt, res: Response): void { XP: userData.Extensions.progression.PlayerProfileXP.Total, Level: userData.Extensions.progression.PlayerProfileXP .ProfileLevel, - MaxLevel: 7500, + MaxLevel: getMaxProfileLevel(req.gameVersion), }, }, }) diff --git a/components/controller.ts b/components/controller.ts index e08042bb..ab370138 100644 --- a/components/controller.ts +++ b/components/controller.ts @@ -28,6 +28,7 @@ import { import type { Campaign, ClientToServerEvent, + CompiledChallengeRuntimeData, ContractSession, GameVersion, GenSingleMissionFunc, @@ -36,6 +37,7 @@ import type { MissionManifest, PeacockLocationsData, PlayNextGetCampaignsHookReturn, + RegistryChallenge, RequestWithJwt, S2CEventWithTimestamp, SMFLastDeploy, @@ -81,7 +83,7 @@ import { createContext, Script } from "vm" import { ChallengeService } from "./candle/challengeService" import { getFlag } from "./flags" import { unpack } from "msgpackr" -import { ChallengePackage } from "./types/challenges" +import { ChallengePackage, SavedChallengeGroup } from "./types/challenges" import { promisify } from "util" import { brotliDecompress } from "zlib" 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 { + 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: [ + { + CategoryId: "global", + Challenges: globalChallenges, + }, + ], + meta: { + Location: "GLOBAL", + }, + }) + } + // Load mastery resources const masteryDirectory = join( PEACOCK_DEV ? process.cwd() : __dirname, diff --git a/components/databaseHandler.ts b/components/databaseHandler.ts index 3c31fea7..2813a7ea 100644 --- a/components/databaseHandler.ts +++ b/components/databaseHandler.ts @@ -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 } +/** + * 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 { + 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. * diff --git a/components/eventHandler.ts b/components/eventHandler.ts index 7f6078d2..50745570 100644 --- a/components/eventHandler.ts +++ b/components/eventHandler.ts @@ -31,7 +31,7 @@ import { Seconds, ServerToClientEvent, } from "./types/types" -import { contractTypes, extractToken, ServerVer } from "./utils" +import { contractTypes, extractToken, gameDifficulty, ServerVer } from "./utils" import { json as jsonMiddleware } from "body-parser" import { log, LogLevel } from "./loggingInterop" import { getUserData, writeUserData } from "./databaseHandler" @@ -47,6 +47,7 @@ import { AmbientChangedC2SEvent, BodyHiddenC2SEvent, ContractStartC2SEvent, + Evergreen_Payout_DataC2SEvent, HeroSpawn_LocationC2SEvent, ItemDroppedC2SEvent, ItemPickedUpC2SEvent, @@ -471,7 +472,25 @@ function saveEvents( const processed: string[] = [] const userData = getUserData(req.jwt.unique_name, req.gameVersion) 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 ( !session || @@ -562,6 +581,18 @@ function saveEvents( if (handleMultiplayerEvent(event, session)) { 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) { @@ -760,6 +791,11 @@ function saveEvents( contract.Metadata.CpdId, ) break + case "Evergreen_Payout_Data": + session.evergreen.payout = (( + event + )).Value.Total_Payout + break // Sinkhole events we don't care about case "ItemPickedUp": log( diff --git a/components/evergreen.ts b/components/evergreen.ts index d6cb12c4..f9581c3c 100644 --- a/components/evergreen.ts +++ b/components/evergreen.ts @@ -51,5 +51,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 + return userData.Extensions.CPD[cpdID] } diff --git a/components/index.ts b/components/index.ts index b199b751..50b0f475 100644 --- a/components/index.ts +++ b/components/index.ts @@ -80,6 +80,7 @@ import { multiplayerRouter } from "./multiplayer/multiplayerService" import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData" import { pack, unpack } from "msgpackr" import { liveSplitManager } from "./livesplit/liveSplitManager" +import { cheapLoadUserData } from "./databaseHandler" // welcome to the bleeding edge setFlagsFromString("--harmony") @@ -356,7 +357,18 @@ app.use( next() }), -) +).use(async (req: RequestWithJwt, _res, next): Promise => { + 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) { return { diff --git a/components/menuData.ts b/components/menuData.ts index 2a71ee94..e76daa2e 100644 --- a/components/menuData.ts +++ b/components/menuData.ts @@ -20,6 +20,7 @@ import { Response, Router } from "express" import { contractCreationTutorialId, gameDifficulty, + getMaxProfileLevel, PEACOCKVERSTRING, unlockOrderComparer, uuidRegex, @@ -322,7 +323,7 @@ menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => { XP: userdata.Extensions.progression.PlayerProfileXP.Total, Level: userdata.Extensions.progression.PlayerProfileXP .ProfileLevel, - MaxLevel: 7500, + MaxLevel: getMaxProfileLevel(req.gameVersion), }, }, }) @@ -1837,7 +1838,7 @@ menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => { XP: userData.Extensions.progression.PlayerProfileXP.Total, Level: userData.Extensions.progression.PlayerProfileXP .ProfileLevel, - MaxLevel: 7500, + MaxLevel: getMaxProfileLevel(req.gameVersion), }, }, }) diff --git a/components/menus/planning.ts b/components/menus/planning.ts index 36f73837..6d314b50 100644 --- a/components/menus/planning.ts +++ b/components/menus/planning.ts @@ -35,6 +35,7 @@ import { getUserData, writeUserData } from "../databaseHandler" import { fastClone, getDefaultSuitFor, + getMaxProfileLevel, nilUuid, unlockOrderComparer, } from "../utils" @@ -471,7 +472,7 @@ export async function planningView( XP: userData.Extensions.progression.PlayerProfileXP.Total, Level: userData.Extensions.progression.PlayerProfileXP .ProfileLevel, - MaxLevel: 7500, + MaxLevel: getMaxProfileLevel(req.gameVersion), }, }, }) diff --git a/components/profileHandler.ts b/components/profileHandler.ts index bd8290cc..34b123f2 100644 --- a/components/profileHandler.ts +++ b/components/profileHandler.ts @@ -18,7 +18,13 @@ import { Router } from "express" 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 { getPlatformEntitlements } from "./platformEntitlements" import { contractSessions, newSession } from "./eventHandler" @@ -542,36 +548,20 @@ profileRouter.post( } for (const challenge of challenges) { - // TODO: Add actual support for shortcut challenges - if (challenge.Challenge.Tags?.includes("shortcut")) { - challenge.Progression = { + challenge.Progression = Object.assign( + { ChallengeId: challenge.Challenge.Id, ProfileId: req.jwt.unique_name, - Completed: true, - Ticked: true, - State: { - CurrentState: "Success", - }, - // @ts-expect-error typescript hates dates - CompletedAt: new Date(new Date() - 10).toISOString(), + Completed: false, + State: {}, + ETag: `W/"datetime'${encodeURIComponent( + new Date().toISOString(), + )}'"`, + CompletedAt: null, MustBeSaved: false, - } - } 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, - ) - } + }, + challenge.Progression, + ) } res.json(challenges) @@ -595,8 +585,8 @@ profileRouter.post( Location: [0], PlayerProfile: { Version: 1, - XpPerLevel: 6000, - MaxLevel: 7500, + XpPerLevel: XP_PER_LEVEL, + MaxLevel: getMaxProfileLevel(req.gameVersion), }, }, }) @@ -817,6 +807,13 @@ async function loadSession( } // Update challenge progression with the user's latest progression data 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 = controller.challengeService.getChallengeById(cid).Definition.Scope if ( diff --git a/components/scoreHandler.ts b/components/scoreHandler.ts index 0e59c963..383c9efa 100644 --- a/components/scoreHandler.ts +++ b/components/scoreHandler.ts @@ -18,19 +18,27 @@ import type { Response } from "express" import { + clampValue, + DEFAULT_MASTERY_MAXLEVEL, difficultyToString, + evergreenLevelForXp, + EVERGREEN_LEVEL_INFO, handleAxiosError, isObjectiveActive, + levelForXp, PEACOCKVERSTRING, + xpRequiredForEvergreenLevel, xpRequiredForLevel, } from "./utils" import { contractSessions, getCurrentState } from "./eventHandler" -import { getConfig, getVersionedConfig } from "./configSwizzleManager" +import { getConfig } from "./configSwizzleManager" import { _theLastYardbirdScpc, controller } from "./controller" import type { ContractSession, + GameVersion, + MissionManifest, MissionManifestObjective, - PeacockLocationsData, + RatingKill, RequestWithJwt, Seconds, } from "./types/types" @@ -41,13 +49,25 @@ import { import { getUserData, writeUserData } from "./databaseHandler" import axios from "axios" import { getFlag } from "./flags" -import { log, LogLevel } from "./loggingInterop" -import { generateCompletionData } from "./contracts/dataGen" +import { log, logDebug, LogLevel } from "./loggingInterop" +import { + generateCompletionData, + getSubLocationByName, +} from "./contracts/dataGen" import { liveSplitManager } from "./livesplit/liveSplitManager" -import { Playstyle, ScoringBonus, ScoringHeadline } from "./types/scoring" +import { Playstyle, ScoringHeadline } from "./types/scoring" import { MissionEndRequestQuery } from "./types/gameSchemas" import { ChallengeFilterType } from "./candle/challengeHelpers" import { getCompletionPercent } from "./menus/destinations" +import { + CalculateXpResult, + CalculateScoreResult, + MissionEndResponse, + MissionEndDrop, + MissionEndEvergreen, + MissionEndChallenge, +} from "./types/score" +import { MasteryData } from "./types/mastery" /** * Checks the criteria of each possible play-style, ranking them by scoring. @@ -56,7 +76,9 @@ import { getCompletionPercent } from "./menus/destinations" * @param session The contract session. * @returns The play-styles, ranked from best fit to worst fit. */ -export function calculatePlaystyle(session: ContractSession): Playstyle[] { +export function calculatePlaystyle( + session: Partial<{ kills: Set }>, +): Playstyle[] { const playstylesCopy = getConfig("Playstyles", true) as Playstyle[] // Resetting the scores... @@ -183,10 +205,300 @@ export function calculatePlaystyle(session: ContractSession): Playstyle[] { return playstylesCopy } +export function calculateXp( + contractSession: ContractSession, +): CalculateXpResult { + const completedChallenges: MissionEndChallenge[] = [] + let totalXp = 0 + + //TODO: Merge with the non-global challenges? + for (const challengeId of Object.keys(contractSession.challengeContexts)) { + const data = contractSession.challengeContexts[challengeId] + + if (data.timesCompleted <= 0) { + continue + } + + const challenge = + controller.challengeService.getChallengeById(challengeId) + + if (!challenge || !challenge.Xp || !challenge.Tags.includes("global")) { + continue + } + + const challengeXp = challenge.Xp * data.timesCompleted + totalXp += challengeXp + + const challengeData = { + ChallengeId: challenge.Id, + ChallengeTags: challenge.Tags, + ChallengeName: challenge.Name, + ChallengeImageUrl: challenge.ImageName, + ChallengeDescription: challenge.Description, + //TODO: We probably have to use Repeatable here somehow to determine when to "repeat" a challenge. + XPGain: challengeXp, + IsGlobal: true, + IsActionReward: challenge.Tags.includes("actionreward"), + Drops: challenge.Drops, + } + + completedChallenges.push(challengeData) + } + + return { + completedChallenges: completedChallenges, + xp: totalXp, + } +} + +export function calculateScore( + gameVersion: GameVersion, + contractSessionId: string, + contractSession: ContractSession, + contractData: MissionManifest, + timeTotal: Seconds, +): CalculateScoreResult { + //Bonuses + const bonuses = [ + { + headline: "UI_SCORING_SUMMARY_OBJECTIVES", + bonusId: "AllObjectivesCompletedBonus", + condition: + gameVersion === "h1" || + contractData.Metadata.Id === + "2d1bada4-aa46-4954-8cf5-684989f1668a" || + contractData.Data.Objectives?.every( + (obj: MissionManifestObjective) => + obj.ExcludeFromScoring || + contractSession.completedObjectives.has(obj.Id) || + (obj.IgnoreIfInactive && + !isObjectiveActive( + obj, + contractSession.completedObjectives, + )) || + "Success" === + getCurrentState(contractSessionId, obj.Id), + ), + fractionNumerator: 2, + fractionDenominator: 3, + }, + { + headline: "UI_SCORING_SUMMARY_NOT_SPOTTED", + bonusId: "Unspotted", + condition: [ + ...contractSession.witnesses, + ...contractSession.spottedBy, + ].every( + (witness) => + (gameVersion === "h1" + ? false + : contractSession.targetKills.has(witness)) || + contractSession.npcKills.has(witness), + ), + }, + { + headline: "UI_SCORING_SUMMARY_NO_NOTICED_KILLS", + bonusId: "NoWitnessedKillsBonus", + condition: [...contractSession.killsNoticedBy].every( + (witness) => + (gameVersion === "h1" + ? true + : contractSession.targetKills.has(witness)) || + contractSession.npcKills.has(witness), + ), + }, + { + headline: "UI_SCORING_SUMMARY_NO_BODIES_FOUND", + bonusId: "NoBodiesFound", + condition: + contractSession.legacyHasBodyBeenFound === false && + [...contractSession.bodiesFoundBy].every( + (witness) => + (gameVersion === "h1" + ? false + : contractSession.targetKills.has(witness)) || + contractSession.npcKills.has(witness), + ), + }, + { + headline: "UI_SCORING_SUMMARY_NO_RECORDINGS", + bonusId: "SecurityErased", + condition: + contractSession.recording === "NOT_SPOTTED" || + contractSession.recording === "ERASED", + }, + ] + + //Non-target kills + const nonTargetKills = + contractData?.Metadata.AllowNonTargetKills === true + ? 0 + : contractSession.npcKills.size + contractSession.crowdNpcKills + + let totalScore = -5000 * nonTargetKills + + //Headlines and bonuses + const scoringHeadlines = [] + const awardedBonuses = [] + const failedBonuses = [] + + const headlineObjTemplate: Partial = { + type: "summary", + count: "", + scoreIsFloatingType: false, + fractionNumerator: 0, + fractionDenominator: 0, + scoreTotal: 20000, + } + + for (const bonus of bonuses) { + const bonusObj = { + Score: 20000, + Id: bonus.bonusId, + FractionNumerator: bonus.fractionNumerator || 0, + FractionDenominator: bonus.fractionDenominator || 0, + } + + const headlineObj = Object.assign( + {}, + headlineObjTemplate, + ) as ScoringHeadline + headlineObj.headline = bonus.headline + headlineObj.fractionNumerator = bonus.fractionNumerator || 0 + headlineObj.fractionDenominator = bonus.fractionDenominator || 0 + + if (bonus.condition) { + totalScore += 20000 + scoringHeadlines.push(headlineObj) + awardedBonuses.push(bonusObj) + } else { + bonusObj.Score = 0 + headlineObj.scoreTotal = 0 + scoringHeadlines.push(headlineObj) + failedBonuses.push(bonusObj) + } + } + + totalScore = Math.max(0, totalScore) + + scoringHeadlines.push( + Object.assign(Object.assign({}, headlineObjTemplate), { + headline: "UI_SCORING_SUMMARY_KILL_PENALTY", + count: nonTargetKills > 0 ? `${nonTargetKills}x-5000` : "", + scoreTotal: -5000 * nonTargetKills, + }) as ScoringHeadline, + ) + + //#region Time + const timeHours = Math.floor(timeTotal / 3600) + const timeMinutes = Math.floor((timeTotal - timeHours * 3600) / 60) + const timeSeconds = Math.floor( + timeTotal - timeHours * 3600 - timeMinutes * 60, + ) + let timebonus = 0 + + // formula from https://hitmanforumarchive.notex.app/#/t/how-the-time-bonus-is-calculated/17438 (https://archive.ph/pRjzI) + const scorePoints = [ + [0, 1.1], // 1.1 bonus multiplier at 0 secs (0 min) + [300, 0.7], // 0.7 bonus multiplier at 300 secs (5 min) + [900, 0.6], // 0.6 bonus multiplier at 900 secs (15 min) + [17100, 0.0], // 0 bonus multiplier at 17100 secs (285 min) + ] + + let prevsecs: number, prevmultiplier: number + + for (const [secs, multiplier] of scorePoints) { + if (timeTotal > secs) { + prevsecs = secs + prevmultiplier = multiplier + continue + } + + // linear interpolation between current and previous scorePoints + const bonusMultiplier = + prevmultiplier! - + ((prevmultiplier! - multiplier) * (timeTotal - prevsecs!)) / + (secs - prevsecs!) + + timebonus = totalScore * bonusMultiplier + break + } + + timebonus = Math.round(timebonus) + + const totalScoreWithBonus = totalScore + timebonus + + awardedBonuses.push({ + Score: timebonus, + Id: "SwiftExecution", + FractionNumerator: 0, + FractionDenominator: 0, + }) + + scoringHeadlines.push( + Object.assign(Object.assign({}, headlineObjTemplate), { + headline: "UI_SCORING_SUMMARY_TIME", + count: `${`0${timeHours}`.slice(-2)}:${`0${timeMinutes}`.slice( + -2, + )}:${`0${timeSeconds}`.slice(-2)}`, + scoreTotal: timebonus, + }) as ScoringHeadline, + ) + //#endregion + + for (const type of ["total", "subtotal"]) { + scoringHeadlines.push( + Object.assign(Object.assign({}, headlineObjTemplate), { + type, + headline: `UI_SCORING_SUMMARY_${type.toUpperCase()}`, + scoreTotal: totalScoreWithBonus, + }) as ScoringHeadline, + ) + } + + //Stars + let stars = + 5 - + [...bonuses, { condition: nonTargetKills === 0 }].filter( + (x) => !x!.condition, + ).length // one star less for each bonus missed + + stars = stars < 0 ? 0 : stars // clamp to 0 + + //Achieved masteries + const achievedMasteries = [ + { + score: -5000 * nonTargetKills, + RatioParts: nonTargetKills, + RatioTotal: nonTargetKills, + Id: "KillPenaltyMastery", + BaseScore: -5000, + }, + ] + + //NOTE: need to have all bonuses except objectives for SA + const silentAssassin = [ + ...bonuses.slice(1), + { condition: nonTargetKills === 0 }, + ].every((x) => x.condition) + + return { + stars: stars, + scoringHeadlines: scoringHeadlines, + achievedMasteries: achievedMasteries, + awardedBonuses: awardedBonuses, + failedBonuses: failedBonuses, + silentAssassin: silentAssassin, + score: totalScore, + scoreWithBonus: totalScoreWithBonus, + } +} + export async function missionEnd( req: RequestWithJwt, res: Response, ): Promise { + //Resolve the contract session if (!req.query.contractSessionId) { res.status(400).end() return @@ -195,19 +507,19 @@ export async function missionEnd( const sessionDetails = contractSessions.get(req.query.contractSessionId) if (!sessionDetails) { - // contract session not found - res.status(404).end() + res.status(404).send("contract session not found") return } if (sessionDetails.userId !== req.jwt.unique_name) { - // requested score for other user's session - res.status(401).end() + res.status(401).send("requested score for other user's session") return } + //Resolve userdata const userData = getUserData(req.jwt.unique_name, req.gameVersion) + //Resolve contract data const contractData = req.gameVersion === "scpc" && sessionDetails.contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d" @@ -215,11 +527,11 @@ export async function missionEnd( : controller.resolveContract(sessionDetails.contractId) if (!contractData) { - // contract not found res.status(404).send("contract not found") return } + //Handle escalation groups if (contractData.Metadata.Type === "escalation") { const eGroupId = contractIdToEscalationGroupId( sessionDetails.contractId, @@ -259,36 +571,24 @@ export async function missionEnd( writeUserData(req.jwt.unique_name, req.gameVersion) } - const nonTargetKills = - contractData?.Metadata.AllowNonTargetKills === true - ? 0 - : sessionDetails.npcKills.size + sessionDetails.crowdNpcKills - - const locations = getVersionedConfig( - "LocationsData", + //Resolve the id of the parent location + const subLocation = getSubLocationByName( + contractData.Metadata.Location, req.gameVersion, - true, ) - const location = contractData.Metadata.Location - const parent = locations.children[location].Properties.ParentLocation - const locationChallenges = - controller.challengeService.getGroupedChallengeLists( - { - type: ChallengeFilterType.None, - }, - parent, - ) - const contractChallenges = - controller.challengeService.getChallengesForContract( - sessionDetails.contractId, - req.gameVersion, - ) - const challengeCompletion = - controller.challengeService.countTotalNCompletedChallenges( - locationChallenges, - userData.Id, - req.gameVersion, - ) + + const locationParentId = subLocation + ? subLocation.Properties?.ParentLocation + : contractData.Metadata.Location + + if (!locationParentId) { + res.status(404).send("location parentid not found") + return + } + + const locationParentIdLowerCase = locationParentId.toLocaleLowerCase() + + //Resolve all opportunities for the location const opportunities = contractData.Metadata.Opportunities const opportunityCount = opportunities ? opportunities.length : 0 const opportunityCompleted = opportunities @@ -296,326 +596,301 @@ export async function missionEnd( (ms) => ms in userData.Extensions.opportunityprogression, ).length : 0 - const result = { + + //Resolve all challenges for the location + const locationChallenges = + controller.challengeService.getGroupedChallengeLists( + { + type: ChallengeFilterType.None, + }, + locationParentId, + ) + const contractChallenges = + controller.challengeService.getChallengesForContract( + sessionDetails.contractId, + req.gameVersion, + ) + const locationChallengeCompletion = + controller.challengeService.countTotalNCompletedChallenges( + locationChallenges, + userData.Id, + req.gameVersion, + ) + + const contractChallengeCompletion = + controller.challengeService.countTotalNCompletedChallenges( + contractChallenges, + userData.Id, + req.gameVersion, + ) + + const locationPercentageComplete = getCompletionPercent( + locationChallengeCompletion.CompletedChallengesCount, + locationChallengeCompletion.ChallengesCount, + opportunityCompleted, + opportunityCount, + ) + + //Get the location and playerprofile progression from the userdata + if (!userData.Extensions.progression.Locations[locationParentIdLowerCase]) { + userData.Extensions.progression.Locations[locationParentIdLowerCase] = { + Xp: 0, + Level: 1, + } + } + + const locationProgressionData = + userData.Extensions.progression.Locations[locationParentIdLowerCase] + const playerProgressionData = + userData.Extensions.progression.PlayerProfileXP + + //Calculate XP based on all challenges, including the global ones. + const calculateXpResult: CalculateXpResult = calculateXp(sessionDetails) + let justTickedChallenges = 0 + let masteryXpGain = 0 + + Object.values(contractChallenges) + .flat() + .filter((challengeData) => { + return ( + !challengeData.Tags.includes("global") && + controller.challengeService.fastGetIsUnticked( + userData, + challengeData.Id, + ) + ) + }) + .forEach((challengeData) => { + const userId = req.jwt.unique_name + const gameVersion = req.gameVersion + + userData.Extensions.ChallengeProgression[challengeData.Id].Ticked = + true + writeUserData(userId, gameVersion) + + justTickedChallenges++ + + masteryXpGain += challengeData.Rewards.MasteryXP + + calculateXpResult.completedChallenges.push({ + ChallengeId: challengeData.Id, + ChallengeTags: challengeData.Tags, + ChallengeName: challengeData.Name, + ChallengeImageUrl: challengeData.ImageName, + ChallengeDescription: challengeData.Description, + XPGain: challengeData.Rewards.MasteryXP, + IsGlobal: false, + IsActionReward: challengeData.Tags.includes("actionreward"), + Drops: challengeData.Drops, + }) + }) + + //NOTE: Official doesn't seem to make up it's mind whether or not XPGain is the same for both Mastery and Profile... + const totalXpGain = calculateXpResult.xp + masteryXpGain + + //Calculate the old location progression based on the current one and process it + const oldLocationXp = locationProgressionData.Xp - masteryXpGain + let oldLocationLevel = levelForXp(oldLocationXp) + const newLocationXp = locationProgressionData.Xp + let newLocationLevel = levelForXp(newLocationXp) + + const masteryData = + controller.masteryService.getMasteryPackage(locationParentId) + + const maxLevel = masteryData.MaxLevel || DEFAULT_MASTERY_MAXLEVEL + + let locationLevelInfo = Array.from({ length: maxLevel }, (_, i) => { + return xpRequiredForLevel(i + 1) + }) + + const completionData = generateCompletionData( + contractData.Metadata.Location, + req.jwt.unique_name, + req.gameVersion, + ) + + //Calculate the old playerprofile progression based on the current one and process it + const oldPlayerProfileXp = playerProgressionData.Total - totalXpGain + const oldPlayerProfileLevel = levelForXp(oldPlayerProfileXp) + const newPlayerProfileXp = playerProgressionData.Total + const newPlayerProfileLevel = levelForXp(newPlayerProfileXp) + + //NOTE: We assume the ProfileLevel is currently already up-to-date + const profileLevelInfo = [] + + for ( + let level = oldPlayerProfileLevel; + level <= newPlayerProfileLevel + 1; + level++ + ) { + profileLevelInfo.push(xpRequiredForLevel(level)) + } + + const profileLevelInfoOffset = oldPlayerProfileLevel - 1 + + //Evergreen + const evergreenData: MissionEndEvergreen = {} + + if (contractData.Metadata.Type === "evergreen") { + evergreenData.Payout = sessionDetails.evergreen.payout + evergreenData.EndStateEventName = + sessionDetails.evergreen.scoringScreenEndState + + locationLevelInfo = EVERGREEN_LEVEL_INFO + + const currentLevelRequiredXp = xpRequiredForEvergreenLevel( + locationProgressionData.Level, + ) + const nextLevelRequiredXp = clampValue( + xpRequiredForEvergreenLevel(locationProgressionData.Level + 1), + 1, + 100, + ) + + //Override completion data for proper animations + completionData.XP = locationProgressionData.Xp + completionData.Level = locationProgressionData.Level + completionData.Completion = + (currentLevelRequiredXp - locationProgressionData.Xp) / + (nextLevelRequiredXp - currentLevelRequiredXp) + + //Override the location levels to trigger potential drops + oldLocationLevel = evergreenLevelForXp( + locationProgressionData.Xp - totalXpGain, + ) + newLocationLevel = locationProgressionData.Level + + //Debug + logDebug( + sessionDetails.failedObjectives, + sessionDetails.completedObjectives, + sessionDetails.objectiveContexts, + sessionDetails.objectiveStates, + ) + + evergreenData.PayoutsCompleted = [ + { + Name: "UI_CONTRACT_EVERGREEN_ELIMINATIONPAYOUT_TITLE", + Payout: 0, + IsPrestige: false, + }, + ] + + evergreenData.PayoutsFailed = [] + } + + //Drops + let drops: MissionEndDrop[] = [] + + if (newLocationLevel - oldLocationLevel > 0) { + const masteryData = + controller.masteryService.getMasteryDataForDestination( + locationParentId, + req.gameVersion, + req.jwt.unique_name, + ) as MasteryData[] + + drops = masteryData[0].Drops.filter( + (e) => e.Level > oldLocationLevel && e.Level <= newLocationLevel, + ).map((e) => { + return { + Unlockable: e.Unlockable, + } + }) + } + + //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: { LocationProgression: { - LevelInfo: Array.from({ length: 1 }, (_, i) => - xpRequiredForLevel(i + 1), - ), - XP: 0, - Level: 1, - Completion: 1, - XPGain: 0, - HideProgression: false, + LevelInfo: locationLevelInfo, + XP: completionData.XP, + Level: completionData.Level, + Completion: completionData.Completion, + //NOTE: Official makes this 0 if maximum Mastery is reached + XPGain: completionData.Level === maxLevel ? 0 : totalXpGain, + HideProgression: masteryData.HideProgression || false, }, ProfileProgression: { - LevelInfo: [0, 6000], - LevelInfoOffset: 0, - XP: userData.Extensions.progression.PlayerProfileXP.Total, - Level: userData.Extensions.progression.PlayerProfileXP - .ProfileLevel, - XPGain: 0, + LevelInfo: profileLevelInfo, + LevelInfoOffset: profileLevelInfoOffset, + XP: newPlayerProfileXp, + Level: newPlayerProfileLevel, + XPGain: totalXpGain, }, - Challenges: Object.values(contractChallenges) - .flat() - .filter((challengeData) => { - return controller.challengeService.fastGetIsUnticked( - userData, - challengeData.Id, - ) - }) - .map((challengeData) => { - const userId = req.jwt.unique_name - const gameVersion = req.gameVersion - userData.Extensions.ChallengeProgression[ - challengeData.Id - ].Ticked = true - writeUserData(userId, gameVersion) - return { - ChallengeId: challengeData.Id, - ChallengeTags: challengeData.Tags, - ChallengeName: challengeData.Name, - ChallengeImageUrl: challengeData.ImageName, - ChallengeDescription: challengeData.Description, - XPGain: challengeData.Rewards.MasteryXP, - IsGlobal: challengeData.Name.includes("GLOBAL"), - IsActionReward: - challengeData.Tags.includes("actionreward"), - Drops: challengeData.Drops, - } - }), - Drops: [], - OpportunityRewards: [], // ? - CompletionData: generateCompletionData( - contractData.Metadata.Location, - req.jwt.unique_name, - req.gameVersion, - ), - ChallengeCompletion: challengeCompletion, - ContractChallengeCompletion: - controller.challengeService.countTotalNCompletedChallenges( - contractChallenges, - userData.Id, - req.gameVersion, - ), + Challenges: calculateXpResult.completedChallenges, + Drops: drops, + //TODO: Do these exist? Appears to be optional. + OpportunityRewards: [], + CompletionData: completionData, + ChallengeCompletion: locationChallengeCompletion, + ContractChallengeCompletion: contractChallengeCompletion, OpportunityStatistics: { Count: opportunityCount, Completed: opportunityCompleted, }, - LocationCompletionPercent: getCompletionPercent( - challengeCompletion.CompletedChallengesCount, - challengeCompletion.ChallengesCount, - opportunityCompleted, - opportunityCount, - ), + LocationCompletionPercent: locationPercentageComplete, }, ScoreOverview: { - XP: 0, - Level: 1, - Completion: 1, + XP: completionData.XP, + Level: completionData.Level, + Completion: completionData.Completion, + //NOTE: Official appears to always make this 0 XPGain: 0, - ChallengesCompleted: 0, - LocationHideProgression: false, + ChallengesCompleted: justTickedChallenges, + LocationHideProgression: masteryData.HideProgression || false, + ProdileId1: req.jwt.unique_name, + stars: calculateScoreResult.stars, ScoreDetails: { - Headlines: [] as ScoringHeadline[], + Headlines: calculateScoreResult.scoringHeadlines, }, - stars: 0, - SilentAssassin: false, ContractScore: { - AchievedMasteries: [ - { - score: -5000 * nonTargetKills, - RatioParts: nonTargetKills, - RatioTotal: nonTargetKills, - Id: "KillPenaltyMastery", - BaseScore: -5000, - }, - ], - TotalNoMultipliers: 0, - AwardedBonuses: [] as ScoringBonus[], - FailedBonuses: [] as ScoringBonus[], - Total: 0, - StarCount: 0, - SilentAssassin: false, - TimeUsedSecs: 0, + Total: calculateScoreResult.scoreWithBonus, + AchievedMasteries: calculateScoreResult.achievedMasteries, + AwardedBonuses: calculateScoreResult.awardedBonuses, + TotalNoMultipliers: calculateScoreResult.score, + TimeUsedSecs: timeTotal, + StarCount: calculateScoreResult.stars, + FailedBonuses: calculateScoreResult.failedBonuses, + SilentAssassin: calculateScoreResult.silentAssassin, }, - // todo + SilentAssassin: calculateScoreResult.silentAssassin, + //TODO: Use data from the leaderboard? NewRank: 1, RankCount: 1, Rank: 1, FriendsRankCount: 1, FriendsRank: 1, IsPartOfTopScores: false, - PlayStyle: {}, + PlayStyle: playstyle, + IsNewBestScore: false, + IsNewBestTime: false, + IsNewBestStars: false, + Evergreen: evergreenData, }, } - const bonuses = [ - { - headline: "UI_SCORING_SUMMARY_OBJECTIVES", - bonusId: "AllObjectivesCompletedBonus", - condition: - req.gameVersion === "h1" || - contractData.Metadata.Id === - "2d1bada4-aa46-4954-8cf5-684989f1668a" || - contractData.Data.Objectives?.every( - (obj: MissionManifestObjective) => - obj.ExcludeFromScoring || - sessionDetails.completedObjectives.has(obj.Id) || - (obj.IgnoreIfInactive && - !isObjectiveActive( - obj, - sessionDetails.completedObjectives, - )) || - "Success" === - getCurrentState( - req.query.contractSessionId!, - obj.Id, - ), - ), - }, - { - headline: "UI_SCORING_SUMMARY_NOT_SPOTTED", - bonusId: "Unspotted", - condition: [ - ...sessionDetails.witnesses, - ...sessionDetails.spottedBy, - ].every( - (witness) => - (req.gameVersion === "h1" - ? false - : sessionDetails.targetKills.has(witness)) || - sessionDetails.npcKills.has(witness), - ), - }, - { - headline: "UI_SCORING_SUMMARY_NO_NOTICED_KILLS", - bonusId: "NoWitnessedKillsBonus", - condition: [...sessionDetails.killsNoticedBy].every( - (witness) => - (req.gameVersion === "h1" - ? true - : sessionDetails.targetKills.has(witness)) || - sessionDetails.npcKills.has(witness), - ), - }, - { - headline: "UI_SCORING_SUMMARY_NO_BODIES_FOUND", - bonusId: "NoBodiesFound", - condition: - sessionDetails.legacyHasBodyBeenFound === false && - [...sessionDetails.bodiesFoundBy].every( - (witness) => - (req.gameVersion === "h1" - ? false - : sessionDetails.targetKills.has(witness)) || - sessionDetails.npcKills.has(witness), - ), - }, - { - headline: "UI_SCORING_SUMMARY_NO_RECORDINGS", - bonusId: "SecurityErased", - condition: - sessionDetails.recording === "NOT_SPOTTED" || - sessionDetails.recording === "ERASED", - }, - ] - - let stars = - 5 - - [...bonuses, { condition: nonTargetKills === 0 }].filter( - (x) => !x!.condition, - ).length // one star less for each bonus missed - - stars = stars < 0 ? 0 : stars // clamp to 0 - - let total = -5000 * nonTargetKills - - const headlineObjTemplate: Partial = { - type: "summary", - count: "", - scoreIsFloatingType: false, - fractionNumerator: 0, - fractionDenominator: 0, - scoreTotal: 20000, - } - - for (const bonus of bonuses) { - const bonusObj = { - Score: 20000, - Id: bonus.bonusId, - FractionNumerator: 0, - FractionDenominator: 0, - } - const headlineObj = Object.assign( - {}, - headlineObjTemplate, - ) as ScoringHeadline - headlineObj.headline = bonus.headline - - if (bonus.condition) { - total += 20000 - result.ScoreOverview.ScoreDetails.Headlines.push(headlineObj) - result.ScoreOverview.ContractScore.AwardedBonuses.push(bonusObj) - } else { - bonusObj.Score = 0 - headlineObj.scoreTotal = 0 - result.ScoreOverview.ScoreDetails.Headlines.push(headlineObj) - result.ScoreOverview.ContractScore.FailedBonuses.push(bonusObj) - } - } - - total = Math.max(total, 0) - result.ScoreOverview.ContractScore.TotalNoMultipliers = - result.ScoreOverview.ContractScore.Total = total - - result.ScoreOverview.ScoreDetails.Headlines.push( - Object.assign(Object.assign({}, headlineObjTemplate), { - headline: "UI_SCORING_SUMMARY_KILL_PENALTY", - count: nonTargetKills > 0 ? `${nonTargetKills}x-5000` : "", - scoreTotal: -5000 * nonTargetKills, - }) as ScoringHeadline, - ) - - //#region Time - const timeTotal: Seconds = - (sessionDetails.timerEnd as number) - - (sessionDetails.timerStart as number) - result.ScoreOverview.ContractScore.TimeUsedSecs = timeTotal - - const timeHours = Math.floor(timeTotal / 3600) - const timeMinutes = Math.floor((timeTotal - timeHours * 3600) / 60) - const timeSeconds = Math.floor( - timeTotal - timeHours * 3600 - timeMinutes * 60, - ) - let timebonus = 0 - - // formula from https://hitmanforumarchive.notex.app/#/t/how-the-time-bonus-is-calculated/17438 (https://archive.ph/pRjzI) - const scorePoints = [ - [0, 1.1], // 1.1 bonus multiplier at 0 secs (0 min) - [300, 0.7], // 0.7 bonus multiplier at 300 secs (5 min) - [900, 0.6], // 0.6 bonus multiplier at 900 secs (15 min) - [17100, 0.0], // 0 bonus multiplier at 17100 secs (285 min) - ] - - let prevsecs: number, prevmultiplier: number - - for (const [secs, multiplier] of scorePoints) { - if (timeTotal > secs) { - prevsecs = secs - prevmultiplier = multiplier - continue - } - - // linear interpolation between current and previous scorePoints - const bonusMultiplier = - prevmultiplier! - - ((prevmultiplier! - multiplier) * (timeTotal - prevsecs!)) / - (secs - prevsecs!) - timebonus = total * bonusMultiplier - break - } - - timebonus = Math.round(timebonus) - - total += timebonus - - result.ScoreOverview.ContractScore.AwardedBonuses.push({ - Score: timebonus, - Id: "SwiftExecution", - FractionNumerator: 0, - FractionDenominator: 0, - }) - - result.ScoreOverview.ScoreDetails.Headlines.push( - Object.assign(Object.assign({}, headlineObjTemplate), { - headline: "UI_SCORING_SUMMARY_TIME", - count: `${`0${timeHours}`.slice(-2)}:${`0${timeMinutes}`.slice( - -2, - )}:${`0${timeSeconds}`.slice(-2)}`, - scoreTotal: timebonus, - }) as ScoringHeadline, - ) - //#endregion - - for (const type of ["total", "subtotal"]) { - result.ScoreOverview.ScoreDetails.Headlines.push( - Object.assign(Object.assign({}, headlineObjTemplate), { - type, - headline: `UI_SCORING_SUMMARY_${type.toUpperCase()}`, - scoreTotal: total, - }) as ScoringHeadline, - ) - } - - result.ScoreOverview.stars = result.ScoreOverview.ContractScore.StarCount = - stars - result.ScoreOverview.SilentAssassin = - result.ScoreOverview.ContractScore.SilentAssassin = [ - ...bonuses.slice(1), - { condition: nonTargetKills === 0 }, - ].every((x) => x.condition) // need to have all bonuses except objectives for SA - + //Finalize the response if ((getFlag("autoSplitterForceSilentAssassin") as boolean) === true) { if (result.ScoreOverview.SilentAssassin) { await liveSplitManager.completeMission(timeTotal) @@ -626,12 +901,6 @@ export async function missionEnd( await liveSplitManager.completeMission(timeTotal) } - // Playstyles - const calculatedPlaystyles = calculatePlaystyle(sessionDetails) - if (calculatedPlaystyles[0].Score !== 0) { - result.ScoreOverview.PlayStyle = calculatedPlaystyles[0] - } - //#region Leaderboards if ( getFlag("leaderboards") === true && @@ -656,10 +925,10 @@ export async function missionEnd( req.jwt.platform === "epic" ? userData.EpicId : userData.SteamId, - score: total, + score: calculateScoreResult.scoreWithBonus, data: { Score: { - Total: total, + Total: calculateScoreResult.scoreWithBonus, AchievedMasteries: result.ScoreOverview.ContractScore .AchievedMasteries, @@ -674,7 +943,7 @@ export async function missionEnd( FailedBonuses: null, IsVR: false, SilentAssassin: result.ScoreOverview.SilentAssassin, - StarCount: stars, + StarCount: calculateScoreResult.stars, }, GroupIndex: 0, // TODO sniper scores @@ -706,6 +975,8 @@ export async function missionEnd( } //#endregion + logDebug(result) + res.json({ template: req.gameVersion === "scpc" diff --git a/components/types/challenges.ts b/components/types/challenges.ts index 1c659e48..86852e6d 100644 --- a/components/types/challenges.ts +++ b/components/types/challenges.ts @@ -40,10 +40,15 @@ export interface SavedChallenge { ParentLocationId: string Type: "Hit" | string RuntimeType: "contract" | string + Xp: number XpModifier?: unknown DifficultyLevels: string[] Definition: MissionManifestObjective["Definition"] & { Scope: ContextScopedStorageLocation + Repeatable?: { + Base: number + Delta: number + } } Tags: string[] InclusionData?: InclusionData diff --git a/components/types/events.ts b/components/types/events.ts index f3c82594..796194fa 100644 --- a/components/types/events.ts +++ b/components/types/events.ts @@ -253,3 +253,7 @@ export type Dart_HitC2SEvent = ClientToServerEvent<{ ActorType: number Sedative: "" | string }> + +export type Evergreen_Payout_DataC2SEvent = ClientToServerEvent<{ + Total_Payout: number +}> diff --git a/components/types/score.ts b/components/types/score.ts new file mode 100644 index 00000000..8172cd2d --- /dev/null +++ b/components/types/score.ts @@ -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 + } +} diff --git a/components/types/types.ts b/components/types/types.ts index 7aa8b63d..65b449a1 100644 --- a/components/types/types.ts +++ b/components/types/types.ts @@ -262,8 +262,18 @@ export interface ContractSession { context: unknown state: string timers: Timer[] + timesCompleted: number } } + /** + * Session Evergreen details. + * + * @since v6.0.0 + */ + evergreen?: { + payout: number + scoringScreenEndState: string + } } /** diff --git a/components/utils.ts b/components/utils.ts index 4a81a299..60de0ffd 100644 --- a/components/utils.ts +++ b/components/utils.ts @@ -123,8 +123,69 @@ export function extractToken( 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 { - 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 { diff --git a/static/GlobalChallenges.json b/static/GlobalChallenges.json index be7416e4..5f217dad 100644 --- a/static/GlobalChallenges.json +++ b/static/GlobalChallenges.json @@ -10,24 +10,15 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { - "Start": { - "PollResult_Drown": { - "Transition": "Active" - } - }, + "Start": { "PollResult_Drown": { "Transition": "Active" } }, "Active": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", true] - }, + { "$eq": ["$Value.IsTarget", true] }, { "$eq": [ "$Value.KillMethodStrict", @@ -43,7 +34,7 @@ }, "Tags": ["actionreward", "high", "crowdchoice"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.9687062", + "LastModified": "2023-01-26T11:26:56.6680525", "Xp": 100, "XpModifier": {}, "PlayableSince": null, @@ -54,6 +45,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -66,9 +58,8 @@ "Locations": null, "GameModes": null }, - "CrowdChoice": { - "Tag": "Drown" - } + "CrowdChoice": { "Tag": "Drown" }, + "OrderIndex": 10000 } }, { @@ -81,13 +72,8 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "IntelItemPickedup": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "IntelItemPickedup": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -130,7 +116,7 @@ }, "Tags": ["actionreward", "low", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.4181805", + "LastModified": "2023-01-26T11:26:56.4048111", "Xp": 10, "XpModifier": {}, "PlayableSince": null, @@ -141,13 +127,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -160,19 +148,12 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { - "Spotted": { - "Transition": "Failure" - }, + "Spotted": { "Transition": "Failure" }, "ContractEnd": { "Actions": { "$pushunique": ["_SessionRewards", true] @@ -184,7 +165,7 @@ }, "Tags": ["actionreward", "intermediate", "mission"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.7795353", + "LastModified": "2023-01-26T11:26:56.5750934", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -195,13 +176,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -214,10 +197,7 @@ "ImageName": null, "Definition": { "Context": {}, - "Repeatable": { - "Base": 10, - "Delta": 10 - }, + "Repeatable": { "Base": 10, "Delta": 10 }, "Scope": "profile", "States": { "Start": { @@ -227,9 +207,7 @@ { "$eq": ["$Value.KillClass", "explosion"] }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.IsTarget", true] } ] }, "Transition": "Success" @@ -239,7 +217,7 @@ }, "Tags": ["story", "stats", "hideinhud"], "Drops": [], - "LastModified": "2021-01-06T23:00:29.1088907", + "LastModified": "2023-01-26T11:26:56.7446747", "Xp": 0, "XpModifier": {}, "PlayableSince": null, @@ -260,7 +238,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -274,34 +253,21 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Checkbodyfound": { "$timer": { - "Condition": { - "$after": 5 - }, + "Condition": { "$after": 5 }, "Transition": "Start" }, - "BodyFound": { - "Transition": "Failure" - }, - "Spotted": { - "Transition": "Failure" - }, - "Unnoticed_Kill": { - "Transition": "Success" - } + "BodyFound": { "Transition": "Failure" }, + "Spotted": { "Transition": "Failure" }, + "Unnoticed_Kill": { "Transition": "Success" } }, "Start": { "Kill": { - "Condition": { - "$eq": ["$Value.IsTarget", true] - }, + "Condition": { "$eq": ["$Value.IsTarget", true] }, "Transition": "Checkbodyfound" } } @@ -309,7 +275,7 @@ }, "Tags": ["actionreward", "intermediate", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.4530638", + "LastModified": "2023-01-26T11:26:56.4235586", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -320,13 +286,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -340,22 +308,15 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { - "Start": { - "AreaDiscovered": { - "Transition": "Success" - } - } + "Start": { "AreaDiscovered": { "Transition": "Success" } } } }, "Tags": ["actionreward", "mediumxp+", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.3884107", + "LastModified": "2023-01-26T11:26:56.3883317", "Xp": 75, "XpModifier": {}, "PlayableSince": null, @@ -366,6 +327,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -376,7 +338,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -396,19 +359,12 @@ "Witnesses": [], "LastAccidentTime": 0 }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "CheckNoticedKill": { - "DeadBodySeen": { - "Transition": "Failure" - }, - "Unnoticed_Kill": { - "Transition": "Start" - } + "DeadBodySeen": { "Transition": "Failure" }, + "Unnoticed_Kill": { "Transition": "Start" } }, "Start": { "AccidentBodyFound": { @@ -451,9 +407,7 @@ "ContractEnd": { "Condition": { "$and": [ - { - "$eq": [true, "$.RecordingDestroyed"] - }, + { "$eq": [true, "$.RecordingDestroyed"] }, { "$all": { "in": "$.Witnesses", @@ -474,16 +428,12 @@ }, "Transition": "Success" }, - "CrowdNPC_Died": { - "Transition": "Failure" - }, + "CrowdNPC_Died": { "Transition": "Failure" }, "Witnesses": { "Condition": { "$any": { "in": "$Value", - "?": { - "$pushunique": ["Witnesses", "$.#"] - } + "?": { "$pushunique": ["Witnesses", "$.#"] } } } }, @@ -491,9 +441,7 @@ "Condition": { "$any": { "in": "$Value", - "?": { - "$pushunique": ["Witnesses", "$.#"] - } + "?": { "$pushunique": ["Witnesses", "$.#"] } } } }, @@ -501,9 +449,7 @@ { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", false] - }, + { "$eq": ["$Value.IsTarget", false] }, { "$not": { "$eq": ["$Value.KillContext", 1] @@ -516,12 +462,8 @@ { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", false] - }, - { - "$eq": ["$Value.KillContext", 1] - } + { "$eq": ["$Value.IsTarget", false] }, + { "$eq": ["$Value.KillContext", 1] } ] }, "Actions": { @@ -545,12 +487,8 @@ { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", true] - }, - { - "$eq": ["$Value.Accident", false] - } + { "$eq": ["$Value.IsTarget", true] }, + { "$eq": ["$Value.Accident", false] } ] }, "Transition": "CheckNoticedKill" @@ -571,12 +509,8 @@ }, "Condition": { "$or": [ - { - "$eq": ["$Value.event", "erased"] - }, - { - "$eq": ["$Value.event", "destroyed"] - } + { "$eq": ["$Value.event", "erased"] }, + { "$eq": ["$Value.event", "destroyed"] } ] } } @@ -586,7 +520,7 @@ }, "Tags": ["actionreward", "intermediate", "mission"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.8010273", + "LastModified": "2023-01-26T11:26:56.5845169", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -597,13 +531,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -616,25 +552,16 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.KillClass", "melee"] - }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.KillClass", "melee"] }, + { "$eq": ["$Value.IsTarget", true] } ] }, "Actions": { @@ -650,7 +577,7 @@ }, "Tags": ["actionreward", "low", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.585956", + "LastModified": "2023-01-26T11:26:56.4747551", "Xp": 10, "XpModifier": {}, "PlayableSince": null, @@ -661,13 +588,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -680,22 +609,15 @@ "ImageName": null, "Definition": { "Context": {}, - "Repeatable": { - "Base": 10, - "Delta": 10 - }, + "Repeatable": { "Base": 10, "Delta": 10 }, "Scope": "profile", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.KillClass", "melee"] - }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.KillClass", "melee"] }, + { "$eq": ["$Value.IsTarget", true] } ] }, "Transition": "Success" @@ -705,7 +627,7 @@ }, "Tags": ["story", "stats", "hideinhud"], "Drops": [], - "LastModified": "2021-01-06T23:00:29.0970658", + "LastModified": "2023-01-26T11:26:56.7388704", "Xp": 0, "XpModifier": {}, "PlayableSince": null, @@ -726,7 +648,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -739,20 +662,13 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "Kill": { - "Condition": { - "$eq": ["$Value.IsTarget", true] - }, + "Condition": { "$eq": ["$Value.IsTarget", true] }, "Actions": { "$pushunique": [ "_SessionRewards", @@ -766,7 +682,7 @@ }, "Tags": ["actionreward", "mediumxp", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.4297565", + "LastModified": "2023-01-26T11:26:56.4106817", "Xp": 50, "XpModifier": {}, "PlayableSince": null, @@ -777,13 +693,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -796,13 +714,8 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -817,7 +730,7 @@ }, "Tags": ["actionreward", "intermediate-", "mission"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.7232384", + "LastModified": "2023-01-26T11:26:56.5481442", "Xp": 20, "XpModifier": {}, "PlayableSince": null, @@ -828,13 +741,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -847,13 +762,8 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "DisguiseEquipped": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "DisguiseEquipped": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -871,7 +781,7 @@ }, "Tags": ["actionreward", "intermediate", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.7165215", + "LastModified": "2023-01-26T11:26:56.5452245", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -882,6 +792,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -894,7 +805,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -908,27 +820,18 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { - "PollResult_HeadShot": { - "Transition": "Active" - } + "PollResult_HeadShot": { "Transition": "Active" } }, "Active": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", true] - }, - { - "$eq": ["$Value.IsHeadshot", true] - } + { "$eq": ["$Value.IsTarget", true] }, + { "$eq": ["$Value.IsHeadshot", true] } ] }, "Transition": "Success" @@ -938,7 +841,7 @@ }, "Tags": ["actionreward", "high", "crowdchoice"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.9503765", + "LastModified": "2023-01-26T11:26:56.6577538", "Xp": 100, "XpModifier": {}, "PlayableSince": null, @@ -949,6 +852,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -961,9 +865,8 @@ "Locations": null, "GameModes": null }, - "CrowdChoice": { - "Tag": "HeadShot" - } + "CrowdChoice": { "Tag": "HeadShot" }, + "OrderIndex": 10000 } }, { @@ -976,25 +879,16 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", true] - }, - { - "$eq": ["$Value.Accident", true] - } + { "$eq": ["$Value.IsTarget", true] }, + { "$eq": ["$Value.Accident", true] } ] }, "Actions": { @@ -1010,7 +904,7 @@ }, "Tags": ["actionreward", "mediumxp", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.4791482", + "LastModified": "2023-01-26T11:26:56.437435", "Xp": 50, "XpModifier": {}, "PlayableSince": null, @@ -1021,13 +915,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1040,25 +936,16 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.KillClass", "poison"] - }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.KillClass", "poison"] }, + { "$eq": ["$Value.IsTarget", true] } ] }, "Actions": { @@ -1074,7 +961,7 @@ }, "Tags": ["actionreward", "intermediate", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.5640731", + "LastModified": "2023-01-26T11:26:56.4652644", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -1085,13 +972,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1105,24 +994,17 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { - "PollResult_FibreWire": { - "Transition": "Active" - } + "PollResult_FibreWire": { "Transition": "Active" } }, "Active": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", true] - }, + { "$eq": ["$Value.IsTarget", true] }, { "$eq": [ "$Value.KillItemCategory", @@ -1138,7 +1020,7 @@ }, "Tags": ["actionreward", "high", "crowdchoice"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.9566263", + "LastModified": "2023-01-26T11:26:56.6607496", "Xp": 100, "XpModifier": {}, "PlayableSince": null, @@ -1149,6 +1031,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -1161,9 +1044,8 @@ "Locations": null, "GameModes": null }, - "CrowdChoice": { - "Tag": "FibreWire" - } + "CrowdChoice": { "Tag": "FibreWire" }, + "OrderIndex": 10000 } }, { @@ -1177,10 +1059,7 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -1197,7 +1076,7 @@ }, "Tags": ["actionreward", "intermediate", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.3691806", + "LastModified": "2023-01-26T11:26:56.3772736", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -1208,6 +1087,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -1220,7 +1100,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1234,25 +1115,16 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.ActorType", 1] - }, - { - "$eq": ["$Value.IsHeadshot", true] - }, - { - "$eq": ["$Value.IsTarget", false] - } + { "$eq": ["$Value.ActorType", 1] }, + { "$eq": ["$Value.IsHeadshot", true] }, + { "$eq": ["$Value.IsTarget", false] } ] }, "Transition": "Success" @@ -1262,7 +1134,7 @@ }, "Tags": ["actionreward", "very-low", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.6511201", + "LastModified": "2023-01-26T11:26:56.5123901", "Xp": 5, "XpModifier": {}, "PlayableSince": null, @@ -1273,6 +1145,7 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign", @@ -1282,7 +1155,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1295,10 +1169,7 @@ "ImageName": null, "Definition": { "Context": {}, - "Repeatable": { - "Base": 10, - "Delta": 10 - }, + "Repeatable": { "Base": 10, "Delta": 10 }, "Scope": "profile", "States": { "Start": { @@ -1308,9 +1179,7 @@ { "$eq": ["$Value.KillClass", "ballistic"] }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.IsTarget", true] } ] }, "Transition": "Success" @@ -1320,7 +1189,7 @@ }, "Tags": ["story", "stats", "hideinhud"], "Drops": [], - "LastModified": "2021-01-06T23:00:29.1031221", + "LastModified": "2023-01-26T11:26:56.7418674", "Xp": 0, "XpModifier": {}, "PlayableSince": null, @@ -1341,7 +1210,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1354,20 +1224,13 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "Kill": { - "Condition": { - "$eq": ["$Value.IsTarget", false] - }, + "Condition": { "$eq": ["$Value.IsTarget", false] }, "Transition": "Failure" }, "ContractEnd": { @@ -1381,7 +1244,7 @@ }, "Tags": ["actionreward", "intermediate", "mission"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.7524133", + "LastModified": "2023-01-26T11:26:56.5635427", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -1392,13 +1255,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1411,13 +1276,8 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -1427,9 +1287,7 @@ { "$eq": ["$Value.KillClass", "ballistic"] }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.IsTarget", true] } ] }, "Actions": { @@ -1445,7 +1303,7 @@ }, "Tags": ["actionreward", "low", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.6036988", + "LastModified": "2023-01-26T11:26:56.4852221", "Xp": 10, "XpModifier": {}, "PlayableSince": null, @@ -1456,13 +1314,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1475,25 +1335,16 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", true] - }, - { - "$eq": ["$Value.IsHeadshot", true] - } + { "$eq": ["$Value.IsTarget", true] }, + { "$eq": ["$Value.IsHeadshot", true] } ] }, "Actions": { @@ -1509,7 +1360,7 @@ }, "Tags": ["actionreward", "low", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.5029428", + "LastModified": "2023-01-26T11:26:56.4503988", "Xp": 10, "XpModifier": {}, "PlayableSince": null, @@ -1520,13 +1371,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1539,13 +1392,8 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -1555,9 +1403,7 @@ { "$eq": ["$Value.KillClass", "explosion"] }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.IsTarget", true] } ] }, "Actions": { @@ -1573,7 +1419,7 @@ }, "Tags": ["actionreward", "low", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.6270265", + "LastModified": "2023-01-26T11:26:56.4991132", "Xp": 10, "XpModifier": {}, "PlayableSince": null, @@ -1584,13 +1430,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1603,34 +1451,21 @@ "ImageName": null, "Definition": { "Context": {}, - "Repeatable": { - "Base": 10, - "Delta": 10 - }, + "Repeatable": { "Base": 10, "Delta": 10 }, "Scope": "profile", "States": { "Checkbodyfound": { "$timer": { - "Condition": { - "$after": 5 - }, + "Condition": { "$after": 5 }, "Transition": "Start" }, - "BodyFound": { - "Transition": "Failure" - }, - "Spotted": { - "Transition": "Failure" - }, - "Unnoticed_Kill": { - "Transition": "Success" - } + "BodyFound": { "Transition": "Failure" }, + "Spotted": { "Transition": "Failure" }, + "Unnoticed_Kill": { "Transition": "Success" } }, "Start": { "Kill": { - "Condition": { - "$eq": ["$Value.IsTarget", true] - }, + "Condition": { "$eq": ["$Value.IsTarget", true] }, "Transition": "Checkbodyfound" } } @@ -1638,7 +1473,7 @@ }, "Tags": ["story", "stats"], "Drops": [], - "LastModified": "2021-01-06T23:00:29.0852276", + "LastModified": "2023-01-26T11:26:56.7322238", "Xp": 0, "XpModifier": {}, "PlayableSince": null, @@ -1659,7 +1494,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1672,22 +1508,15 @@ "ImageName": null, "Definition": { "Context": {}, - "Repeatable": { - "Base": 10, - "Delta": 10 - }, + "Repeatable": { "Base": 10, "Delta": 10 }, "Scope": "profile", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.KillClass", "poison"] - }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.KillClass", "poison"] }, + { "$eq": ["$Value.IsTarget", true] } ] }, "Transition": "Success" @@ -1697,7 +1526,7 @@ }, "Tags": ["story", "stats", "hideinhud"], "Drops": [], - "LastModified": "2021-01-06T23:00:29.0909942", + "LastModified": "2023-01-26T11:26:56.7356016", "Xp": 0, "XpModifier": {}, "PlayableSince": null, @@ -1718,7 +1547,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1732,39 +1562,24 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "CheckUnnoticed": { "$timer": { - "Condition": { - "$after": 5 - }, + "Condition": { "$after": 5 }, "Transition": "Start" }, - "BodyFound": { - "Transition": "Failure" - }, - "Spotted": { - "Transition": "Failure" - }, - "Unnoticed_Pacified": { - "Transition": "Success" - } + "BodyFound": { "Transition": "Failure" }, + "Spotted": { "Transition": "Failure" }, + "Unnoticed_Pacified": { "Transition": "Success" } }, - "Start": { - "Pacify": { - "Transition": "CheckUnnoticed" - } - } + "Start": { "Pacify": { "Transition": "CheckUnnoticed" } } } }, "Tags": ["actionreward", "intermediate", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.3824438", + "LastModified": "2023-01-26T11:26:56.3836655", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -1775,6 +1590,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -1786,7 +1602,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1799,22 +1616,15 @@ "ImageName": null, "Definition": { "Context": {}, - "Repeatable": { - "Base": 10, - "Delta": 10 - }, + "Repeatable": { "Base": 10, "Delta": 10 }, "Scope": "profile", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.Accident", true] - }, - { - "$eq": ["$Value.IsTarget", true] - } + { "$eq": ["$Value.Accident", true] }, + { "$eq": ["$Value.IsTarget", true] } ] }, "Transition": "Success" @@ -1824,7 +1634,7 @@ }, "Tags": ["story", "stats"], "Drops": [], - "LastModified": "2021-01-06T23:00:29.0743994", + "LastModified": "2023-01-26T11:26:56.7266038", "Xp": 0, "XpModifier": {}, "PlayableSince": null, @@ -1845,7 +1655,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1858,22 +1669,15 @@ "ImageName": null, "Definition": { "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { - "Start": { - "BodyHidden": { - "Transition": "Success" - } - } + "Start": { "BodyHidden": { "Transition": "Success" } } } }, "Tags": ["actionreward", "intermediate", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.3943428", + "LastModified": "2023-01-26T11:26:56.3918938", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -1884,6 +1688,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -1896,7 +1701,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1910,16 +1716,11 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { - "FoxTargetIdEvent": { - "Transition": "Success" - }, + "FoxTargetIdEvent": { "Transition": "Success" }, "Level_Setup_Events": { "Condition": { "$eq": [ @@ -1934,7 +1735,7 @@ }, "Tags": ["actionreward", "intermediate", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.9256854", + "LastModified": "2023-01-26T11:26:56.645071", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -1945,6 +1746,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -1957,7 +1759,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -1970,13 +1773,8 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -1996,9 +1794,7 @@ ] }, "CheckDisguise": { - "Disguise": { - "Transition": "Failure" - }, + "Disguise": { "Transition": "Failure" }, "ContractEnd": { "Actions": { "$pushunique": ["_SessionRewards", true] @@ -2010,7 +1806,7 @@ }, "Tags": ["actionreward", "intermediate", "mission"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.8236103", + "LastModified": "2023-01-26T11:26:56.597844", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -2021,13 +1817,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -2045,19 +1843,14 @@ "RecordingDestroyed": true, "LastAccidentTime": 0 }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "ContractEnd": { "Condition": { "$and": [ - { - "$eq": [true, "$.RecordingDestroyed"] - }, + { "$eq": [true, "$.RecordingDestroyed"] }, { "$all": { "in": "$.Witnesses", @@ -2082,9 +1875,7 @@ "Condition": { "$any": { "in": "$Value", - "?": { - "$pushunique": ["Witnesses", "$.#"] - } + "?": { "$pushunique": ["Witnesses", "$.#"] } } } }, @@ -2092,9 +1883,7 @@ "Condition": { "$any": { "in": "$Value", - "?": { - "$pushunique": ["Witnesses", "$.#"] - } + "?": { "$pushunique": ["Witnesses", "$.#"] } } } }, @@ -2102,9 +1891,7 @@ { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", false] - }, + { "$eq": ["$Value.IsTarget", false] }, { "$not": { "$eq": ["$Value.KillContext", 1] @@ -2117,12 +1904,8 @@ { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", false] - }, - { - "$eq": ["$Value.KillContext", 1] - } + { "$eq": ["$Value.IsTarget", false] }, + { "$eq": ["$Value.KillContext", 1] } ] }, "Actions": { @@ -2144,9 +1927,7 @@ } } ], - "CrowdNPC_Died": { - "Transition": "Failure" - }, + "CrowdNPC_Died": { "Transition": "Failure" }, "MurderedBodySeen": [ { "Condition": { @@ -2196,12 +1977,8 @@ }, "Condition": { "$or": [ - { - "$eq": ["$Value.event", "erased"] - }, - { - "$eq": ["$Value.event", "destroyed"] - } + { "$eq": ["$Value.event", "erased"] }, + { "$eq": ["$Value.event", "destroyed"] } ] } } @@ -2209,9 +1986,7 @@ } }, "ContextListeners": { - "RecordingDestroyed": { - "type": "toggle" - }, + "RecordingDestroyed": { "type": "toggle" }, "KilledTargets": { "comparand": "$.Witnesses", "type": "matcharrays" @@ -2220,258 +1995,11 @@ }, "Tags": ["story", "stats", "hideinhud", "sa-feedback"], "Drops": [], - "LastModified": "2021-01-06T23:00:29.1322786", + "LastModified": "2023-01-26T11:26:56.7520587", "Xp": 0, "XpModifier": {}, "PlayableSince": null, "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": [ - "mission", - "flashback", - "elusive", - "escalation", - "usercreated", - "tutorial", - "creation", - "orbis", - "featured", - "campaign", - "vsrace" - ], - "Locations": null, - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "b3be835a-0fc4-4caa-b5be-dfd9a621ec12", - "GroupId": "00000000-0000-0000-0000-000000000000", - "Name": "UI_CHALLENGES_GLOBAL_KEY_OBTAINED_NAME", - "Type": "Hit", - "Description": "", - "ImageName": null, - "Definition": { - "ResetOnCycleCompletion": false, - "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, - "Scope": "session", - "States": { - "Start": { - "ItemPickedUp": { - "Condition": { - "$eq": ["$Value.ActionRewardType", "AR_Key"] - }, - "Transition": "Success" - } - } - } - }, - "Tags": ["actionreward", "low", "infiltration"], - "Drops": [], - "LastModified": "2021-01-06T23:00:28.4118573", - "Xp": 10, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": [ - "mission", - "flashback", - "elusive", - "escalation", - "usercreated", - "tutorial", - "creation", - "orbis", - "featured", - "campaign" - ], - "Locations": null, - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "b55bb4da-bb14-46a4-9975-af79e5a3e088", - "GroupId": "00000000-0000-0000-0000-000000000000", - "Name": "UI_CHALLENGES_GLOBAL_HEADSHOT_NAME", - "Type": "Statistic", - "Description": "UI_CHALLENGES_GLOBAL_HEADSHOT_DESC", - "ImageName": null, - "Definition": { - "Context": {}, - "Repeatable": { - "Base": 10, - "Delta": 10 - }, - "Scope": "profile", - "States": { - "Start": { - "Kill": { - "Condition": { - "$and": [ - { - "$eq": ["$Value.IsHeadshot", true] - }, - { - "$eq": ["$Value.IsTarget", true] - } - ] - }, - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "stats"], - "Drops": [], - "LastModified": "2021-01-06T23:00:29.07981", - "Xp": 0, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": [ - "mission", - "flashback", - "elusive", - "escalation", - "usercreated", - "tutorial", - "creation", - "orbis", - "featured", - "campaign" - ], - "Locations": null, - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "b6cc96db-9cd4-4d79-93a9-da33e887683b", - "GroupId": "00000000-0000-0000-0000-000000000000", - "Name": "UI_CHALLENGES_GLOBAL_CROWDCHOICE_POISON_NAME", - "Type": "Hit", - "Description": "", - "ImageName": null, - "Definition": { - "ResetOnCycleCompletion": false, - "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, - "Scope": "session", - "States": { - "Start": { - "PollResult_Poison": { - "Transition": "Active" - } - }, - "Active": { - "Kill": { - "Condition": { - "$and": [ - { - "$eq": ["$Value.IsTarget", true] - }, - { - "$eq": ["$Value.KillClass", "poison"] - } - ] - }, - "Transition": "Success" - } - } - } - }, - "Tags": ["actionreward", "high", "crowdchoice"], - "Drops": [], - "LastModified": "2021-01-06T23:00:28.9745505", - "Xp": 100, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": [ - "mission", - "flashback", - "elusive", - "escalation", - "usercreated", - "tutorial", - "creation", - "orbis", - "featured", - "campaign", - "placeholder" - ], - "Locations": null, - "GameModes": null - }, - "CrowdChoice": { - "Tag": "Poison" - } - } - }, - { - "Challenge": { - "Id": "baa81f5b-c1de-44e2-8217-8d96cf2f5f17", - "GroupId": "00000000-0000-0000-0000-000000000000", - "Name": "UI_CHALLENGES_GLOBAL_RECORDING_DELETED_NAME", - "Type": "Hit", - "Description": "", - "ImageName": null, - "Definition": { - "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, - "Scope": "session", - "States": { - "Start": { - "SecuritySystemRecorder": { - "Condition": { - "$or": [ - { - "$eq": ["$Value.event", "erased"] - }, - { - "$eq": ["$Value.event", "destroyed"] - } - ] - }, - "Actions": { - "$pushunique": ["_SessionRewards", true] - }, - "Transition": "Success" - } - } - } - }, - "Tags": ["actionreward", "mediumxp", "infiltration"], - "Drops": [], - "LastModified": "2021-01-06T23:00:28.3764926", - "Xp": 50, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, "InclusionData": { "ContractIds": null, "ContractTypes": [ @@ -2486,11 +2014,237 @@ "featured", "campaign", "vsrace", + "arcade" + ], + "Locations": null, + "GameModes": null + }, + "OrderIndex": 10000 + } + }, + { + "Challenge": { + "Id": "b3be835a-0fc4-4caa-b5be-dfd9a621ec12", + "GroupId": "00000000-0000-0000-0000-000000000000", + "Name": "UI_CHALLENGES_GLOBAL_KEY_OBTAINED_NAME", + "Type": "Hit", + "Description": "", + "ImageName": null, + "Definition": { + "ResetOnCycleCompletion": false, + "Context": {}, + "Repeatable": { "Base": 1, "Delta": 0 }, + "Scope": "session", + "States": { + "Start": { + "ItemPickedUp": { + "Condition": { + "$eq": ["$Value.ActionRewardType", "AR_Key"] + }, + "Transition": "Success" + } + } + } + }, + "Tags": ["actionreward", "low", "infiltration"], + "Drops": [], + "LastModified": "2023-01-26T11:26:56.4012596", + "Xp": 10, + "XpModifier": {}, + "PlayableSince": null, + "PlayableUntil": null, + "InclusionData": { + "ContractIds": null, + "ContractTypes": [ + "mission", + "flashback", + "elusive", + "arcade", + "escalation", + "usercreated", + "tutorial", + "creation", + "orbis", + "featured", + "campaign" + ], + "Locations": null, + "GameModes": null + }, + "OrderIndex": 10000 + } + }, + { + "Challenge": { + "Id": "b55bb4da-bb14-46a4-9975-af79e5a3e088", + "GroupId": "00000000-0000-0000-0000-000000000000", + "Name": "UI_CHALLENGES_GLOBAL_HEADSHOT_NAME", + "Type": "Statistic", + "Description": "UI_CHALLENGES_GLOBAL_HEADSHOT_DESC", + "ImageName": null, + "Definition": { + "Context": {}, + "Repeatable": { "Base": 10, "Delta": 10 }, + "Scope": "profile", + "States": { + "Start": { + "Kill": { + "Condition": { + "$and": [ + { "$eq": ["$Value.IsHeadshot", true] }, + { "$eq": ["$Value.IsTarget", true] } + ] + }, + "Transition": "Success" + } + } + } + }, + "Tags": ["story", "stats"], + "Drops": [], + "LastModified": "2023-01-26T11:26:56.7293855", + "Xp": 0, + "XpModifier": {}, + "PlayableSince": null, + "PlayableUntil": null, + "InclusionData": { + "ContractIds": null, + "ContractTypes": [ + "mission", + "flashback", + "elusive", + "escalation", + "usercreated", + "tutorial", + "creation", + "orbis", + "featured", + "campaign" + ], + "Locations": null, + "GameModes": null + }, + "OrderIndex": 10000 + } + }, + { + "Challenge": { + "Id": "b6cc96db-9cd4-4d79-93a9-da33e887683b", + "GroupId": "00000000-0000-0000-0000-000000000000", + "Name": "UI_CHALLENGES_GLOBAL_CROWDCHOICE_POISON_NAME", + "Type": "Hit", + "Description": "", + "ImageName": null, + "Definition": { + "ResetOnCycleCompletion": false, + "Context": {}, + "Repeatable": { "Base": 1, "Delta": 0 }, + "Scope": "session", + "States": { + "Start": { + "PollResult_Poison": { "Transition": "Active" } + }, + "Active": { + "Kill": { + "Condition": { + "$and": [ + { "$eq": ["$Value.IsTarget", true] }, + { "$eq": ["$Value.KillClass", "poison"] } + ] + }, + "Transition": "Success" + } + } + } + }, + "Tags": ["actionreward", "high", "crowdchoice"], + "Drops": [], + "LastModified": "2023-01-26T11:26:56.6715121", + "Xp": 100, + "XpModifier": {}, + "PlayableSince": null, + "PlayableUntil": null, + "InclusionData": { + "ContractIds": null, + "ContractTypes": [ + "mission", + "flashback", + "elusive", + "arcade", + "escalation", + "usercreated", + "tutorial", + "creation", + "orbis", + "featured", + "campaign", "placeholder" ], "Locations": null, "GameModes": null - } + }, + "CrowdChoice": { "Tag": "Poison" }, + "OrderIndex": 10000 + } + }, + { + "Challenge": { + "Id": "baa81f5b-c1de-44e2-8217-8d96cf2f5f17", + "GroupId": "00000000-0000-0000-0000-000000000000", + "Name": "UI_CHALLENGES_GLOBAL_RECORDING_DELETED_NAME", + "Type": "Hit", + "Description": "", + "ImageName": null, + "Definition": { + "ResetOnCycleCompletion": false, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, + "Scope": "session", + "States": { + "Start": { + "SecuritySystemRecorder": { + "Condition": { + "$or": [ + { "$eq": ["$Value.event", "erased"] }, + { "$eq": ["$Value.event", "destroyed"] } + ] + }, + "Actions": { + "$pushunique": ["_SessionRewards", true] + }, + "Transition": "Success" + } + } + } + }, + "Tags": ["actionreward", "mediumxp", "infiltration"], + "Drops": [], + "LastModified": "2023-01-26T11:26:56.3806504", + "Xp": 50, + "XpModifier": {}, + "PlayableSince": null, + "PlayableUntil": null, + "InclusionData": { + "ContractIds": null, + "ContractTypes": [ + "mission", + "flashback", + "elusive", + "arcade", + "escalation", + "usercreated", + "tutorial", + "creation", + "orbis", + "featured", + "campaign", + "vsrace", + "placeholder" + ], + "Locations": null, + "GameModes": null + }, + "OrderIndex": 10000 } }, { @@ -2504,25 +2258,16 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.ActorType", 1] - }, - { - "$eq": ["$Value.Accident", true] - }, - { - "$eq": ["$Value.IsTarget", false] - } + { "$eq": ["$Value.ActorType", 1] }, + { "$eq": ["$Value.Accident", true] }, + { "$eq": ["$Value.IsTarget", false] } ] }, "Transition": "Success" @@ -2532,7 +2277,7 @@ }, "Tags": ["actionreward", "very-low", "elimination"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.6696904", + "LastModified": "2023-01-26T11:26:56.522326", "Xp": 5, "XpModifier": {}, "PlayableSince": null, @@ -2543,6 +2288,7 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign", @@ -2552,7 +2298,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -2565,13 +2312,8 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "_SessionRewards": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "_SessionRewards": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -2589,7 +2331,7 @@ }, "Tags": ["actionreward", "low", "mission"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.6876217", + "LastModified": "2023-01-26T11:26:56.532715", "Xp": 10, "XpModifier": {}, "PlayableSince": null, @@ -2600,13 +2342,15 @@ "mission", "flashback", "elusive", + "arcade", "tutorial", "orbis", "campaign" ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -2620,10 +2364,7 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -2638,7 +2379,7 @@ }, "Tags": ["actionreward", "low", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.4057529", + "LastModified": "2023-01-26T11:26:56.3980977", "Xp": 10, "XpModifier": {}, "PlayableSince": null, @@ -2649,6 +2390,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -2659,7 +2401,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -2672,13 +2415,8 @@ "ImageName": null, "Definition": { "ResetOnCycleCompletion": false, - "Context": { - "FriskZone": [] - }, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Context": { "FriskZone": [] }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { @@ -2690,15 +2428,13 @@ } }, "WaitForSuccess": { - "FriskedSuccess": { - "Transition": "Success" - } + "FriskedSuccess": { "Transition": "Success" } } } }, "Tags": ["actionreward", "low", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.3629123", + "LastModified": "2023-01-26T11:26:56.3741123", "Xp": 10, "XpModifier": {}, "PlayableSince": null, @@ -2709,6 +2445,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -2721,7 +2458,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -2735,22 +2473,15 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { - "Start": { - "Door_Unlocked": { - "Transition": "Success" - } - } + "Start": { "Door_Unlocked": { "Transition": "Success" } } } }, "Tags": ["actionreward", "intermediate", "infiltration"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.4237377", + "LastModified": "2023-01-26T11:26:56.4076936", "Xp": 25, "XpModifier": {}, "PlayableSince": null, @@ -2761,6 +2492,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -2772,7 +2504,8 @@ ], "Locations": null, "GameModes": null - } + }, + "OrderIndex": 10000 } }, { @@ -2785,22 +2518,15 @@ "ImageName": null, "Definition": { "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "profile", "States": { "Start": { - "CrowdNPC_Died": { - "Transition": "Success" - }, + "CrowdNPC_Died": { "Transition": "Success" }, "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", false] - }, + { "$eq": ["$Value.IsTarget", false] }, { "$not": { "$eq": ["$Value.KillContext", 1] @@ -2853,27 +2579,18 @@ "Definition": { "ResetOnCycleCompletion": false, "Context": {}, - "Repeatable": { - "Base": 1, - "Delta": 0 - }, + "Repeatable": { "Base": 1, "Delta": 0 }, "Scope": "session", "States": { "Start": { - "PollResult_Accident": { - "Transition": "Active" - } + "PollResult_Accident": { "Transition": "Active" } }, "Active": { "Kill": { "Condition": { "$and": [ - { - "$eq": ["$Value.IsTarget", true] - }, - { - "$eq": ["$Value.Accident", true] - } + { "$eq": ["$Value.IsTarget", true] }, + { "$eq": ["$Value.Accident", true] } ] }, "Transition": "Success" @@ -2883,7 +2600,7 @@ }, "Tags": ["actionreward", "high", "crowdchoice"], "Drops": [], - "LastModified": "2021-01-06T23:00:28.9624484", + "LastModified": "2023-01-26T11:26:56.6638147", "Xp": 100, "XpModifier": {}, "PlayableSince": null, @@ -2894,6 +2611,7 @@ "mission", "flashback", "elusive", + "arcade", "escalation", "usercreated", "tutorial", @@ -2906,593 +2624,8 @@ "Locations": null, "GameModes": null }, - "CrowdChoice": { - "Tag": "Accident" - } - } - }, - { - "Challenge": { - "Id": "c64de151-22e3-4801-8954-51445525a185", - "GroupId": "ffbb1ec4-3d33-4cfc-a750-4830910bf8dc", - "Name": "UI_CHALLENGES_EDGY_LADDER_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_EDGY_LADDER_A_DESC", - "ImageName": "images/challenges/Edgy/Edgy_persistentladder_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Edgy_Ladder_A_Down": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:38.0500356", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_EDGY_FOX"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "b1df314f-4c76-48c6-931b-bfdc171d0c8d", - "GroupId": "ffbb1ec4-3d33-4cfc-a750-4830910bf8dc", - "Name": "UI_CHALLENGES_EDGY_DOOR_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_EDGY_DOOR_A_DESC", - "ImageName": "images/challenges/Edgy/Edgy_persistentdoor_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Edgy_Door_A_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:38.0551326", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_EDGY_FOX"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "2f68f1d4-e7d7-4f10-8f38-df014a900b12", - "GroupId": "ffbb1ec4-3d33-4cfc-a750-4830910bf8dc", - "Name": "UI_CHALLENGES_EDGY_DOOR_B_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_EDGY_DOOR_B_DESC", - "ImageName": "images/challenges/Edgy/Edgy_persistentdoor_b.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Edgy_Door_B_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:38.067445", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_EDGY_FOX"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "fe368941-ba7a-419d-b870-abbf45fdf899", - "GroupId": "34d53a35-4612-43e9-84b3-e265349918c1", - "Name": "UI_CHALLENGES_ELEGANT_DOOR_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_ELEGANT_DOOR_A_DESC", - "ImageName": "images/challenges/Elegant/Elegant_Door_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Elegant_Door_A_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:36.413789", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_ELEGANT_LLAMA"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "4a75fb3f-b8a5-4f0e-b64c-044f48246c99", - "GroupId": "34d53a35-4612-43e9-84b3-e265349918c1", - "Name": "UI_CHALLENGES_ELEGANT_DOOR_C_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_ELEGANT_DOOR_C_DESC", - "ImageName": "images/challenges/Elegant/Elegant_Door_C.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Elegant_Door_C_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:36.5555553", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_ELEGANT_LLAMA"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "1131c30d-cf02-4444-aafa-694cea2ce124", - "GroupId": "34d53a35-4612-43e9-84b3-e265349918c1", - "Name": "UI_CHALLENGES_ELEGANT_DOOR_B_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_ELEGANT_DOOR_B_DESC", - "ImageName": "images/challenges/Elegant/Elegant_Door_B.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Elegant_Door_B_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:36.4180207", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_ELEGANT_LLAMA"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "fcddb4fe-1a44-414d-b769-bef1307ed05b", - "GroupId": "82331e45-36fa-4e15-a5e7-e4207e006f30", - "Name": "UI_CHALLENGES_TRAPPED_DOOR_B_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_TRAPPED_DOOR_B_DESC", - "ImageName": "images/challenges/Trapped/trapped_door_b.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Wolverine_Door_B_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:38.3643818", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_TRAPPED_WOLVERINE"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "18636f91-e1a9-4091-935c-adbf6cc3de55", - "GroupId": "82331e45-36fa-4e15-a5e7-e4207e006f30", - "Name": "UI_CHALLENGES_TRAPPED_DOOR_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_TRAPPED_DOOR_A_DESC", - "ImageName": "images/challenges/Trapped/trapped_door_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Wolverine_Door_A_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:38.359856", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_TRAPPED_WOLVERINE"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "04ef301f-a62d-47f6-8cb8-3b8697e0c06c", - "GroupId": "7e9a3b46-9642-466a-ad17-5356a4c42982", - "Name": "UI_CHALLENGES_RAT_DOOR_B_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_RAT_DOOR_B_DESC", - "ImageName": "images/challenges/Wet/Rat_door_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Rat_Door_A_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-02-23T13:33:48.3866227", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_WET_RAT"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "5f8cef5b-c631-4304-af90-46efa4ca7deb", - "GroupId": "7e9a3b46-9642-466a-ad17-5356a4c42982", - "Name": "UI_CHALLENGES_RAT_DOOR_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_RAT_DOOR_A_DESC", - "ImageName": "images/challenges/Wet/Rat_door_c.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Rat_Door_B_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-02-23T13:33:48.4011233", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_WET_RAT"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "aa4982db-045c-47d0-aabb-42b86a0714fb", - "GroupId": "7e9a3b46-9642-466a-ad17-5356a4c42982", - "Name": "UI_CHALLENGES_RAT_LADDER_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_RAT_LADDER_A_DESC", - "ImageName": "images/challenges/Wet/Rat_ladder_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Rat_Ladder_A_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:38.4390467", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_WET_RAT"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "23ff6a96-6540-4f24-9bd2-a62c3580ab8e", - "GroupId": "787f547d-67d8-4b53-b500-de81e244239a", - "Name": "UI_CHALLENGES_ANCESTRAL_LADDER_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_ANCESTRAL_LADDER_A_DESC", - "ImageName": "images/challenges/Ancestral/Bulldog_ladder_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Bulldog_Ladder_A_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:35.7147281", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": [ - "LOCATION_ANCESTRAL_BULLDOG", - "LOCATION_ANCESTRAL_SMOOTHSNAKE" - ], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "87d616bc-37d9-47ef-8efd-01b621e5a54c", - "GroupId": "787f547d-67d8-4b53-b500-de81e244239a", - "Name": "UI_CHALLENGES_ANCESTRAL_LADDER_B_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_ANCESTRAL_LADDER_B_DESC", - "ImageName": "images/challenges/Ancestral/Bulldog_ladder_b.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Bulldog_Ladder_B_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:35.8532662", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": [ - "LOCATION_ANCESTRAL_BULLDOG", - "LOCATION_ANCESTRAL_SMOOTHSNAKE" - ], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "319d9198-336b-4739-b218-7794f4433f06", - "GroupId": "d62d4b44-1ad3-4cbb-891c-5256e628a3e9", - "Name": "UI_CHALLENGES_GOLDEN_LADDER_C_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_GOLDEN_LADDER_C_DESC", - "ImageName": "images/challenges/Golden/Golden_ladder_c.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Gecko_Ladder_C_Down": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:36.5734366", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_GOLDEN_GECKO"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "4a3120cc-403c-41a7-807c-767f8cfb80af", - "GroupId": "d62d4b44-1ad3-4cbb-891c-5256e628a3e9", - "Name": "UI_CHALLENGES_GOLDEN_LADDER_B_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_GOLDEN_LADDER_B_DESC", - "ImageName": "images/challenges/Golden/Golden_ladder_b.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Gecko_Ladder_B_Down": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:36.5692687", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_GOLDEN_GECKO"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "6c537401-4577-4b5e-b1f9-a0b3cbb645f6", - "GroupId": "d62d4b44-1ad3-4cbb-891c-5256e628a3e9", - "Name": "UI_CHALLENGES_GOLDEN_DOOR_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_GOLDEN_DOOR_A_DESC", - "ImageName": "images/challenges/Golden/Golden_door_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Gecko_Door_A_Open": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:36.5777154", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_GOLDEN_GECKO"], - "GameModes": null - } - } - }, - { - "Challenge": { - "Id": "a6a5a962-8e92-42a7-bd44-824610d7399f", - "GroupId": "d62d4b44-1ad3-4cbb-891c-5256e628a3e9", - "Name": "UI_CHALLENGES_GOLDEN_LADDER_A_NAME", - "Type": "Hit", - "Description": "UI_CHALLENGES_GOLDEN_LADDER_A_DESC", - "ImageName": "images/challenges/Golden/Golden_ladder_a.jpg", - "Definition": { - "Context": {}, - "Scope": "session", - "States": { - "Start": { - "Gecko_Ladder_A_Down": { - "Transition": "Success" - } - } - } - }, - "Tags": ["story", "easy", "discovery", "shortcut"], - "Drops": [], - "LastModified": "2021-01-06T23:00:36.5650563", - "Xp": 1000, - "XpModifier": {}, - "PlayableSince": null, - "PlayableUntil": null, - "InclusionData": { - "ContractIds": null, - "ContractTypes": null, - "Locations": ["LOCATION_GOLDEN_GECKO"], - "GameModes": null - } + "CrowdChoice": { "Tag": "Accident" }, + "OrderIndex": 10000 } }, {