Merge branch 'master' into dev/patcher-core

This commit is contained in:
Reece Dunham 2024-05-01 12:32:01 -04:00 committed by GitHub
commit 4768eeaa44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
209 changed files with 25379 additions and 14592 deletions

View File

@ -8,4 +8,4 @@ webui/dist
*.plugin.js
*Plugin.js
packaging/livesplit-node-client/build
webstorm.config.js
tests/testData/scripts

20
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,20 @@
<!-- Thanks for contributing to Peacock! Here's a bit of a template to help make sure everything relevant is covered. -->
## Scope
<!-- List any relevant changes you have made here. Be sure to link any issues fixed, or that are relevant to these changes. -->
## Test Plan
<!-- List how you have verified these changes work as intended. -->
## Checklist
<!--
Just a few reminders to make sure everything is perfect. You can place an "X" in the boxes to tick them off.
If you have not completed one of the steps below, you can create the pull request as a draft, and then check off the items as you go.
When you have completed the checklist, press the "Ready for review" button.
-->
- [ ] I have run Prettier to reformat any changed files
- [ ] I have verified my changes work

87
.github/workflows/locale-mod.yml vendored Normal file
View File

@ -0,0 +1,87 @@
name: Update Localisation Mod
on:
push:
tags: ["v*"]
workflow_dispatch:
jobs:
update-mod:
name: Update Mod
runs-on: windows-latest
steps:
- name: Checkout Peacock
uses: actions/checkout@v4
with:
token: ${{ secrets.PEACOCKBOT_TOKEN }}
path: "./Peacock"
- name: Checkout Peacock Strings
uses: actions/checkout@v4
with:
token: ${{ secrets.PEACOCKBOT_TOKEN }}
repository: thepeacockproject/peacock-strings
path: "./PeacockStrings"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: "./Peacock/.nvmrc"
cache: "yarn"
cache-dependency-path: "./Peacock/yarn.lock"
- name: Install Packages
run: |
cd Peacock
yarn install --immutable
#- name: Download ResourceLib
# uses: robinraju/release-downloader@v1.7
# with:
# repository: "OrfeasZ/ZHMTools"
# latest: true
# fileName: "ResourceLib-win-x64.zip"
# out-file-path: "Peacock/resources"
- name: Download RPKG-CLI
id: rpkgcli
uses: robinraju/release-downloader@v1.10
with:
repository: "glacier-modding/RPKG-Tool"
latest: true
fileName: "rpkg_*-cli.zip"
out-file-path: "Peacock/resources"
- name: Download HMLanguageTools
id: hmlt
uses: robinraju/release-downloader@v1.10
with:
repository: "AnthonyFuller/TonyTools"
latest: true
fileName: "TonyTools.zip"
out-file-path: "Peacock/resources"
- name: Unzip dependencies
run: |
cd Peacock/resources
7z x ${{ fromJson(steps.rpkgcli.outputs.downloaded_files)[0] }}
7z x ${{ fromJson(steps.hmlt.outputs.downloaded_files)[0] }}
- name: Rebuild Locale Packages
run: |
cd Peacock
yarn rebuild-locale
- name: Copy peacockstrings.locr.json
run: |
copy ./Peacock/resources/peacockstrings.locr.json ./PeacockStrings/content/chunk0/peacockstrings.locr.json
- name: Push updated Peacock LOCR
uses: EndBug/add-and-commit@v9
with:
cwd: "./PeacockStrings"
add: content/chunk0/peacockstrings.locr.json
author_name: PeacockBot
author_email: admin@thepeacockproject.org
message: "enhancement: update strings"

View File

@ -2,7 +2,7 @@ name: Localisation
on:
push:
branches: ["v*"]
branches: ["master"]
paths: ["resources/locale.json", ".github/workflows/locale.yml"]
workflow_dispatch:
@ -18,13 +18,6 @@ jobs:
token: ${{ secrets.PEACOCKBOT_TOKEN }}
path: "./Peacock"
- name: Checkout Peacock Strings
uses: actions/checkout@v4
with:
token: ${{ secrets.PEACOCKBOT_TOKEN }}
repository: thepeacockproject/peacock-strings
path: "./PeacockStrings"
- name: Setup Node
uses: actions/setup-node@v4
with:
@ -47,7 +40,7 @@ jobs:
- name: Download RPKG-CLI
id: rpkgcli
uses: robinraju/release-downloader@v1.9
uses: robinraju/release-downloader@v1.10
with:
repository: "glacier-modding/RPKG-Tool"
latest: true
@ -56,7 +49,7 @@ jobs:
- name: Download HMLanguageTools
id: hmlt
uses: robinraju/release-downloader@v1.9
uses: robinraju/release-downloader@v1.10
with:
repository: "AnthonyFuller/TonyTools"
latest: true
@ -84,16 +77,3 @@ jobs:
author_name: PeacockBot
author_email: admin@thepeacockproject.org
message: "[skip ci] Update locale packages"
- name: Copy peacockstrings.locr.json
run: |
copy ./Peacock/resources/peacockstrings.locr.json ./PeacockStrings/content/chunk0/peacockstrings.locr.json
- name: Push updated Peacock LOCR
uses: EndBug/add-and-commit@v9
with:
cwd: "./PeacockStrings"
add: content/chunk0/peacockstrings.locr.json
author_name: PeacockBot
author_email: admin@thepeacockproject.org
message: "enhancement: update strings"

4
.gitignore vendored
View File

@ -20,6 +20,7 @@ resources/**/*.*
!resources/locale.json
!resources/dynamic_resources_*/*.meta
!resources/dynamic_resources_*.rpkg
!resources/dynamic_resources_*/*.JSON
!resources/rebuildLocale.cjs
components/contracts.json
@ -58,4 +59,5 @@ overrides
DEBUG_PROFILE.zip
packaging/add_itemsize/*
!packaging/add_itemsize/add_itemsize.js
!packaging/add_itemsize/add_itemsize.js
/.idea/AIAssistantCustomInstructionsStorage.xml

3
.idea/.gitignore vendored
View File

@ -3,3 +3,6 @@
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# GitHub Copilot persisted chat sessions
/copilot/chatSessions
/AIAssistantCustomInstructionsStorage.xml

View File

@ -31,8 +31,12 @@
<excludeFolder url="file://$MODULE_DIR$/resources/challenges" />
<excludeFolder url="file://$MODULE_DIR$/patcher/.idea/.idea.HitmanPatcher/.idea/shelf" />
<excludeFolder url="file://$MODULE_DIR$/images" />
<excludeFolder url="file://$MODULE_DIR$/logs" />
<excludePattern pattern="chunk*.js" />
<excludePattern pattern="components/contracts.json,*.tsbuildinfo,offlineassets.zip,chunk0.js.map" />
<excludePattern pattern="components/contracts.json" />
<excludePattern pattern="*.tsbuildinfo" />
<excludePattern pattern="offlineassets.zip" />
<excludePattern pattern="chunk0.js.map" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@ -1,12 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GithubSharedProjectSettings">
<option name="branchProtectionPatterns">
<list>
<option value="main" />
</list>
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>

2
.nvmrc
View File

@ -1 +1 @@
v20.10.0
v20.12.2

12
.vscode/launch.json vendored
View File

@ -4,6 +4,18 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Dev Tools (run-dev tools)",
"skipFiles": ["<node_internals>/**"],
"env": {
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
},
"runtimeExecutable": "yarn",
"runtimeArgs": ["run-dev", "tools"],
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",

View File

@ -1,10 +1,9 @@
{
"editor.codeActionsOnSave": {
"source.organizeImports": false
"source.organizeImports": "never"
},
"typescript.enablePromptUseWorkspaceTsdk": true,
"npm.packageManager": "yarn",
"eslint.packageManager": "yarn",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.format.enable": true,
"search.exclude": {
@ -14,5 +13,8 @@
"yarn.lock": true,
"**/.yarn": true
},
"omnisharp.useModernNet": false
"omnisharp.useModernNet": false,
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

File diff suppressed because one or more lines are too long

View File

@ -14,4 +14,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
spec: "https://mskelton.dev/yarn-outdated/v2"
yarnPath: .yarn/releases/yarn-4.1.0.cjs
yarnPath: .yarn/releases/yarn-4.1.1.cjs

View File

@ -93,8 +93,8 @@ legacyContractRouter.post(
req.body.id === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
? _legacyBull
: req.body.id === "ff9f46cf-00bd-4c12-b887-eac491c3a96d"
? _theLastYardbirdScpc
: controller.resolveContract(req.body.id)
? _theLastYardbirdScpc
: controller.resolveContract(req.body.id)
if (!contractData) {
log(

View File

@ -29,7 +29,7 @@ import { controller } from "../controller"
import { json as jsonMiddleware } from "body-parser"
import { uuidRegex } from "../utils"
import { compileRuntimeChallenge } from "../candle/challengeHelpers"
import { LegacyGetProgressionBody } from "../types/gameSchemas"
import { GetChallengeProgressionBody } from "../types/gameSchemas"
const legacyProfileRouter = Router()
@ -95,7 +95,7 @@ legacyProfileRouter.post(
"/ChallengesService/GetProgression",
jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<never, LegacyGetProgressionBody>, res) => {
(req: RequestWithJwt<never, GetChallengeProgressionBody>, res) => {
if (!Array.isArray(req.body.challengeids)) {
res.status(400).send("invalid body")
return

View File

@ -79,6 +79,14 @@ type GroupIndexedChallengeLists = {
[groupId: string]: RegistryChallenge[]
}
export type ChallengePack = {
Name: string
Description: string
GameVersions: GameVersion[]
Image: string
Icon: string
}
/**
* A base class providing challenge registration support.
*/
@ -99,7 +107,7 @@ export abstract class ChallengeRegistry {
/**
* @Key1 Game version.
* @Key2 The parent location Id.
* @Key3 The group Id.
* @Key3 The group's categoryId.
* @Value A `SavedChallengeGroup` object.
*/
protected groups: Record<
@ -145,6 +153,39 @@ export abstract class ChallengeRegistry {
protected constructor(protected readonly controller: Controller) {}
public challengePacks: Map<string, ChallengePack> = new Map([
[
"cheesecake-pack",
{
Name: "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
Description: "",
GameVersions: ["h3"],
Image: "images/challenges/categories/packcheesecake/tile.jpg",
Icon: "challenge_category_feats",
},
],
[
"argentum-pack",
{
Name: "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
Description: "",
GameVersions: ["h3"],
Image: "images/challenges/categories/packargentum/tile.jpg",
Icon: "challenge_category_feats",
},
],
[
"argon-pack",
{
Name: "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGON",
Description: "",
GameVersions: ["h3"],
Image: "images/challenges/categories/packargon/tile.jpg",
Icon: "challenge_category_feats",
},
],
])
registerChallenge(
challenge: RegistryChallenge,
groupId: string,
@ -192,6 +233,23 @@ export abstract class ChallengeRegistry {
return this.challenges[gameVersion].get(challengeId)
}
getChallengeIds(gameVersion: GameVersion): string[] {
return Array.from(this.challenges[gameVersion].keys())
}
removeChallenge(challengeId: string, gameVersion: GameVersion): boolean {
const challenge = this.challenges[gameVersion].get(challengeId)
if (!challenge) return false
return (
this.challenges[gameVersion].delete(challengeId) &&
this.groupContents[gameVersion]
.get(challenge.ParentLocationId)!
.get(challenge.inGroup!)!
.delete(challengeId)
)
}
/**
* This method retrieves all the unlockables associated with the challenges for a given game version.
* It iterates over all the challenges for the specified game version and for each challenge, it checks if there are any unlockables (Drops).
@ -286,6 +344,17 @@ export abstract class ChallengeRegistry {
return mainGroup
}
public getChallengesForGroup(
groupId: string,
gameVersion: GameVersion,
): GroupIndexedChallengeLists {
return {
[groupId]: Array.from(this.challenges[gameVersion].values()).filter(
(value) => value.inGroup === groupId,
),
}
}
public getGroupContentByIdLoc(
groupId: string,
location: string,
@ -513,7 +582,7 @@ export class ChallengeService extends ChallengeRegistry {
}
/**
* This is a helper function for @see getGroupedChallengeLists. It is not expected to be used elsewhere.
* This is a helper function for {@link getGroupedChallengeLists}. It is not expected to be used elsewhere.
*
* Filter all challenges in a parent location using a given filter, sort them into groups,
* and write them into the `challenges` array provided.
@ -587,37 +656,48 @@ export class ChallengeService extends ChallengeRegistry {
): GroupIndexedChallengeLists {
let challenges: [string, RegistryChallenge[]][] = []
this.getGroupedChallengesByLoc(
filter,
location,
challenges,
gameVersion,
)
if (filter.type === ChallengeFilterType.Contract && filter.isFeatured) {
// Challenge packs ignore the filter
if (this.challengePacks.has(location)) {
challenges.push([
location,
this.getChallengesForGroup(location, gameVersion)[location],
])
} else {
this.getGroupedChallengesByLoc(
filter,
"GLOBAL_FEATURED_CHALLENGES",
location,
challenges,
gameVersion,
)
}
this.getGroupedChallengesByLoc(
filter,
"GLOBAL_ARCADE_CHALLENGES",
challenges,
gameVersion,
)
if (
filter.type === ChallengeFilterType.Contract &&
filter.isFeatured
) {
this.getGroupedChallengesByLoc(
filter,
"GLOBAL_FEATURED_CHALLENGES",
challenges,
gameVersion,
)
}
// H2 & H1 have the escalation challenges in "feats"
if (gameVersion === "h3") {
this.getGroupedChallengesByLoc(
filter,
"GLOBAL_ESCALATION_CHALLENGES",
"GLOBAL_ARCADE_CHALLENGES",
challenges,
gameVersion,
)
// H2 & H1 have the escalation challenges in "feats"
if (gameVersion === "h3") {
this.getGroupedChallengesByLoc(
filter,
"GLOBAL_ESCALATION_CHALLENGES",
challenges,
gameVersion,
)
}
}
// remove empty groups
@ -989,6 +1069,7 @@ export class ChallengeService extends ChallengeRegistry {
forContract,
userId,
gameVersion,
false,
subLocation,
)
}
@ -999,18 +1080,20 @@ export class ChallengeService extends ChallengeRegistry {
gameVersion: GameVersion,
compiler: Compiler,
): CompiledChallengeTreeData[] {
return challenges.map((challengeData) => {
return compiler(
challengeData,
this.getPersistentChallengeProgression(
userId,
challengeData.Id,
return challenges
.map((challengeData) => {
return compiler(
challengeData,
this.getPersistentChallengeProgression(
userId,
challengeData.Id,
gameVersion,
),
gameVersion,
),
gameVersion,
userId,
)
})
userId,
)
})
.sort((a, b) => a.OrderIndex - b.OrderIndex)
}
private getChallengeDependencyData(
@ -1103,43 +1186,27 @@ export class ChallengeService extends ChallengeRegistry {
forLocation,
userId,
gameVersion,
locationData,
true,
locationData,
)
}
getChallengeDataForLocation(
locationId: string,
getChallengeDataForCategory(
categoryId: string | null,
location: Unlockable | undefined,
gameVersion: GameVersion,
userId: string,
): CompiledChallengeTreeCategory[] {
const locationsData = getVersionedConfig<PeacockLocationsData>(
"LocationsData",
gameVersion,
false,
)
const locationData = locationsData.children[locationId]
if (!locationData) {
log(
LogLevel.WARN,
`Failed to get location data in CSERV [${locationId}]`,
)
return []
}
const forLocation = this.getChallengesForLocation(
locationId,
gameVersion,
)
const challenges = location
? this.getChallengesForLocation(location.Id, gameVersion)
: this.getChallengesForGroup(categoryId!, gameVersion)
return this.reBatchIntoSwitchedData(
forLocation,
challenges,
userId,
gameVersion,
locationData,
true,
location,
)
}
@ -1148,33 +1215,35 @@ export class ChallengeService extends ChallengeRegistry {
* @param challengeLists The challenge lists to use.
* @param userId The id of the user.
* @param gameVersion The current game version.
* @param location A location as an `Unlockable`. Might be a parent location or a sublocation, depending on `isDestination`.
* @param isDestination Will also get escalation challenges if set to true.
* @param location A location as an `Unlockable`. If challengeLists contains all challenges of a challenge pack, this parameter should be "undefined". Otherwise, it might be a parent location or a sublocation, depending on `isDestination`.
* @returns An array of `CompiledChallengeTreeCategory` objects.
*/
private reBatchIntoSwitchedData(
challengeLists: GroupIndexedChallengeLists,
userId: string,
gameVersion: GameVersion,
location: Unlockable,
isDestination = false,
isDestination: boolean,
location?: Unlockable,
): CompiledChallengeTreeCategory[] {
const entries = Object.entries(challengeLists)
const compiler = isDestination
? this.compileRegistryDestinationChallengeData.bind(this)
: this.compileRegistryChallengeTreeData.bind(this)
const completion = generateCompletionData(
location?.Id,
userId,
gameVersion,
)
const completion = location
? generateCompletionData(location?.Id, userId, gameVersion)
: {}
return entries
const groups = entries
.map(([groupId, challenges], index) => {
if (challenges.length === 0) {
return undefined
}
const groupData = this.getGroupByIdLoc(
groupId,
location.Properties.ParentLocation ?? location.Id,
challenges[0].ParentLocationId,
gameVersion,
)
@ -1191,16 +1260,20 @@ export class ChallengeService extends ChallengeRegistry {
),
)
const lastGroup = this.getGroupByIdLoc(
Object.keys(challengeLists)[index - 1],
location.Properties.ParentLocation ?? location.Id,
gameVersion,
)
const nextGroup = this.getGroupByIdLoc(
Object.keys(challengeLists)[index + 1],
location.Properties.ParentLocation ?? location.Id,
gameVersion,
)
const lastGroup = location
? this.getGroupByIdLoc(
Object.keys(challengeLists)[index - 1],
location.Properties.ParentLocation ?? location.Id,
gameVersion,
)
: undefined
const nextGroup = location
? this.getGroupByIdLoc(
Object.keys(challengeLists)[index + 1],
location.Properties.ParentLocation ?? location.Id,
gameVersion,
)
: undefined
return {
Name: groupData.Name,
@ -1214,9 +1287,11 @@ export class ChallengeService extends ChallengeRegistry {
).length,
CompletionData: completion,
Location: location,
IsLocked: location.Properties.IsLocked || false,
ImageLocked: location.Properties.LockedIcon || "",
RequiredResources: location.Properties.RequiredResources!,
IsLocked: location?.Properties.IsLocked || false,
ImageLocked: location?.Properties.LockedIcon || "",
RequiredResources:
location?.Properties.RequiredResources || [],
OrderIndex: groupData.OrderIndex ?? 10000,
SwitchData: {
Data: {
Challenges: this.mapSwitchChallenges(
@ -1253,6 +1328,10 @@ export class ChallengeService extends ChallengeRegistry {
}
})
.filter(Boolean) as CompiledChallengeTreeCategory[]
return groups.sort((a, b) => {
return a.OrderIndex - b.OrderIndex
})
}
compileRegistryChallengeTreeData(
@ -1297,6 +1376,7 @@ export class ChallengeService extends ChallengeRegistry {
userId,
gameVersion,
),
OrderIndex: challenge.OrderIndex ?? 10000,
DifficultyLevels: challenge.DifficultyLevels ?? [],
// Only include CompletionData if ParentLocationId is not an empty string
...(challenge.ParentLocationId !== "" && {

View File

@ -21,7 +21,7 @@ import {
getSubLocationByName,
} from "../contracts/dataGen"
import { log, LogLevel } from "../loggingInterop"
import { getVersionedConfig } from "../configSwizzleManager"
import { getConfig } from "../configSwizzleManager"
import { getUserData } from "../databaseHandler"
import {
LocationMasteryData,
@ -265,12 +265,9 @@ export class MasteryService {
// TODO: Refactor this into the new inventory system?
const name = isSniper
? getVersionedConfig<Unlockable[]>(
"SniperUnlockables",
gameVersion,
false,
).find((unlockable) => unlockable.Id === subPackageId)?.Properties
.Name
? getConfig<Unlockable[]>("SniperUnlockables", false).find(
(unlockable) => unlockable.Id === subPackageId,
)?.Properties.Name
: undefined
return {
@ -283,8 +280,8 @@ export class MasteryService {
contractType === "sniper"
? xpRequiredForSniperLevel
: contractType === "evergreen"
? xpRequiredForEvergreenLevel
: xpRequiredForLevel,
? xpRequiredForEvergreenLevel
: xpRequiredForLevel,
subPackageId,
),
Id: isSniper ? subPackageId! : masteryPkg.LocationId,
@ -410,8 +407,8 @@ export class MasteryService {
isSniper
? "sniper"
: locationParentId.includes("SNUG")
? "evergreen"
: "mission",
? "evergreen"
: "mission",
subPkg.Id,
)

View File

@ -187,29 +187,25 @@ export class ProgressionService {
if (masteryData) {
const previousLevel = locationData.Level
let newLocationXp = xpRequiredForLevel(maxLevel)
if (isEvergreenContract) {
newLocationXp = xpRequiredForEvergreenLevel(maxLevel)
} else if (sniperUnlockable) {
newLocationXp = xpRequiredForSniperLevel(maxLevel)
}
locationData.Xp = clampValue(
locationData.Xp + masteryXp + actionXp,
0,
newLocationXp,
isEvergreenContract
? xpRequiredForEvergreenLevel(maxLevel)
: sniperUnlockable
? xpRequiredForSniperLevel(maxLevel)
: xpRequiredForLevel(maxLevel),
)
let newLocationLevel = levelForXp(newLocationXp)
if (isEvergreenContract) {
newLocationLevel = evergreenLevelForXp(newLocationXp)
} else if (sniperUnlockable) {
newLocationLevel = sniperLevelForXp(newLocationXp)
}
locationData.Level = clampValue(newLocationLevel, 1, maxLevel)
locationData.Level = clampValue(
isEvergreenContract
? evergreenLevelForXp(locationData.Xp)
: sniperUnlockable
? sniperLevelForXp(locationData.Xp)
: levelForXp(locationData.Xp),
1,
maxLevel,
)
// If mastery level has gone up, check if there are available drop rewards and award them
if (locationData.Level > previousLevel) {
@ -244,22 +240,14 @@ export class ProgressionService {
// Update the SubLocation data
const profileData = userProfile.Extensions.progression.PlayerProfileXP
let foundSubLocation = profileData.Sublocations.find(
(e) => e.Location === parentLocationId,
)
if (!foundSubLocation) {
foundSubLocation = {
Location: parentLocationId,
Xp: 0,
ActionXp: 0,
}
profileData.Sublocations.push(foundSubLocation)
profileData.Sublocations[contract.Metadata.Location] ??= {
Xp: 0,
ActionXp: 0,
}
foundSubLocation.Xp += masteryXp
foundSubLocation.ActionXp += actionXp
profileData.Sublocations[contract.Metadata.Location].Xp += masteryXp
profileData.Sublocations[contract.Metadata.Location].ActionXp +=
actionXp
return true
}

100
components/cli.ts Normal file
View File

@ -0,0 +1,100 @@
/*
* The Peacock Project - a HITMAN server replacement.
* Copyright (C) 2021-2024 The Peacock Project Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// load as soon as possible to prevent dependency issues
import "./generatedPeacockRequireTable"
// load flags as soon as possible
import { getFlag, loadFlags } from "./flags"
loadFlags()
import { program } from "commander"
import { toolsMenu } from "./tools"
import { readFileSync, writeFileSync } from "fs"
import { pack, unpack } from "msgpackr"
import { log, LogLevel } from "./loggingInterop"
import { startServer } from "./index"
import { PEACOCKVERSTRING } from "./utils"
import * as process from "node:process"
program.description(
"The Peacock Project is a HITMAN™ World of Assassination Trilogy server replacement.",
)
program.option("-v, --version", "print the version number and exit")
program.option(
"--hmr",
"enable experimental hot reloading of contracts",
getFlag("experimentalHMR") as boolean,
)
program.option(
"--plugin-dev-host",
"activate plugin development features - requires plugin dev workspace setup",
getFlag("developmentPluginDevHost") as boolean,
)
program.action(
(options: { hmr: boolean; pluginDevHost: boolean; version: boolean }) => {
if (options.version) {
console.log(`Peacock ${PEACOCKVERSTRING}`)
return process.exit()
}
return startServer(options)
},
)
program.command("tools").description("open the tools UI").action(toolsMenu)
// noinspection RequiredAttributes
program
.command("pack")
.argument("<input>", "input file to pack")
.option("-o, --output <path>", "where to output the packed file to", "")
.description("packs an input file into a Challenge Resource Package")
.action((input, options: { output: string }) => {
const outputPath =
options.output || input.replace(/\.[^/\\.]+$/, ".crp")
writeFileSync(
outputPath,
pack(JSON.parse(readFileSync(input).toString())),
)
log(LogLevel.INFO, `Packed "${input}" to "${outputPath}" successfully.`)
})
// noinspection RequiredAttributes
program
.command("unpack")
.argument("<input>", "input file to unpack")
.option("-o, --output <path>", "where to output the unpacked file to", "")
.description("unpacks a Challenge Resource Package")
.action((input, options: { output: string }) => {
const outputPath =
options.output || input.replace(/\.[^/\\.]+$/, ".json")
writeFileSync(outputPath, JSON.stringify(unpack(readFileSync(input))))
log(
LogLevel.INFO,
`Unpacked "${input}" to "${outputPath}" successfully.`,
)
})
program.parse(process.argv)

View File

@ -108,7 +108,7 @@ import MultiplayerPresets from "../static/MultiplayerPresets.json"
import LobbySlimTemplate from "../static/LobbySlimTemplate.json"
import MasteryDataForLocationTemplate from "../static/MasteryDataForLocationTemplate.json"
import LegacyMasteryLocationTemplate from "../static/LegacyMasteryLocationTemplate.json"
import DefaultCpdConfig from "../static/DefaultCpdConfig.json"
import DefaultCpdConfigs from "../static/DefaultCpdConfigs.json"
import EvergreenGameChangerProperties from "../static/EvergreenGameChangerProperties.json"
import AreaMap from "../static/AreaMap.json"
import ArcadePageTemplate from "../static/ArcadePageTemplate.json"
@ -217,7 +217,7 @@ const configs = {
LobbySlimTemplate,
MasteryDataForLocationTemplate,
LegacyMasteryLocationTemplate,
DefaultCpdConfig,
DefaultCpdConfigs,
EvergreenGameChangerProperties,
AreaMap,
ArcadePageTemplate,

View File

@ -49,8 +49,8 @@ import {
import { log, LogLevel } from "../loggingInterop"
import { randomUUID } from "crypto"
import {
createObjectivesForTarget,
createTimeLimit,
TargetCreator,
} from "../statemachines/contractCreation"
import { createSniperLoadouts, SniperCharacter } from "../menus/sniper"
import { GetForPlay2Body } from "../types/gameSchemas"
@ -277,13 +277,13 @@ contractRoutingRouter.post(
return
}
req.body.creationData.Targets.forEach((target) => {
for (const target of req.body.creationData.Targets) {
if (!target.Selected) {
return
continue
}
objectives.push(...new TargetCreator(target).build())
})
objectives.push(...createObjectivesForTarget(target))
}
req.body.creationData.ContractConditionIds.forEach(
(contractConditionId) => {

View File

@ -833,85 +833,3 @@ export function complications(timeString: string) {
targetsOnly,
]
}
/*
const PISTOL_ELIM = {
$any: {
"?": {
$or: [
{
$eq: ["$.#", "pistol"],
},
{
$eq: ["$.#", "close_combat_pistol_elimination"],
},
],
},
in: ["$Value.KillMethodBroad", "$Value.KillMethodStrict"],
},
}
export class StateMachineGenerationContext {
public createKillMethodStateMachine(): void {
this.weaponTargetStateMachine = {
_comment: `Eliminate ${this.targetId} using weapon`,
Type: "statemachine",
Id: this.weaponTargetStateMachineId,
Category: "secondary",
Definition: {
Scope: "Hit",
Context: {
Targets: [this.targetId],
},
States: {
Start: {
Kill: [
{
Condition: {
$and: [
{
$eq: [
"$Value.RepositoryId",
this.targetId,
],
},
{
$eq: [
"$Value.KillMethodStrict",
this.genContextParams
.KillMethodStrict,
],
},
],
},
Transition: "Success",
},
{
Condition: {
$eq: ["$Value.RepositoryId", this.targetId],
},
Transition: "Failure",
},
],
},
},
},
}
}
private kmConditions(killMethodBroad) {
switch (killMethodBroad) {
case "pistol":
return PISTOL_ELIM
default:
// weapon
return {
$eq: [
"$Value.KillMethodStrict",
this.genContextParams.KillMethodStrict,
],
}
}
}
}
*/

View File

@ -18,6 +18,7 @@
export const orderedETAs = [
"797e204a-ef3d-463b-a386-57df0fe29b8f",
"de9788cc-b9c4-47fc-b5df-86451cd82c43",
"ff5b4e53-49ea-4d85-b94e-d3c8b3fc7ab3",
"80cf04de-8e0b-4f38-b094-600753e2ac24",
"85a67f31-75ce-40f5-a281-7765791f58ca",

View File

@ -21,7 +21,7 @@ import {
ContractHistory,
GameVersion,
HitsCategoryCategory,
IHit,
Hit,
} from "../types/types"
import {
contractIdToHitObject,
@ -242,9 +242,9 @@ export class HitsCategoryService {
for (const id of escalations) {
const contract = controller.resolveContract(id)
if (!contract) {
continue
}
if (!contract) continue
if (contract.Metadata.Season === -1) continue
const isPeacock = contract.Metadata.Season === 0
const season = isPeacock
@ -326,6 +326,17 @@ export class HitsCategoryService {
hit.UserCentricContract.Data.PlaylistData.IsAdded =
favorites.includes(hit.Id)
}
// Save the contract
if (
!controller.contracts.has(
hit.UserCentricContract.Contract.Metadata.Id,
)
) {
await controller.commitNewContract(
hit.UserCentricContract.Contract,
)
}
}
return resp.data.data
@ -525,7 +536,7 @@ export class HitsCategoryService {
const hitObjectList = hits
.map((id) => contractIdToHitObject(id, gameVersion, userId))
.filter(Boolean) as IHit[]
.filter(Boolean) as Hit[]
if (!this.paginationExempt.includes(category)) {
const paginated = paginate(hitObjectList, this.hitsPerPage)

View File

@ -139,8 +139,12 @@ export const missionsInLocations = {
"85a2b618-2e3c-444f-931c-b89d566e45f7",
"88451dd9-4b57-441e-9eab-e20b9879bafa",
],
LOCATION_HOKKAIDO_MAMUSHI: [],
LOCATION_HOKKAIDO_FLU: [],
LOCATION_NEWZEALAND: ["3efc73f9-33f0-4af6-9508-7208e6851394"],
LOCATION_NEWZEALAND: [
"e1e86206-d3f0-a819-e477-3d80e55e8a40",
"3efc73f9-33f0-4af6-9508-7208e6851394",
],
LOCATION_MIAMI: [
"719ee044-4b05-4bd9-b2bb-75029f6d2a35",
"5284cb9f-9bdd-4b00-99c3-0b5939b01818",
@ -287,6 +291,7 @@ export const missionsInLocations = {
"dd68d30f-3900-415f-bb17-84681a2cd4fc",
"9cbdb972-95df-4e0a-be77-7937ec6f2fb0",
],
LOCATION_MIAMI: ["de9788cc-b9c4-47fc-b5df-86451cd82c43"],
},
sniper: {
LOCATION_AUSTRIA: ["ff9f46cf-00bd-4c12-b887-eac491c3a96d"],

View File

@ -18,19 +18,6 @@
import { ContractSession } from "../types/types"
/**
* Changes a set to an array.
*
* @param set The set.
*/
export function normalizeSet<T>(set: Set<T>): T[] {
const l: T[] = []
set.forEach((i) => l.push(i))
return l
}
const SESSION_SET_PROPS: (keyof ContractSession)[] = [
"targetKills",
"npcKills",
@ -76,7 +63,7 @@ export function serializeSession(session: ContractSession): unknown {
if (session[key as K] instanceof Set) {
// @ts-expect-error Type mismatch.
o[key] = normalizeSet(session[key])
o[key] = Array.from(session[key])
continue
}
@ -109,7 +96,7 @@ export function deserializeSession(
}
for (const map of SESSION_MAP_PROPS) {
if (Object.hasOwn(session, map)) {
if (session[map]) {
// @ts-expect-error Type mismatch.
session[map] = new Map(session[map])
}

View File

@ -31,7 +31,7 @@ import type {
GameVersion,
GenSingleMissionFunc,
GenSingleVideoFunc,
IHit,
Hit,
MissionManifest,
PeacockLocationsData,
PlayNextGetCampaignsHookReturn,
@ -359,6 +359,7 @@ export class Controller {
PlayNextGetCampaignsHookReturn | undefined
>
onMissionEnd: SyncHook<[/** session */ ContractSession]>
onEscalationReset: SyncHook<[/** groupId */ string]>
}
public configManager: typeof configManagerType = {
getConfig,
@ -402,9 +403,49 @@ export class Controller {
getSearchResults: new AsyncSeriesHook(),
getNextCampaignMission: new SyncBailHook(),
onMissionEnd: new SyncHook(),
onEscalationReset: new SyncHook(),
}
}
/**
* You should use {@link smf.modIsInstalled} instead!
*
* Returns whether a mod is UNAVAILABLE.
*
* @param modId The mod's ID.
* @returns If the mod is unavailable. You should probably abort initialization if true is returned. Also returns true if the `overrideFrameworkChecks` flag is set.
* @deprecated since v5.5.0, use `!controller.smf.modIsInstalled`
*/
public addClientSideModDependency(modId: string): boolean {
log(
LogLevel.WARN,
"controller.addClientSideModDependency is deprecated, use !controller.smf.modIsInstalled instead!",
"plugins",
)
return (
getFlag("overrideFrameworkChecks") === true ||
!this.smf.modIsInstalled(modId)
)
}
/**
* You should use {@link smf.modIsInstalled} instead!
*
* Returns whether a mod is available and installed.
*
* @param modId The mod's ID.
* @returns If the mod is available (or the `overrideFrameworkChecks` flag is set). You should probably abort initialisation if false is returned.
* @deprecated since v7.0.0, use `controller.smf.modIsInstalled`
*/
public modIsInstalled(modId: string): boolean {
log(
LogLevel.WARN,
"controller.modIsInstalled is deprecated, use controller.smf.modIsInstalled instead!",
"plugins",
)
return this.smf.modIsInstalled(modId)
}
/**
* Starts the service and loads in all contracts.
*
@ -434,6 +475,16 @@ export class Controller {
this._getETALocations()
this.index()
try {
await this._loadResources()
this.hooks.challengesLoaded.call()
this.hooks.masteryDataLoaded.call()
} catch (e) {
log(LogLevel.ERROR, `Fatal error with challenge bootstrap`, "boot")
log(LogLevel.ERROR, e)
}
const deployPath = SMFSupport.modFrameworkDataPath
if (typeof deployPath === "string") {
@ -447,16 +498,6 @@ export class Controller {
}
this.hooks.serverStart.call()
try {
await this._loadResources()
this.hooks.challengesLoaded.call()
this.hooks.masteryDataLoaded.call()
} catch (e) {
log(LogLevel.ERROR, `Fatal error with challenge bootstrap`, "boot")
log(LogLevel.ERROR, e)
}
}
private _getETALocations(): void {
@ -730,9 +771,7 @@ export class Controller {
)
await this.commitNewContract(contractData)
if (PEACOCK_DEV) {
log(LogLevel.DEBUG, `Saved contract to contracts/${pubId}.json`)
}
log(LogLevel.DEBUG, `Saved contract to contracts/${pubId}.json`)
return contractData
}
@ -825,7 +864,6 @@ export class Controller {
/**
* Get all global challenges and register a simplified version of them.
* @param gameVersion A GameVersion object representing the version of the game.
*
*/
private registerGlobalChallenges(gameVersion: GameVersion) {
const regGlobalChallenges: RegistryChallenge[] = getVersionedConfig<
@ -856,7 +894,7 @@ export class Controller {
],
meta: {
Location: "GLOBAL",
GameVersion: gameVersion,
GameVersions: [gameVersion],
},
})
}
@ -914,20 +952,22 @@ export class Controller {
}
private _handleChallengeResources(data: ChallengePackage): void {
for (const group of data.groups) {
this.challengeService.registerGroup(
group,
data.meta.Location,
data.meta.GameVersion,
)
for (const challenge of group.Challenges) {
this.challengeService.registerChallenge(
challenge,
group.CategoryId,
for (const version of data.meta.GameVersions) {
for (const group of data.groups) {
this.challengeService.registerGroup(
group,
data.meta.Location,
data.meta.GameVersion,
version,
)
for (const challenge of group.Challenges) {
this.challengeService.registerChallenge(
challenge,
group.CategoryId,
data.meta.Location,
version,
)
}
}
}
}
@ -1035,6 +1075,7 @@ export class Controller {
module: { exports: {} },
exports: {},
process,
fetch,
require: createPeacockRequire(pluginName),
})
@ -1175,7 +1216,7 @@ export function contractIdToHitObject(
contractId: string,
gameVersion: GameVersion,
userId: string,
): IHit | undefined {
): Hit | undefined {
const contract = controller.resolveContract(contractId)
if (!contract) {

View File

@ -16,56 +16,115 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { readFile, writeFile } from "atomically"
import { join } from "path"
import type { ContractSession, GameVersion, UserProfile } from "./types/types"
import { serializeSession, deserializeSession } from "./contracts/sessions"
import { deserializeSession, serializeSession } from "./contracts/sessions"
import { castUserProfile } from "./utils"
import { log, LogLevel } from "./loggingInterop"
import { unlink, readdir } from "fs/promises"
import { mkdir, readdir, readFile, unlink, writeFile } from "fs/promises"
import type * as nodeFs from "node:fs/promises"
import { existsSync } from "fs"
// unlink, mkdir, readdir from node:fs/promises
type NodeUnlinkMkdirReaddir = Pick<
typeof nodeFs,
"unlink" | "mkdir" | "readdir" | "writeFile" | "readFile"
>
// custom exists function because node doesn't have an async version of existsSync
type ExistsPromise = {
exists: (path: string) => Promise<boolean>
}
/**
* Container for functions that handle file read/writes,
* which could otherwise break if writing partial data.
* The fs implementation that this system uses.
*/
export type DataStorageFs = NodeUnlinkMkdirReaddir & ExistsPromise
/**
* Handles the dispatching of user data in a way that avoids FS operations unless absolutely needed.
*/
class AsyncUserDataGuard {
private readonly userData: Record<string, UserProfile> = {}
private readonly dirtyProfiles: Set<string> = new Set()
/** @internal */
readonly userData: Map<string, UserProfile> = new Map()
/** @internal */
readonly dirtyProfiles: Set<string> = new Set()
/**
* Internal list of background tasks that have been scheduled.
* The key is the versioned user ID.
* @internal
*/
readonly tasks: Map<string, NodeJS.Timeout> = new Map()
getProfile(id: string): UserProfile {
return this.userData[id]
/** If true, none of the background tasks will attempt to write. */
#paused = false
/**
* Get the fs implementation to use for file read/writes.
* Mainly for test purposes, but could also be used by plugins to make it use a real database.
*/
getFs(): DataStorageFs {
return {
writeFile,
readFile,
unlink,
mkdir,
readdir,
exists(path) {
// node's fs doesn't have a promise version of exists
return Promise.resolve(existsSync(path))
},
}
}
/**
* Get a loaded user's profile.
* @param id The target user ID.
* @returns The profile, or undefined if they're not loaded.
* Profiles are loaded when they perform the auth handshake via the game.
*/
getProfile(id: string): UserProfile | undefined {
return this.userData.get(id)
}
addLoadedProfile(id: string, profile: UserProfile): void {
if (!this.userData[id]) {
setInterval(() => {
if (!this.dirtyProfiles.has(id)) {
if (!this.userData.has(id) && !this.tasks.has(id)) {
const interval = setInterval(() => {
if (!this.dirtyProfiles.has(id) || this.#paused) {
return
}
this.dirtyProfiles.delete(id)
this.save(id)
}, 3000)
this.write(id)
.then(() => undefined)
.catch((e) => {
log(
LogLevel.ERROR,
`Failed to write user profile ${id}: ${e}`,
)
})
}, 3000).unref()
this.tasks.set(id, interval.unref())
}
this.userData[id] = profile
this.userData.set(id, profile)
// just in case
this.dirtyProfiles.delete(id)
}
/**
* Saves any modifications to a given profile, called by a background scheduled task.
* @param id The user ID.
*/
save(id: string) {
this.dirtyProfiles.delete(id)
this.write(id)
.then(() => undefined)
.catch((e) => {
log(LogLevel.ERROR, `Failed to write user profile ${id}: ${e}`)
})
}
markDirty(id: string): void {
this.dirtyProfiles.add(id)
}
private async write(versionedId: string): Promise<void> {
/** @internal */
async write(versionedId: string): Promise<void> {
let path
const [id, gameVersion] = versionedId.split(".")
@ -76,24 +135,58 @@ class AsyncUserDataGuard {
path = join("userdata", "users", `${id}.json`)
}
await writeFile(path, JSON.stringify(this.getProfile(versionedId)))
await this.getFs().writeFile(
path,
JSON.stringify(this.getProfile(versionedId)),
)
}
/**
* Immediately write all loaded profiles to the disk, even if no changes are pending.
*/
async forceFlush() {
const taskKeys = this.tasks.keys()
this.#paused = true
for (const id of taskKeys) {
this.dirtyProfiles.delete(id)
await this.write(id)
}
this.#paused = false
}
/**
* Unload all profiles without saving.
*/
unloadAll() {
for (const id of this.tasks.keys()) {
clearInterval(this.tasks.get(id))
this.dirtyProfiles.delete(id)
this.userData.delete(id)
}
}
}
const asyncGuard = new AsyncUserDataGuard()
/**
* If you are touching this, you better know what you're doing.
*/
export const asyncGuard = new AsyncUserDataGuard()
/**
* Gets a user's profile data.
*
* @param userId The user's ID.
* @param gameVersion The game's version.
* @returns The user's profile
* @returns The user's profile, OR UNDEFINED if not loaded.
*/
export function getUserData(
userId: string,
gameVersion: GameVersion,
): UserProfile {
const data = asyncGuard.getProfile(`${userId}.${gameVersion}`)
// TODO: consumers could have undefined returned - this function needs undefined
// as part of it's signature, but that requires a lot of changes.
const data = asyncGuard.getProfile(`${userId}.${gameVersion}`)!
// NOTE: ProfileLevel always starts at 1
if (data?.Extensions?.progression?.PlayerProfileXP?.ProfileLevel === 0) {
@ -104,7 +197,7 @@ export function getUserData(
}
/**
* Only attempt to load a user's profile if it hasn't been loaded yet
* Attempts 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.
@ -150,7 +243,7 @@ export async function loadUserData(
}
const userProfile = castUserProfile(
JSON.parse((await readFile(path)).toString()),
JSON.parse((await asyncGuard.getFs().readFile(path)).toString()),
gameVersion,
path,
)
@ -199,16 +292,18 @@ export async function getExternalUserData(
externalFolder: string,
gameVersion: GameVersion,
): Promise<string> {
const fs = asyncGuard.getFs()
if (["scpc", "h1", "h2"].includes(gameVersion)) {
return (
await readFile(
await fs.readFile(
join("userdata", gameVersion, externalFolder, `${userId}.json`),
)
).toString()
}
return (
await readFile(join("userdata", externalFolder, `${userId}.json`))
await fs.readFile(join("userdata", externalFolder, `${userId}.json`))
).toString()
}
@ -226,14 +321,16 @@ export async function writeExternalUserData(
userData: string,
gameVersion: GameVersion,
): Promise<void> {
const fs = asyncGuard.getFs()
if (["scpc", "h1", "h2"].includes(gameVersion)) {
return await writeFile(
return await fs.writeFile(
join("userdata", gameVersion, externalFolder, `${userId}.json`),
userData,
)
}
return await writeFile(
return await fs.writeFile(
join("userdata", externalFolder, `${userId}.json`),
userData,
)
@ -248,7 +345,8 @@ export async function writeExternalUserData(
export async function getContractSession(
identifier: string,
): Promise<ContractSession> {
const files = await readdir("contractSessions")
const fs = asyncGuard.getFs()
const files = await fs.readdir("contractSessions")
const filtered = files.filter((fn) => fn.endsWith(`_${identifier}.json`))
if (filtered.length === 0) {
@ -256,10 +354,12 @@ export async function getContractSession(
}
// The filtered files have the same identifier, they are just stored at different slots
// So we can read any of them and it will be the same.
// So we can read any of them, and it will be the same.
return deserializeSession(
JSON.parse(
(await readFile(join("contractSessions", filtered[0]))).toString(),
(
await fs.readFile(join("contractSessions", filtered[0]))
).toString(),
),
)
}
@ -274,7 +374,9 @@ export async function writeContractSession(
identifier: string,
session: ContractSession,
): Promise<void> {
return await writeFile(
const fs = asyncGuard.getFs()
return await fs.writeFile(
join("contractSessions", `${identifier}.json`),
JSON.stringify(serializeSession(session)),
)
@ -287,5 +389,46 @@ export async function writeContractSession(
* @throws ENOENT if the file is not found.
*/
export async function deleteContractSession(fileName: string): Promise<void> {
return await unlink(join("contractSessions", `${fileName}.json`))
const fs = asyncGuard.getFs()
return await fs.unlink(join("contractSessions", `${fileName}.json`))
}
/**
* Sets up the required file structure for the server.
*
* @param joinFunc The path join function to use, defaulting to Node's. You may need to specify it if working in a VFS.
*/
export async function setupFileStructure(joinFunc = join) {
const fs = asyncGuard.getFs()
for (const dir of [
"contractSessions",
"plugins",
"userdata",
"contracts",
joinFunc("userdata", "epicids"),
joinFunc("userdata", "steamids"),
joinFunc("userdata", "users"),
joinFunc("userdata", "h1", "steamids"),
joinFunc("userdata", "h1", "epicids"),
joinFunc("userdata", "h1", "users"),
joinFunc("userdata", "h2", "steamids"),
joinFunc("userdata", "h2", "users"),
joinFunc("userdata", "scpc", "users"),
joinFunc("userdata", "scpc", "steamids"),
joinFunc("images", "actors"),
joinFunc("images", "contracts"),
joinFunc("images", "contracts", "elusive"),
joinFunc("images", "contracts", "escalation"),
joinFunc("images", "contracts", "featured"),
joinFunc("images", "unlockables_override"),
]) {
if (await fs.exists(dir)) {
continue
}
log(LogLevel.DEBUG, `Creating missing directory ${dir}`)
await fs.mkdir(dir, { recursive: true })
}
}

View File

@ -16,28 +16,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { RPCClient } from "./discord/client"
import { getConfig } from "./configSwizzleManager"
import type { GameVersion, MissionType } from "./types/types"
import { getFlag } from "./flags"
import { log, LogLevel } from "./loggingInterop"
import { RPCClient } from "./client"
import { getConfig } from "../configSwizzleManager"
import type { GameVersion, MissionType } from "../types/types"
import { getFlag } from "../flags"
import { log, LogLevel } from "../loggingInterop"
let rpcClient: undefined | RPCClient
/*@__NOINLINE__*/
const processStartTime = Math.round(Date.now() / 1000)
export function initRp(): void {
Object.keys(getConfig("Entrances", false)).forEach((key) => {
for (const key of Object.keys(getConfig("Entrances", false))) {
if (!scenePathToRpAsset(key, []) && PEACOCK_DEV) {
log(LogLevel.DEBUG, `WARNING missing scene ${key} for RP!`)
log(LogLevel.WARN, `Missing scene ${key} for RP!`, "discord")
}
})
}
// creates a new rp client, pretty self-explanatory.
rpcClient = new RPCClient()
// connects to the Peacock discord developer app, which contains the images for the rp.
rpcClient.login({
void rpcClient.login({
clientId: "846361353027584013",
})
@ -143,7 +143,7 @@ type BrickDataMap = Record<string, [string, string, string]>
export function scenePathToRpAsset(
scenePath: string,
bricks: string[],
): string[] | undefined {
): [string, string, string] | undefined {
const brickAssetsMap = getConfig<BrickDataMap>(
"DiscordRichAssetsForBricks",
false,
@ -220,11 +220,16 @@ export function scenePathToRpAsset(
// dartmoor
case "assembly:/_pro/scenes/missions/ancestral/scene_bulldog.entity":
case "assembly:/_pro/scenes/missions/ancestral/scene_bulldog_fern.entity":
return ["dartmoordeathoffamily", "Death in the Family", "Dartmoor"]
case "assembly:/_pro/scenes/missions/ancestral/scene_smoothsnake.entity":
return ["dartmoorgardenshow", "Dartmoor Garden Show", "Dartmoor"]
case "assembly:/_pro/scenes/missions/ancestral/scene_ancestral_vesper.entity":
return ["elusivevesper", "The Procurers", "Dartmoor"]
case "assembly:/_pro/scenes/missions/ancestral/scene_ancestral_harebell.entity":
return ["harebell", "The Sloth Depletion", "Dartmoor"]
case "assembly:/_pro/scenes/missions/ancestral/scene_hollyhock.entity":
return ["hollyhock", "The Wrath Termination", "Dartmoor"]
// columbia
case "assembly:/_pro/scenes/missions/colombia/mission_millipede/scene_millipede.entity":
@ -258,6 +263,8 @@ export function scenePathToRpAsset(
return ["berlinegghunt", "Berlin Egg Hunt", "Berlin"]
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_tomorrowland.entity":
return ["elusivetomorrowland", "The Drop", "Berlin"]
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_ambrosia.entity":
return ["ambrosia", "The Lust Assignation", "Berlin"]
// mendozer
case "assembly:/_pro/scenes/missions/elegant/scene_llama_elusive_clerico.entity":
@ -270,6 +277,8 @@ export function scenePathToRpAsset(
case "assembly:/_pro/scenes/missions/elegant/scene_whitedryas_level2.entity":
case "assembly:/_pro/scenes/missions/elegant/scene_whitedryas_level3.entity":
return ["mendozafarewell", "The Farewell", "Mendoza"]
case "assembly:/_pro/scenes/missions/elegant/scene_frangipani.entity":
return ["frangipani", "The Envy Contention", "Mendoza"]
// dubai
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gecko_angelica.entity":
@ -290,6 +299,8 @@ export function scenePathToRpAsset(
return ["chongqingendofanera", "End Of An Era", "Chongqing"]
case "assembly:/_pro/scenes/missions/wet/scene_rat_elusive_redsnapper.entity":
return ["elusiveredsnapper", "The Rage", "Chongqing"]
case "assembly:/_pro/scenes/missions/wet/scene_wet_azalea.entity":
return ["azalea", "The Gluttony Gobble", "Chongqing"]
// training
case "assembly:/_pro/scenes/missions/thefacility/_scene_polarbear_005.entity":
@ -366,6 +377,10 @@ export function scenePathToRpAsset(
return ["austria", "The Last Yardbird", "Austria"]
case "assembly:/_pro/scenes/missions/salty/mission_seagull/scene_seagull.entity":
return ["hantuport", "The Pen and the Sword", "Hantu Port"]
// snug
case "assembly:/_pro/scenes/missions/snug/scene_vanilla.entity":
return ["snug", "Safehouse", "Safehouse"]
}
return undefined

View File

@ -64,7 +64,10 @@ export class EpicH3Strategy extends EntitlementStrategy {
export class IOIStrategy extends EntitlementStrategy {
private readonly _remoteService: string
constructor(gameVersion: GameVersion, private readonly issuerId: string) {
constructor(
gameVersion: GameVersion,
private readonly issuerId: string,
) {
super()
this.issuerId = issuerId
this._remoteService = getRemoteService(gameVersion)!

View File

@ -33,10 +33,10 @@ import {
} from "./types/types"
import { contractTypes, gameDifficulty, ServerVer } from "./utils"
import { json as jsonMiddleware } from "body-parser"
import { log, LogLevel } from "./loggingInterop"
import { log, logDebug, LogLevel } from "./loggingInterop"
import { getUserData, writeUserData } from "./databaseHandler"
import { controller } from "./controller"
import { swapToLocationStatus } from "./discordRp"
import { swapToLocationStatus } from "./discord/discordRp"
import { randomUUID } from "crypto"
import { liveSplitManager } from "./livesplit/liveSplitManager"
import { handleMultiplayerEvent } from "./multiplayer/multiplayerService"
@ -86,7 +86,6 @@ const pushMessageQueue = new Map<string, PushMessage[]>()
* @param userId The push message's target user.
* @param message The raw push message to send.
* @see enqueueEvent
* @author grappigegovert
*/
export function enqueuePushMessage(userId: string, message: unknown): void {
let userQueue
@ -113,7 +112,6 @@ export function enqueuePushMessage(userId: string, message: unknown): void {
*
* @param session The contract session.
* @param objective The objective object.
* @author Reece Dunham
*/
export function registerObjectiveListener(
session: ContractSession,
@ -225,7 +223,6 @@ export function setupScoring(
* @param userId The event's target user.
* @param event The event to send.
* @see enqueuePushMessage
* @author grappigegovert
*/
export function enqueueEvent(userId: string, event: ServerToClientEvent): void {
let userQueue: S2CEventWithTimestamp[] | undefined
@ -352,7 +349,9 @@ export function newSession(
failedObjectives: new Set(),
recording: PeacockCameraStatus.NotSpotted,
lastAccident: 0,
lastKill: {},
lastKill: {
legacyIsUnnoticed: true,
},
kills: new Set(),
compat: doScoring,
markedTargets: new Set(),
@ -520,7 +519,6 @@ eventRouter.post(
*
* @param uId The user's ID.
* @returns The ID for the user's active session.
* @author Reece Dunham
*/
export function getActiveSessionIdForUser(uId: string): string | undefined {
return userIdToTempSession.get(uId)
@ -531,7 +529,6 @@ export function getActiveSessionIdForUser(uId: string): string | undefined {
*
* @param uId The user's ID.
* @returns The user's active contract session.
* @author Reece Dunham
*/
export function getSession(uId: string): ContractSession | undefined {
const currentSession = getActiveSessionIdForUser(uId)
@ -643,7 +640,8 @@ function saveEvents(
const response: string[] = []
const processed: string[] = []
const userData = getUserData(userId, gameVersion)
events.forEach((event) => {
for (const event of events) {
let session = contractSessions.get(event.ContractSessionId)
if (!session) {
@ -669,13 +667,11 @@ function saveEvents(
session.contractId !== event.ContractId ||
session.userId !== userId
) {
if (PEACOCK_DEV) {
log(LogLevel.DEBUG, "No session or session user ID mismatch!")
console.debug(session)
console.debug(event)
}
log(LogLevel.DEBUG, "No session or session user ID mismatch!")
logDebug(session)
logDebug(event)
return // session does not exist or contractid/userid doesn't match
continue // session does not exist or contractid/userid doesn't match
}
session.duration = event.Timestamp
@ -717,7 +713,7 @@ function saveEvents(
)
if (val.state === "Failure") {
if (PEACOCK_DEV && contractType !== "evergreen") {
if (contractType !== "evergreen") {
log(LogLevel.DEBUG, `Objective failed: ${objectiveId}`)
}
@ -772,7 +768,7 @@ function saveEvents(
processed.push(event.Name)
response.push(process.hrtime.bigint().toString())
return
continue
}
// these events are important but may be fired after the timer is over
@ -790,14 +786,14 @@ function saveEvents(
) {
// Do not handle events that occur after exiting the level
response.push(process.hrtime.bigint().toString())
return
continue
}
if (handleMultiplayerEvent(event, session)) {
processed.push(event.Name)
response.push(process.hrtime.bigint().toString())
return
continue
}
switch (event.Name) {
@ -808,6 +804,8 @@ function saveEvents(
)
break
case "Kill": {
let couldCauseNoticedKill = true
const killValue = (event as KillC2SEvent).Value
if (session.firstKillTimestamp === undefined) {
@ -821,6 +819,12 @@ function saveEvents(
timestamp: event.Timestamp,
repositoryIds: [killValue.RepositoryId],
}
if (gameVersion === "h1" && killValue.Accident) {
// this was an accident, can't be a noticed kill
session.lastKill.legacyIsUnnoticed = true
couldCauseNoticedKill = false
}
}
if (killValue.KillContext === EDeathContext.eDC_NOT_HERO) {
@ -830,7 +834,9 @@ function saveEvents(
`${killValue.RepositoryId} eliminated, 47 not responsible`,
)
response.push(process.hrtime.bigint().toString())
return
session.lastKill.legacyIsUnnoticed = true
couldCauseNoticedKill = false
continue
}
log(
@ -838,6 +844,10 @@ function saveEvents(
`Actor ${killValue.RepositoryId} eliminated.`,
)
if (couldCauseNoticedKill) {
session.lastKill.legacyIsUnnoticed = false
}
if (killValue.IsTarget || contractType === "creation") {
const kill: RatingKill = {
KillClass: killValue.KillClass,
@ -859,6 +869,9 @@ function saveEvents(
break
}
case "Unnoticed_Kill":
session.lastKill.legacyIsUnnoticed = true
break
case "CrowdNPC_Died":
session.crowdNpcKills += 1
break
@ -1120,9 +1133,9 @@ function saveEvents(
processed.push(event.Name)
response.push(process.hrtime.bigint().toString())
})
}
if (PEACOCK_DEV && processed.length > 0) {
if (processed.length > 0) {
log(
LogLevel.DEBUG,
`Event summary: ${picocolors.gray(processed.join(", "))}`,

View File

@ -22,6 +22,10 @@ import { ContractProgressionData } from "./types/types"
import { getFlag } from "./flags"
import { EVERGREEN_LEVEL_INFO } from "./utils"
type DefaultCpdConfigs = {
[cpdId: string]: ContractProgressionData
}
export function setCpd(
data: ContractProgressionData,
uID: string,
@ -42,15 +46,22 @@ export function getCpd(uID: string, cpdID: string): ContractProgressionData {
if (!Object.keys(userData.Extensions.CPD).includes(cpdID)) {
const defaultCPD = getConfig(
"DefaultCpdConfig",
"DefaultCpdConfigs",
false,
) as ContractProgressionData
) as DefaultCpdConfigs
setCpd(defaultCPD, uID, cpdID)
setCpd(
Object.keys(defaultCPD).includes(cpdID) ? defaultCPD[cpdID] : {},
uID,
cpdID,
)
}
// NOTE: Override the EvergreenLevel with the latest Mastery Level
if (getFlag("gameplayUnlockAllFreelancerMasteries")) {
if (
getFlag("gameplayUnlockAllFreelancerMasteries") &&
cpdID === "f8ec92c2-4fa2-471e-ae08-545480c746ee"
) {
userData.Extensions.CPD[cpdID]["EvergreenLevel"] =
EVERGREEN_LEVEL_INFO.length
}

View File

@ -45,6 +45,10 @@ const defaultFlags: Flags = {
desc: "[Gameplay] Show elusive targets in instinct like normal targets would appear on normal missions. (for speedrunners who are submitting to speedrun.com, just as a reminder, this tool is for practice only!)",
default: false,
},
legacyNoticedKillScoring: {
desc: '[Gameplay] In the HITMAN 2016 engine, if noticed kills should behave in the official way ("vanilla"), or how they were previously handled by Peacock ("sane")',
default: "vanilla",
},
jokes: {
desc: "[Services] The Peacock server window will tell you a joke on startup if this is set to true.",
default: false,

View File

@ -19,7 +19,6 @@
import * as configSwizzleManager from "./configSwizzleManager"
import * as controller from "./controller"
import * as databaseHandler from "./databaseHandler"
import * as discordRp from "./discordRp"
import * as entitlementStrategies from "./entitlementStrategies"
import * as eventHandler from "./eventHandler"
import * as evergreen from "./evergreen"
@ -57,6 +56,7 @@ import * as leaderboards from "./contracts/leaderboards"
import * as missionsInLocation from "./contracts/missionsInLocation"
import * as sessions from "./contracts/sessions"
import * as client from "./discord/client"
import * as discordRp from "./discord/discordRp"
import * as ipc from "./discord/ipc"
import * as liveSplitClient from "./livesplit/liveSplitClient"
import * as liveSplitManager from "./livesplit/liveSplitManager"
@ -79,176 +79,66 @@ import * as contractCreation from "./statemachines/contractCreation"
import * as escalationService from "./contracts/escalations/escalationService"
export default {
"@peacockproject/core/configSwizzleManager": {
__esModule: true,
...configSwizzleManager,
},
"@peacockproject/core/controller": { __esModule: true, ...controller },
"@peacockproject/core/databaseHandler": {
__esModule: true,
...databaseHandler,
},
"@peacockproject/core/discordRp": { __esModule: true, ...discordRp },
"@peacockproject/core/entitlementStrategies": {
__esModule: true,
...entitlementStrategies,
},
"@peacockproject/core/eventHandler": { __esModule: true, ...eventHandler },
"@peacockproject/core/evergreen": { __esModule: true, ...evergreen },
"@peacockproject/core/flags": { __esModule: true, ...flags },
"@peacockproject/core/hooksImpl": { __esModule: true, ...hooksImpl },
"@peacockproject/core/hotReloadService": {
__esModule: true,
...hotReloadService,
},
"@peacockproject/core/inventory": { __esModule: true, ...inventory },
"@peacockproject/core/loadouts": { __esModule: true, ...loadouts },
"@peacockproject/core/loggingInterop": {
__esModule: true,
...loggingInterop,
},
"@peacockproject/core/menuData": { __esModule: true, ...menuData },
"@peacockproject/core/oauthToken": { __esModule: true, ...oauthToken },
"@peacockproject/core/officialServerAuth": {
__esModule: true,
...officialServerAuth,
},
"@peacockproject/core/ownership": { __esModule: true, ...ownership },
"@peacockproject/core/platformEntitlements": {
__esModule: true,
...platformEntitlements,
},
"@peacockproject/core/playStyles": { __esModule: true, ...playStyles },
"@peacockproject/core/profileHandler": {
__esModule: true,
...profileHandler,
},
"@peacockproject/core/scoreHandler": { __esModule: true, ...scoreHandler },
"@peacockproject/core/smfSupport": { __esModule: true, ...smfSupport },
"@peacockproject/core/utils": { __esModule: true, ...utils },
"@peacockproject/core/webFeatures": { __esModule: true, ...webFeatures },
"@peacockproject/core/2016/legacyContractHandler": {
__esModule: true,
...legacyContractHandler,
},
"@peacockproject/core/2016/legacyMenuData": {
__esModule: true,
...legacyMenuData,
},
"@peacockproject/core/2016/legacyProfileRouter": {
__esModule: true,
...legacyProfileRouter,
},
"@peacockproject/core/candle/challengeHelpers": {
__esModule: true,
...challengeHelpers,
},
"@peacockproject/core/candle/challengeService": {
__esModule: true,
...challengeService,
},
"@peacockproject/core/candle/masteryService": {
__esModule: true,
...masteryService,
},
"@peacockproject/core/candle/progressionService": {
__esModule: true,
...progressionService,
},
"@peacockproject/core/contracts/contractRouting": {
__esModule: true,
...contractRouting,
},
"@peacockproject/core/contracts/contractsModeRouting": {
__esModule: true,
...contractsModeRouting,
},
"@peacockproject/core/contracts/dataGen": { __esModule: true, ...dataGen },
"@peacockproject/core/contracts/elusiveTargetArcades": {
__esModule: true,
...elusiveTargetArcades,
},
"@peacockproject/core/contracts/elusiveTargets": {
__esModule: true,
...elusiveTargets,
},
"@peacockproject/core/contracts/hitsCategoryService": {
__esModule: true,
...hitsCategoryService,
},
"@peacockproject/core/contracts/leaderboards": {
__esModule: true,
...leaderboards,
},
"@peacockproject/core/contracts/missionsInLocation": {
__esModule: true,
...missionsInLocation,
},
"@peacockproject/core/contracts/sessions": {
__esModule: true,
...sessions,
},
"@peacockproject/core/discord/client": { __esModule: true, ...client },
"@peacockproject/core/discord/ipc": { __esModule: true, ...ipc },
"@peacockproject/core/livesplit/liveSplitClient": {
__esModule: true,
...liveSplitClient,
},
"@peacockproject/core/livesplit/liveSplitManager": {
__esModule: true,
...liveSplitManager,
},
"@peacockproject/core/menus/campaigns": { __esModule: true, ...campaigns },
"@peacockproject/core/menus/destinations": {
__esModule: true,
...destinations,
},
"@peacockproject/core/menus/favoriteContracts": {
__esModule: true,
...favoriteContracts,
},
"@peacockproject/core/menus/hub": { __esModule: true, ...hub },
"@peacockproject/core/menus/imageHandler": {
__esModule: true,
...imageHandler,
},
"@peacockproject/core/menus/menuSystem": {
__esModule: true,
...menuSystem,
},
"@peacockproject/core/menus/planning": { __esModule: true, ...planning },
"@peacockproject/core/menus/playerProfile": {
__esModule: true,
...playerProfile,
},
"@peacockproject/core/menus/playnext": { __esModule: true, ...playnext },
"@peacockproject/core/menus/sniper": { __esModule: true, ...sniper },
"@peacockproject/core/menus/stashpoints": {
__esModule: true,
...stashpoints,
},
"@peacockproject/core/multiplayer/multiplayerMenuData": {
__esModule: true,
...multiplayerMenuData,
},
"@peacockproject/core/multiplayer/multiplayerService": {
__esModule: true,
...multiplayerService,
},
"@peacockproject/core/multiplayer/multiplayerUtils": {
__esModule: true,
...multiplayerUtils,
},
"@peacockproject/core/statemachines/contextListeners": {
__esModule: true,
...contextListeners,
},
"@peacockproject/core/statemachines/contractCreation": {
__esModule: true,
...contractCreation,
},
"@peacockproject/core/contracts/escalations/escalationService": {
__esModule: true,
...escalationService,
},
"@peacockproject/core/configSwizzleManager": configSwizzleManager,
"@peacockproject/core/controller": controller,
"@peacockproject/core/databaseHandler": databaseHandler,
"@peacockproject/core/entitlementStrategies": entitlementStrategies,
"@peacockproject/core/eventHandler": eventHandler,
"@peacockproject/core/evergreen": evergreen,
"@peacockproject/core/flags": flags,
"@peacockproject/core/hooksImpl": hooksImpl,
"@peacockproject/core/hotReloadService": hotReloadService,
"@peacockproject/core/inventory": inventory,
"@peacockproject/core/loadouts": loadouts,
"@peacockproject/core/loggingInterop": loggingInterop,
"@peacockproject/core/menuData": menuData,
"@peacockproject/core/oauthToken": oauthToken,
"@peacockproject/core/officialServerAuth": officialServerAuth,
"@peacockproject/core/ownership": ownership,
"@peacockproject/core/platformEntitlements": platformEntitlements,
"@peacockproject/core/playStyles": playStyles,
"@peacockproject/core/profileHandler": profileHandler,
"@peacockproject/core/scoreHandler": scoreHandler,
"@peacockproject/core/smfSupport": smfSupport,
"@peacockproject/core/utils": utils,
"@peacockproject/core/webFeatures": webFeatures,
"@peacockproject/core/2016/legacyContractHandler": legacyContractHandler,
"@peacockproject/core/2016/legacyMenuData": legacyMenuData,
"@peacockproject/core/2016/legacyProfileRouter": legacyProfileRouter,
"@peacockproject/core/candle/challengeHelpers": challengeHelpers,
"@peacockproject/core/candle/challengeService": challengeService,
"@peacockproject/core/candle/masteryService": masteryService,
"@peacockproject/core/candle/progressionService": progressionService,
"@peacockproject/core/contracts/contractRouting": contractRouting,
"@peacockproject/core/contracts/contractsModeRouting": contractsModeRouting,
"@peacockproject/core/contracts/dataGen": dataGen,
"@peacockproject/core/contracts/elusiveTargetArcades": elusiveTargetArcades,
"@peacockproject/core/contracts/elusiveTargets": elusiveTargets,
"@peacockproject/core/contracts/hitsCategoryService": hitsCategoryService,
"@peacockproject/core/contracts/leaderboards": leaderboards,
"@peacockproject/core/contracts/missionsInLocation": missionsInLocation,
"@peacockproject/core/contracts/sessions": sessions,
"@peacockproject/core/discord/client": client,
"@peacockproject/core/discord/discordRp": discordRp,
"@peacockproject/core/discord/ipc": ipc,
"@peacockproject/core/livesplit/liveSplitClient": liveSplitClient,
"@peacockproject/core/livesplit/liveSplitManager": liveSplitManager,
"@peacockproject/core/menus/campaigns": campaigns,
"@peacockproject/core/menus/destinations": destinations,
"@peacockproject/core/menus/favoriteContracts": favoriteContracts,
"@peacockproject/core/menus/hub": hub,
"@peacockproject/core/menus/imageHandler": imageHandler,
"@peacockproject/core/menus/menuSystem": menuSystem,
"@peacockproject/core/menus/planning": planning,
"@peacockproject/core/menus/playerProfile": playerProfile,
"@peacockproject/core/menus/playnext": playnext,
"@peacockproject/core/menus/sniper": sniper,
"@peacockproject/core/menus/stashpoints": stashpoints,
"@peacockproject/core/multiplayer/multiplayerMenuData": multiplayerMenuData,
"@peacockproject/core/multiplayer/multiplayerService": multiplayerService,
"@peacockproject/core/multiplayer/multiplayerUtils": multiplayerUtils,
"@peacockproject/core/statemachines/contextListeners": contextListeners,
"@peacockproject/core/statemachines/contractCreation": contractCreation,
"@peacockproject/core/contracts/escalations/escalationService":
escalationService,
}

View File

@ -16,15 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// load as soon as possible to prevent dependency issues
import "./generatedPeacockRequireTable"
// load flags as soon as possible
import { getFlag, loadFlags } from "./flags"
loadFlags()
import { program } from "commander"
import express, { Request, Router } from "express"
import http from "http"
import {
@ -33,7 +24,6 @@ import {
handleAxiosError,
IS_LAUNCHER,
jokes,
PEACOCKVER,
PEACOCKVERSTRING,
ServerVer,
} from "./utils"
@ -49,8 +39,7 @@ import type {
S2CEventWithTimestamp,
ServerConnectionConfig,
} from "./types/types"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
import { join } from "path"
import { readFileSync } from "fs"
import {
errorLoggingMiddleware,
log,
@ -73,7 +62,7 @@ import {
import { legacyProfileRouter } from "./2016/legacyProfileRouter"
import { legacyMenuDataRouter } from "./2016/legacyMenuData"
import { legacyContractRouter } from "./2016/legacyContractHandler"
import { initRp } from "./discordRp"
import { initRp } from "./discord/discordRp"
import random from "random"
import { generateUserCentric } from "./contracts/dataGen"
import { json as jsonMiddleware, urlencoded } from "body-parser"
@ -82,15 +71,12 @@ import { setupHotListener } from "./hotReloadService"
import type { AxiosError } from "axios"
import serveStatic from "serve-static"
import { webFeaturesRouter } from "./webFeatures"
import { toolsMenu } from "./tools"
import picocolors from "picocolors"
import { multiplayerRouter } from "./multiplayer/multiplayerService"
import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData"
import { pack, unpack } from "msgpackr"
import { liveSplitManager } from "./livesplit/liveSplitManager"
import { cheapLoadUserData } from "./databaseHandler"
loadFlags()
import { cheapLoadUserData, setupFileStructure } from "./databaseHandler"
import { getFlag } from "./flags"
const host = process.env.HOST || "0.0.0.0"
const port = process.env.PORT || 80
@ -115,6 +101,10 @@ function uncaught(error: Error): void {
LogLevel.ERROR,
` - Your user account doesn't have permission (firewall can block it)`,
)
log(
LogLevel.INFO,
`Check this wiki page: https://thepeacockproject.org/wiki/troubleshooting/fix-port-in-use for steps on how to fix this!`,
)
process.exit(1)
}
@ -317,7 +307,7 @@ app.use(
break
case "fghi4567xQOCheZIin0pazB47qGUvZw4":
case STEAM_NAMESPACE_2021:
req.serverVersion = "8-14"
req.serverVersion = "8-15"
break
default:
res.status(400).json({ message: "no game data" })
@ -508,7 +498,7 @@ app.use(
}
if (
["6-74", "7-3", "7-17", "8-14"].includes(
["6-74", "7-3", "7-17", "8-15"].includes(
<string>req.serverVersion,
)
) {
@ -527,11 +517,10 @@ app.all("*", (req, res) => {
app.use(errorLoggingMiddleware)
program.description(
"The Peacock Project is a HITMAN™ World of Assassination Trilogy server replacement.",
)
function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
export async function startServer(options: {
hmr: boolean
pluginDevHost: boolean
}): Promise<void> {
void checkForUpdates()
if (!IS_LAUNCHER) {
@ -551,10 +540,9 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
log(
LogLevel.INFO,
`This is Peacock v${PEACOCKVERSTRING} (rev ${PEACOCKVER}), with Node v${process.versions.node}.`,
`This is Peacock v${PEACOCKVERSTRING} with Node v${process.versions.node}.`,
)
// jokes lol
if (getFlag("jokes") === true) {
log(
LogLevel.INFO,
@ -564,117 +552,40 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
)
}
// make sure required folder structure is in place
for (const dir of [
"contractSessions",
"plugins",
"userdata",
"contracts",
join("userdata", "epicids"),
join("userdata", "steamids"),
join("userdata", "users"),
join("userdata", "h1", "steamids"),
join("userdata", "h1", "epicids"),
join("userdata", "h1", "users"),
join("userdata", "h2", "steamids"),
join("userdata", "h2", "users"),
join("userdata", "scpc", "users"),
join("userdata", "scpc", "steamids"),
join("images", "actors"),
join("images", "contracts"),
join("images", "contracts", "elusive"),
join("images", "contracts", "escalation"),
join("images", "contracts", "featured"),
join("images", "unlockables_override"),
]) {
if (existsSync(dir)) {
continue
try {
// make sure required folder structure is in place
await setupFileStructure()
if (options.hmr) {
void setupHotListener("contracts", () => {
log(
LogLevel.INFO,
"Detected a change in contracts! Re-indexing...",
)
controller.index()
})
}
log(LogLevel.DEBUG, `Creating missing directory ${dir}`)
mkdirSync(dir, { recursive: true })
// once contracts directory is present, we are clear to boot
await loadouts.init()
await controller.boot(options.pluginDevHost)
const httpServer = http.createServer(app)
// @ts-expect-error Non-matching method sig
httpServer.listen(port, host)
log(LogLevel.INFO, "Server started.")
if (getFlag("discordRp") === true) {
initRp()
}
// initialize livesplit
await liveSplitManager.init()
return
} catch (e) {
log(LogLevel.ERROR, "Critical error during bootstrap!")
log(LogLevel.ERROR, e)
}
if (options.hmr) {
log(LogLevel.DEBUG, "Experimental HMR enabled.")
void setupHotListener("contracts", () => {
log(LogLevel.INFO, "Detected a change in contracts! Re-indexing...")
controller.index()
})
}
// once contracts directory is present, we are clear to boot
loadouts.init()
void controller.boot(options.pluginDevHost)
const httpServer = http.createServer(app)
// @ts-expect-error Non-matching method sig
httpServer.listen(port, host)
log(LogLevel.INFO, "Server started.")
if (getFlag("discordRp") === true) {
initRp()
}
// initialize livesplit
void liveSplitManager.init()
}
program.option(
"--hmr",
"enable experimental hot reloading of contracts",
getFlag("experimentalHMR") as boolean,
)
program.option(
"--plugin-dev-host",
"activate plugin development features - requires plugin dev workspace setup",
getFlag("developmentPluginDevHost") as boolean,
)
program.action(startServer)
program
.command("tools")
.description("open the tools UI")
.action(() => {
void toolsMenu()
})
// noinspection RequiredAttributes
program
.command("pack")
.argument("<input>", "input file to pack")
.option("-o, --output <path>", "where to output the packed file to", "")
.description("packs an input file into a Challenge Resource Package")
.action((input, options: { output: string }) => {
const outputPath =
options.output || input.replace(/\.[^/\\.]+$/, ".crp")
writeFileSync(
outputPath,
pack(JSON.parse(readFileSync(input).toString())),
)
log(LogLevel.INFO, `Packed "${input}" to "${outputPath}" successfully.`)
})
// noinspection RequiredAttributes
program
.command("unpack")
.argument("<input>", "input file to unpack")
.option("-o, --output <path>", "where to output the unpacked file to", "")
.description("unpacks a Challenge Resource Package")
.action((input, options: { output: string }) => {
const outputPath =
options.output || input.replace(/\.[^/\\.]+$/, ".json")
writeFileSync(outputPath, JSON.stringify(unpack(readFileSync(input))))
log(
LogLevel.INFO,
`Unpacked "${input}" to "${outputPath}" successfully.`,
)
})
program.parse(process.argv)

View File

@ -27,6 +27,7 @@ import {
H1_REQUIEM_UNLOCKABLES,
H2_RACCOON_STINGRAY_UNLOCKABLES,
MAKESHIFT_UNLOCKABLES,
SAMBUCA_UNLOCKABLES,
SIN_ENVY_UNLOCKABLES,
SIN_GLUTTONY_UNLOCKABLES,
SIN_GREED_UNLOCKABLES,
@ -34,6 +35,7 @@ import {
SIN_PRIDE_UNLOCKABLES,
SIN_SLOTH_UNLOCKABLES,
SIN_WRATH_UNLOCKABLES,
SMART_CASUAL_UNLOCKABLES,
TRINITY_UNLOCKABLES,
WINTERSPORTS_UNLOCKABLES,
} from "./ownership"
@ -63,6 +65,7 @@ const DELUXE_DATA = [
...SIN_WRATH_UNLOCKABLES,
...TRINITY_UNLOCKABLES,
...WINTERSPORTS_UNLOCKABLES,
...SAMBUCA_UNLOCKABLES,
]
/**
@ -250,6 +253,15 @@ function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
)
}
if (SMART_CASUAL_UNLOCKABLES.includes(id)) {
return (
e.includes("6408de14f7dc46b9a33adcf6cbc4d159") ||
e.includes("afa4b921503f43339c360d4b53910791") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829590")
)
}
if (H1_REQUIEM_UNLOCKABLES.includes(id)) {
return (
e.includes("e698e1a4b63947b0bc9349a5ae2dc015") ||
@ -301,16 +313,6 @@ function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
)
}
/*
TODO: Fix this entitlement check (confirmed its broken with Blazer)
if (LEGACY_UNLOCKABLES.includes(id)) {
return (
e.includes("0b59243cb8aa420691b66be1ecbe68c0") ||
e.includes("1829593")
)
}
*/
if (SIN_GREED_UNLOCKABLES.includes(id)) {
return (
e.includes("0e8632b4cdfb415e94291d97d727b98d") ||
@ -374,7 +376,6 @@ function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
)
}
// The following two must be confirmed, epic entitlements may be in the wrong order! - AF
if (MAKESHIFT_UNLOCKABLES.includes(id)) {
return (
e.includes("08d2bc4d20754191b6c488541d2b4fa1") ||
@ -389,6 +390,13 @@ function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
)
}
if (SAMBUCA_UNLOCKABLES.includes(id)) {
return (
e.includes("9220c020262f420da06eb46a4b1ce86f") ||
e.includes("2828470")
)
}
return true
}
}

View File

@ -19,10 +19,10 @@
import { LiveSplitClient, LiveSplitResult } from "./liveSplitClient"
import { log, LogLevel } from "../loggingInterop"
import { getAllCampaigns } from "../menus/campaigns"
import { Campaign, GameVersion, IHit, Seconds, StoryData } from "../types/types"
import { Campaign, GameVersion, Hit, Seconds, StoryData } from "../types/types"
import { getFlag } from "../flags"
import { controller } from "../controller"
import { scenePathToRpAsset } from "../discordRp"
import { scenePathToRpAsset } from "../discord/discordRp"
import { LiveSplitTimeCalcEntry } from "../types/livesplit"
import assert from "assert"
@ -593,7 +593,7 @@ function getCampaignMissions(
const campaignMissionIds = (campaignStoryData: StoryData[]): string[] => {
return campaignStoryData
.filter((data) => data.Type === "Mission")
.map((sd) => (sd.Data as IHit).Id)
.map((sd) => (sd.Data as Hit).Id)
}
// the trilogy is the only place where multiple campaigns are merged together

View File

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { existsSync, readFileSync, writeFileSync } from "fs"
import type {
GameVersion,
Loadout,
@ -25,9 +24,10 @@ import type {
} from "./types/types"
import { Request, Router } from "express"
import { json as jsonMiddleware } from "body-parser"
import { writeFile } from "atomically"
import { writeFile } from "fs/promises"
import { nanoid } from "nanoid"
import { versions } from "./utils"
import { asyncGuard } from "./databaseHandler"
const LOADOUT_PROFILES_FILE = "userdata/users/lop.json"
@ -50,47 +50,33 @@ const defaultValue: LoadoutFile = {
* A class for managing loadouts.
*/
export class Loadouts {
private _loadouts!: LoadoutFile
/**
* Get the loadouts data.
*
* @returns The loadouts data.
*/
public get loadouts(): LoadoutFile {
return this._loadouts
}
/**
* Mutate the current LoadoutFile object.
*
* @internal Intended for internal use only.
* @param newValue The object after the mutation.
*/
set loadouts(newValue: LoadoutFile) {
this._loadouts = newValue
}
loadouts!: LoadoutFile
/**
* Initializes the loadouts manager.
*/
public init(): void {
if (!existsSync(LOADOUT_PROFILES_FILE)) {
this._loadouts = defaultValue
async init(): Promise<void> {
const fs = asyncGuard.getFs()
writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(defaultValue))
if (!(await fs.exists(LOADOUT_PROFILES_FILE))) {
this.loadouts = defaultValue
await fs.writeFile(
LOADOUT_PROFILES_FILE,
JSON.stringify(defaultValue),
)
return
}
this._loadouts = JSON.parse(
readFileSync(LOADOUT_PROFILES_FILE).toString(),
this.loadouts = JSON.parse(
(await fs.readFile(LOADOUT_PROFILES_FILE)).toString(),
)
let dirty = false
// make sure they all have IDs
for (const gameVersion of versions) {
for (const loadout of this._loadouts[gameVersion].loadouts) {
for (const loadout of this.loadouts[gameVersion].loadouts) {
if (!loadout.id) {
dirty = true
loadout.id = nanoid()
@ -99,26 +85,28 @@ export class Loadouts {
// if the selected value is null/undefined or is not length 0 or 21, it's not a valid id
if (
!this._loadouts[gameVersion].selected ||
!this.loadouts[gameVersion].selected ||
// first condition ensures selected is truthy, but TS doesn't know
![0, 21].includes(
this._loadouts[gameVersion].selected?.length || -1,
this.loadouts[gameVersion].selected?.length || -1,
)
) {
dirty = true
// long story short: find a loadout with a name matching the selected value,
// and if found, set selected to the id
this._loadouts[gameVersion].selected =
this._loadouts[gameVersion].loadouts.find(
(lo) =>
lo.name === this._loadouts[gameVersion].selected,
this.loadouts[gameVersion].selected =
this.loadouts[gameVersion].loadouts.find(
(lo) => lo.name === this.loadouts[gameVersion].selected,
)?.id || ""
}
}
if (dirty) {
writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts))
await writeFile(
LOADOUT_PROFILES_FILE,
JSON.stringify(this.loadouts),
)
}
}
@ -129,7 +117,7 @@ export class Loadouts {
* @param name The optional name for the new loadout set, defaults to "Unnamed loadout set".
* @returns The Loadout object.
*/
public createDefault(
createDefault(
gameVersion: GameVersion,
name = "Unnamed loadout set",
): Loadout {
@ -143,8 +131,8 @@ export class Loadouts {
data: {},
}
this._loadouts[gameVersion].loadouts.push(l)
this._loadouts[gameVersion].selected = l.id
this.loadouts[gameVersion].loadouts.push(l)
this.loadouts[gameVersion].selected = l.id
return l
}
@ -155,12 +143,12 @@ export class Loadouts {
* @param gameVersion The game version.
* @returns The loadout profile or undefined if one isn't selected or none exist.
*/
public getLoadoutFor(gameVersion: GameVersion): Loadout | undefined {
getLoadoutFor(gameVersion: GameVersion): Loadout | undefined {
if (gameVersion === "scpc") {
gameVersion = "h1"
}
const theLoadouts = this._loadouts[gameVersion] as LoadoutsGameVersion
const theLoadouts = this.loadouts[gameVersion] as LoadoutsGameVersion
return theLoadouts.loadouts.find((s) => s.id === theLoadouts.selected)
}
@ -168,12 +156,13 @@ export class Loadouts {
* Saves the loadout data to the Peacock userdata/users folder.
*/
public async save(): Promise<void> {
await writeFile(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts))
await writeFile(LOADOUT_PROFILES_FILE, JSON.stringify(this.loadouts))
}
}
/**
* A synthetic default bind to the global Loadouts instance.
* @todo Move this somewhere that makes more sense with a dependency injection model.
*/
export const loadouts = new Loadouts()

View File

@ -95,28 +95,46 @@ import { getPlayerProfileData } from "./menus/playerProfile"
const menuDataRouter = Router()
// We make this lookup table to quickly get it, there's no other quick way for it.
export const SNIPER_UNLOCK_TO_LOCATION: Record<string, string> = {
FIREARMS_SC_HERO_SNIPER_HM: "LOCATION_PARENT_AUSTRIA",
FIREARMS_SC_HERO_SNIPER_KNIGHT: "LOCATION_PARENT_AUSTRIA",
FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA",
FIREARMS_SC_SEAGULL_HM: "LOCATION_PARENT_SALTY",
FIREARMS_SC_SEAGULL_KNIGHT: "LOCATION_PARENT_SALTY",
FIREARMS_SC_SEAGULL_STONE: "LOCATION_PARENT_SALTY",
FIREARMS_SC_FALCON_HM: "LOCATION_PARENT_CAGED",
FIREARMS_SC_FALCON_KNIGHT: "LOCATION_PARENT_CAGED",
FIREARMS_SC_FALCON_STONE: "LOCATION_PARENT_CAGED",
}
// /profiles/page/
menuDataRouter.get(
"/ChallengeLocation",
// @ts-expect-error Jwt props.
(req: RequestWithJwt<ChallengeLocationQuery>, res) => {
const pack = controller.challengeService.challengePacks.get(
req.query.locationId,
)
const location = getVersionedConfig<PeacockLocationsData>(
"LocationsData",
req.gameVersion,
true,
).children[req.query.locationId]
if (!location) {
if (!pack && !location) {
res.status(400).send("Invalid locationId")
return
}
const data = {
Name: location.DisplayNameLocKey,
Name: pack ? pack.Name : location.DisplayNameLocKey,
Location: location,
Children: controller.challengeService.getChallengeDataForLocation(
req.query.locationId,
Children: controller.challengeService.getChallengeDataForCategory(
pack ? req.query.locationId : null,
pack ? undefined : location,
req.gameVersion,
req.jwt.unique_name,
),
@ -1395,25 +1413,12 @@ menuDataRouter.get(
"/GetMasteryCompletionDataForUnlockable",
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<GetMasteryCompletionDataForUnlockableQuery>, res) => {
// We make this lookup table to quickly get it, there's no other quick way for it.
const unlockToLoc: Record<string, string> = {
FIREARMS_SC_HERO_SNIPER_HM: "LOCATION_PARENT_AUSTRIA",
FIREARMS_SC_HERO_SNIPER_KNIGHT: "LOCATION_PARENT_AUSTRIA",
FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA",
FIREARMS_SC_SEAGULL_HM: "LOCATION_PARENT_SALTY",
FIREARMS_SC_SEAGULL_KNIGHT: "LOCATION_PARENT_SALTY",
FIREARMS_SC_SEAGULL_STONE: "LOCATION_PARENT_SALTY",
FIREARMS_SC_FALCON_HM: "LOCATION_PARENT_CAGED",
FIREARMS_SC_FALCON_KNIGHT: "LOCATION_PARENT_CAGED",
FIREARMS_SC_FALCON_STONE: "LOCATION_PARENT_CAGED",
}
res.json({
template: null,
data: {
CompletionData: controller.masteryService.getLocationCompletion(
unlockToLoc[req.query.unlockableId],
unlockToLoc[req.query.unlockableId],
SNIPER_UNLOCK_TO_LOCATION[req.query.unlockableId],
SNIPER_UNLOCK_TO_LOCATION[req.query.unlockableId],
req.gameVersion,
req.jwt.unique_name,
"sniper",

View File

@ -23,7 +23,7 @@ import type {
GenSingleMissionFunc,
CampaignMission,
CampaignVideo,
IVideo,
Video,
StoryData,
} from "../types/types"
import { log, LogLevel } from "../loggingInterop"
@ -65,7 +65,7 @@ function genSingleVideo(
videoId: string,
gameVersion: GameVersion,
): CampaignVideo {
const videos = getConfig<Record<string, IVideo>>("Videos", true) // we modify videos so we need to clone this
const videos = getConfig<Record<string, Video>>("Videos", true) // we modify videos so we need to clone this
const video = videos[videoId]
switch (gameVersion) {

View File

@ -23,7 +23,7 @@ import type {
CompletionData,
GameLocationsData,
GameVersion,
IHit,
Hit,
MissionStory,
OpportunityStatistics,
PeacockLocationsData,
@ -68,13 +68,13 @@ type GameFacingDestination = {
type LocationMissionData = {
Location: Unlockable
SubLocation: Unlockable
Missions: IHit[]
SarajevoSixMissions: IHit[]
ElusiveMissions: IHit[]
EscalationMissions: IHit[]
SniperMissions: IHit[]
PlaceholderMissions: IHit[]
CampaignMissions: IHit[]
Missions: Hit[]
SarajevoSixMissions: Hit[]
ElusiveMissions: Hit[]
EscalationMissions: Hit[]
SniperMissions: Hit[]
PlaceholderMissions: Hit[]
CampaignMissions: Hit[]
CompletionData: CompletionData
}
@ -409,9 +409,7 @@ export function getDestination(
}
}
if (PEACOCK_DEV) {
log(LogLevel.DEBUG, `Looking up locations details for ${LOCATION}.`)
}
log(LogLevel.DEBUG, `Looking up locations details for ${LOCATION}.`)
const sublocationsData = Object.values(locData.children).filter(
(subLocation) => subLocation.Properties.ParentLocation === LOCATION,
@ -425,7 +423,7 @@ export function getDestination(
SubLocation: locationData,
Missions: [controller.missionsInLocations.pro1[LOCATION as Cast]]
.map((id) => contractIdToHitObject(id, gameVersion, userId))
.filter(Boolean) as IHit[],
.filter(Boolean) as Hit[],
SarajevoSixMissions: [],
ElusiveMissions: [],
EscalationMissions: [],
@ -447,7 +445,7 @@ export function getDestination(
for (const e of sublocationsData) {
log(LogLevel.DEBUG, `Looking up sublocation details for ${e.Id}`)
const escalations: IHit[] = []
const escalations: Hit[] = []
type ECast = keyof typeof controller.missionsInLocations.escalations
// every unique escalation from the sublocation
@ -476,7 +474,7 @@ export function getDestination(
}
}
const sniperMissions: IHit[] = []
const sniperMissions: Hit[] = []
type SCast = keyof typeof controller.missionsInLocations.sniper
for (const sniperMission of controller.missionsInLocations.sniper[

View File

@ -17,12 +17,13 @@
*/
import type {
ChallengeCompletion,
CompletionData,
GameVersion,
PeacockLocationsData,
Unlockable,
} from "../types/types"
import { swapToBrowsingMenusStatus } from "../discordRp"
import { swapToBrowsingMenusStatus } from "../discord/discordRp"
import { getUserData } from "../databaseHandler"
import { controller } from "../controller"
import { contractCreationTutorialId, getMaxProfileLevel } from "../utils"
@ -53,7 +54,32 @@ type CareerEntryChild = {
ImageLocked: string
RequiredResources: string[]
IsPack?: boolean
CompletionData: CompletionData
CompletionData: CompletionData | Record<string, never>
}
function generateCareerEntryChild(
location: Unlockable,
completion: ChallengeCompletion,
categoryId: string,
completionData?: CompletionData,
): CareerEntryChild {
const pack = controller.challengeService.challengePacks.get(categoryId)
return {
IsLocked: Boolean(location.Properties.IsLocked),
Name: pack ? pack.Name : location.DisplayNameLocKey,
Image: pack ? pack.Image : location.Properties.Icon || "",
Icon: pack ? pack.Icon : location.Type,
CompletedChallengesCount: completion.CompletedChallengesCount,
ChallengesCount: completion.ChallengesCount,
CategoryId: categoryId,
Description: pack ? pack.Description : `UI_${categoryId}_PRIMARY_DESC`,
Location: location,
ImageLocked: location.Properties.LockedIcon || "",
RequiredResources: location.Properties.RequiredResources || [],
IsPack: pack !== undefined,
CompletionData: completionData || {},
}
}
export function getHubData(gameVersion: GameVersion, userId: string) {
@ -71,18 +97,34 @@ export function getHubData(gameVersion: GameVersion, userId: string) {
gameVersion,
true,
)
const career: Record<string, CareerEntry> =
gameVersion === "h3"
? {}
: {
// TODO: Add data on elusive challenges. They are only shown on the Career->Challenges page for H1 and H2. They are not supported by Peacock as of v6.0.0.
ELUSIVES_UNSUPPORTED: {
Children: [],
Name: "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
Location:
locations.parents["LOCATION_PARENT_ICA_FACILITY"],
},
}
const career: Record<string, CareerEntry> = {}
for (const [
id,
pack,
] of controller.challengeService.challengePacks.entries()) {
if (!pack.GameVersions.includes(gameVersion)) continue
career[id] = {
Children: [
generateCareerEntryChild(
locations.parents["LOCATION_PARENT_ICA_FACILITY"],
controller.challengeService.countTotalNCompletedChallenges(
controller.challengeService.getChallengesForGroup(
id,
gameVersion,
),
userId,
gameVersion,
),
id,
),
],
Name: pack.Name,
Location: locations.parents["LOCATION_PARENT_ICA_FACILITY"],
}
}
const masteryData = []
@ -168,22 +210,14 @@ export function getHubData(gameVersion: GameVersion, userId: string) {
gameVersion,
)
career[parent!]?.Children.push({
IsLocked: Boolean(location.Properties.IsLocked),
Name: location.DisplayNameLocKey,
Image: location.Properties.Icon || "",
Icon: location.Type, // should be "location" for all locations
CompletedChallengesCount:
challengeCompletion.CompletedChallengesCount,
ChallengesCount: challengeCompletion.ChallengesCount,
CategoryId: child,
Description: `UI_${child}_PRIMARY_DESC`,
Location: location,
ImageLocked: location.Properties.LockedIcon || "",
RequiredResources: location.Properties.RequiredResources || [],
IsPack: false, // should be false for all locations
CompletionData: generateCompletionData(child, userId, gameVersion),
})
career[parent!]?.Children.push(
generateCareerEntryChild(
location,
challengeCompletion,
child,
generateCompletionData(child, userId, gameVersion),
),
)
}
return {

View File

@ -21,7 +21,7 @@ import serveStatic from "serve-static"
import { join } from "path"
import md5File from "md5-file"
import { getConfig } from "../configSwizzleManager"
import { readFile } from "atomically"
import { readFile } from "fs/promises"
import { GameVersion, RequestWithJwt } from "../types/types"
import { log, LogLevel } from "../loggingInterop"
import { imageFetchingMiddleware } from "./imageHandler"
@ -33,7 +33,7 @@ import { SyncBailHook, SyncHook } from "../hooksImpl"
const menuSystemPreRouter = Router()
const menuSystemRouter = Router()
// /resources-8-14/
// /resources-8-15/
/**
* A class for managing the menu system's fetched JSON data.

View File

@ -154,6 +154,8 @@ export async function getPlanningData(
const escalationGroupId =
contractData.Metadata.InGroup ?? contractData.Metadata.Id
controller.hooks.onEscalationReset.call(escalationGroupId)
resetUserEscalationProgress(userData, escalationGroupId)
writeUserData(userId, gameVersion)

View File

@ -30,6 +30,13 @@ import { getDestinationCompletion } from "./destinations"
import { getUserData } from "../databaseHandler"
import { isSniperLocation } from "../utils"
type XpData = {
[location: string]: {
Xp: number
ActionXp: number
}
}
export function getPlayerProfileData(
gameVersion: GameVersion,
userId: string,
@ -47,7 +54,27 @@ export function getPlayerProfileData(
playerProfilePage.SubLocationData = []
const userProfile = getUserData(userId, gameVersion)
const subLocationMap =
userProfile.Extensions.progression.PlayerProfileXP.Sublocations
const xpData: XpData = {}
for (const subLocationKey in locationData.children) {
const subLocation = locationData.children[subLocationKey]
const parentLocation =
locationData.parents[subLocation.Properties.ParentLocation || ""]
// We find all sublocations and add their XP for the season data
const subLocationData = subLocationMap[subLocationKey]
xpData[parentLocation.Id] ??= {
Xp: 0,
ActionXp: 0,
}
xpData[parentLocation.Id].Xp += subLocationData?.Xp ?? 0
xpData[parentLocation.Id].ActionXp += subLocationData?.ActionXp ?? 0
// Ewww...
if (
subLocationKey === "LOCATION_ICA_FACILITY_ARRIVAL" ||
@ -56,10 +83,6 @@ export function getPlayerProfileData(
continue
}
const subLocation = locationData.children[subLocationKey]
const parentLocation =
locationData.parents[subLocation.Properties.ParentLocation || ""]
const completionData = generateCompletionData(
subLocation.Id,
userId,
@ -109,24 +132,17 @@ export function getPlayerProfileData(
})
}
const userProfile = getUserData(userId, gameVersion)
playerProfilePage.PlayerProfileXp.Total =
userProfile.Extensions.progression.PlayerProfileXP.Total
playerProfilePage.PlayerProfileXp.Level =
userProfile.Extensions.progression.PlayerProfileXP.ProfileLevel
const subLocationMap = new Map(
userProfile.Extensions.progression.PlayerProfileXP.Sublocations.map(
(obj) => [obj.Location, obj],
),
)
for (const season of playerProfilePage.PlayerProfileXp.Seasons) {
for (const location of season.Locations) {
const subLocationData = subLocationMap.get(location.LocationId)
const locationData = xpData[location.LocationId]
location.Xp = subLocationData?.Xp || 0
location.ActionXp = subLocationData?.ActionXp || 0
location.Xp = locationData?.Xp || 0
location.ActionXp = locationData?.ActionXp || 0
if (
location.LocationProgression &&

View File

@ -26,7 +26,6 @@ import type { GameVersion } from "./types/types"
/**
* Creates the body for the authentication request (urlencoded format).
*
* @param params The parameters object.
* @returns The urlencoded body.
*/
@ -46,7 +45,7 @@ function createUrlencodedBody(params: Record<string, string>): string {
const requestHeadersH3 = {
"User-agent": "G2 Http/1.0 (Windows NT 10.0; DX12/1; d3d12/1)",
Version: "8.14.0",
Version: "8.15.0",
}
const requestHeadersH2 = {
@ -79,24 +78,24 @@ export class OfficialServerAuth {
/**
* Kick things off.
*
* @param gameVersion The game version.
* @param gameAuthToken The token for the 3rd party game provider (steam or epic).
* @param gameAuthToken The token for the 3rd party game provider (Steam or Epic).
*/
constructor(gameVersion: GameVersion, gameAuthToken: string) {
this._gameAuthToken = gameAuthToken
this._headers =
gameVersion === "h1"
? requestHeadersH1
: gameVersion === "h2"
? requestHeadersH2
: requestHeadersH3
this._headers = requestHeadersH1
if (gameVersion === "h2") {
this._headers = requestHeadersH2
} else if (gameVersion === "h3") {
this._headers = requestHeadersH3
}
this.initialized = false
}
/**
* Authenticates the client with the official service the first time.
*
* @param req The initial client request.
*/
async _initiallyAuthenticate(req: Request): Promise<void> {
@ -119,7 +118,6 @@ export class OfficialServerAuth {
/**
* Makes a request with the required context.
*
* @param url The URL to fetch.
* @param get If the request should be a GET (true), or POST (false).
* @param body The request's body (defaults to {}).
@ -181,7 +179,6 @@ export class OfficialServerAuth {
/**
* Authenticate for the first time.
*
* @param req The request from the Hitman client connecting to Peacock.
* @returns The token data fetched from the official servers.
*/

View File

@ -170,6 +170,12 @@ export const EXECUTIVE_UNLOCKABLES = [
"PROP_CONTAINER_SUITCASE_ICA_DELUXE",
]
export const SMART_CASUAL_UNLOCKABLES = [
"TOKEN_OUTFIT_URBAN_CLASSIC",
"PROP_DEVICE_ICA_REMOTE_FLASH_PHONE",
"PROP_MELEE_BLACK_PHONE_CORD",
]
export const DELUXE_UNLOCKABLES = [
// dubai
"PROP_CONTAINER_SUITCASE_GOLDEN",
@ -271,6 +277,13 @@ export const CONCRETEART_UNLOCKABLES = [
"TOKEN_OUTFIT_HERO_CONCRETEART",
]
export const SAMBUCA_UNLOCKABLES = [
"TOKEN_OUTFIT_REWARD_HERO_SB_PATCH_SUIT",
"PROP_EXPLOSIVE_PEN_SAMBUCA",
"PROP_GADGET_ROBOT_FLASH_SAMBUCA",
"PROP_MELEE_BLACK_PHONE_CORD_SAMBUCA",
]
export const brokenItems = [
// duped dart gun (thanks IOI)
"835ad050-6d19-4e94-80b1-f5cec9815ba3",

View File

@ -81,6 +81,8 @@ export const H3_EPIC_ENTITLEMENTS = [
"a1e9a63fa4f3425aa66b9b8fa3c9cc35",
// THESARAJEVOSIX:
"28455871cd0d4ffab52f557cc012ea5e",
// SAMBUCA:
"9220c020262f420da06eb46a4b1ce86f",
]
export const H2_STEAM_ENTITLEMENTS = [
@ -138,9 +140,7 @@ export function getPlatformEntitlements(
req: RequestWithJwt,
res: Response,
): void {
if (PEACOCK_DEV) {
log(LogLevel.DEBUG, `Platform issuer: ${req.body.issuerId}`)
}
log(LogLevel.DEBUG, `Platform issuer: ${req.body.issuerId}`)
const exts = getUserData(req.jwt.unique_name, req.gameVersion).Extensions
.entP

View File

@ -58,7 +58,11 @@ import {
compileRuntimeChallenge,
inclusionDataCheck,
} from "./candle/challengeHelpers"
import { LoadSaveBody, ResolveGamerTagsBody } from "./types/gameSchemas"
import {
GetChallengeProgressionBody,
LoadSaveBody,
ResolveGamerTagsBody,
} from "./types/gameSchemas"
import assert from "assert"
const profileRouter = Router()
@ -285,7 +289,6 @@ export async function resolveProfiles(
Gamertag: null,
DevId: "IOI",
SteamId: null,
StadiaId: null,
EpicId: null,
NintendoId: null,
XboxLiveId: null,
@ -312,7 +315,6 @@ export async function resolveProfiles(
fakePlayer.platform === "steam"
? fakePlayer.platformId
: null,
StadiaId: null,
EpicId:
fakePlayer.platform === "epic"
? fakePlayer.platformId
@ -579,6 +581,54 @@ profileRouter.post(
},
)
profileRouter.post(
"/ChallengesService/GetProgression",
jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<never, GetChallengeProgressionBody>, res) => {
if (!Array.isArray(req.body.challengeids)) {
res.status(400).send("invalid body")
return
}
if (req.jwt.unique_name !== req.body.profileid) {
res.status(403).send("unauthorised")
return
}
const challenges: ChallengeProgressionData[] = []
for (const challengeId of req.body.challengeids) {
const challenge = controller.challengeService.getChallengeById(
challengeId,
req.gameVersion,
)
if (!challenge) {
log(LogLevel.ERROR, `Unknown challenge in CSGP: ${challengeId}`)
continue
}
const progression =
controller.challengeService.getPersistentChallengeProgression(
req.jwt.unique_name,
challengeId,
req.gameVersion,
)
challenges.push({
ChallengeId: challengeId,
ProfileId: req.jwt.unique_name,
State: progression.State,
CompletedAt: progression.CompletedAt,
Completed: progression.Completed,
})
}
res.json(challenges)
},
)
profileRouter.post(
"/HubPagesService/GetChallengeTreeFor",
jsonMiddleware(),
@ -732,13 +782,6 @@ profileRouter.post(
"No such save detected! Might be an official servers save.",
)
if (PEACOCK_DEV) {
log(
LogLevel.DEBUG,
`(Save-context: ${req.body.contractSessionId}; ${req.body.saveToken})`,
)
}
log(
LogLevel.WARN,
"Creating a fake session to avoid problems... scoring will not work!",

View File

@ -71,7 +71,7 @@ import {
MissionEndResult,
} from "./types/score"
import { MasteryData } from "./types/mastery"
import { createInventory, InventoryItem, getUnlockablesById } from "./inventory"
import { createInventory, getUnlockablesById, InventoryItem } from "./inventory"
import { calculatePlaystyle } from "./playStyles"
import assert from "assert"
@ -132,6 +132,9 @@ export function calculateScore(
contractData: MissionManifest,
timeTotal: Seconds,
): CalculateScoreResult {
const noticedKillsAreVanilla =
getFlag("legacyNoticedKillScoring") === "vanilla"
// Bonuses
const bonuses = [
{
@ -173,13 +176,14 @@ export function calculateScore(
{
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),
),
condition:
gameVersion === "h1" && noticedKillsAreVanilla
? contractSession.lastKill.legacyIsUnnoticed
: [...contractSession.killsNoticedBy].every(
(witness) =>
contractSession.targetKills.has(witness) ||
contractSession.npcKills.has(witness),
),
},
{
headline: "UI_SCORING_SUMMARY_NO_BODIES_FOUND",

View File

@ -16,19 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { MissionManifestObjective } from "../types/types"
import type { MissionManifestObjective, RepositoryId } from "../types/types"
import { randomUUID } from "crypto"
import assert from "assert"
/**
* The payload provided to the game for each target in the create menu route.
*/
export interface IContractCreationPayload {
export type ContractCreationNpcTargetPayload = {
RepositoryId: string
Selected: boolean
Weapon: {
RepositoryId: string
KillMethodBroad: string
KillMethodStrict: string
RequiredKillMethod: string
RequiredKillMethodType: number
}
Outfit: {
@ -39,169 +41,217 @@ export interface IContractCreationPayload {
}
/**
* The target creator API.
* @internal
*/
export class TargetCreator {
// @ts-expect-error TODO: type this
private _targetSm
// @ts-expect-error TODO: type this
private _outfitSm
private _targetConds: undefined | unknown[] = undefined
/**
* The constructor.
*
* @param _params The contract creation payload.
*/
public constructor(private readonly _params: IContractCreationPayload) {}
private _requireTargetConds(): void {
if (!this._targetConds || !Array.isArray(this._targetConds)) {
this._targetConds = []
}
}
private _bootstrapEntrySm(): void {
this._targetSm = {
Type: "statemachine",
Id: randomUUID(),
BriefingText: {
$loc: {
key: "UI_CONTRACT_GENERAL_OBJ_KILL",
data: `$($repository ${this._params.RepositoryId}).Name`,
},
export const createTargetKillObjective = (
params: ContractCreationNpcTargetPayload,
): MissionManifestObjective => ({
Type: "statemachine",
Id: randomUUID(),
BriefingText: {
$loc: {
key: "UI_CONTRACT_GENERAL_OBJ_KILL",
data: `$($repository ${params.RepositoryId}).Name`,
},
},
HUDTemplate: {
display: {
$loc: {
key: "UI_CONTRACT_GENERAL_OBJ_KILL",
data: `$($repository ${params.RepositoryId}).Name`,
},
HUDTemplate: {
display: {
$loc: {
key: "UI_CONTRACT_GENERAL_OBJ_KILL",
data: `$($repository ${this._params.RepositoryId}).Name`,
},
},
Category: "primary",
Definition: {
Scope: "Hit",
Context: {
Targets: [params.RepositoryId],
},
States: {
Start: {
Kill: {
Condition: {
$eq: ["$Value.RepositoryId", params.RepositoryId],
},
Transition: "Success",
},
},
Category: "primary",
Definition: {
Scope: "Hit",
Context: {
Targets: [this._params.RepositoryId],
},
States: {
Start: {
Kill: [
// this is the base state machine, we don't have fail states
{
Condition: {
},
},
})
/**
* @internal
*/
export const createRequiredOutfitObjective = (
params: ContractCreationNpcTargetPayload,
): MissionManifestObjective => ({
Type: "statemachine",
Id: randomUUID(),
Category: "secondary",
Definition: {
Scope: "Hit",
Context: {
Targets: [params.RepositoryId],
},
States: {
Start: {
Kill: [
{
Condition: {
$and: [
{
$eq: [
"$Value.RepositoryId",
this._params.RepositoryId,
params.RepositoryId,
],
},
Transition: "Success",
},
],
params.Outfit.IsHitmanSuit
? {
$eq: [
"$Value.OutfitIsHitmanSuit",
true,
],
}
: {
$eq: [
"$Value.OutfitRepositoryId",
params.Outfit.RepositoryId,
],
},
],
},
Transition: "Success",
},
},
// state machines fall through to the next state if the condition is not met
{
Condition: {
$eq: ["$Value.RepositoryId", params.RepositoryId],
},
Transition: "Failure",
},
],
},
}
},
},
TargetConditions: [],
})
/**
* Create the target, weapon, and kill conditions for a contracts target.
* @param params The parameters from the request.
* @param customIds Custom objective IDs for testing purposes.
*/
export function createObjectivesForTarget(
params: ContractCreationNpcTargetPayload,
customIds?: { base: string; kill: string; outfit: string },
): MissionManifestObjective[] {
const targetSm = createTargetKillObjective(params)
if (customIds?.base) {
targetSm.Id = customIds.base
}
private _createOutfitSm(hmSuit: boolean): void {
this._requireTargetConds()
const objectives: MissionManifestObjective[] = [targetSm]
this._outfitSm = {
Type: "statemachine",
Id: randomUUID(),
Category: "secondary",
Definition: {
Scope: "Hit",
Context: {
Targets: [this._params.RepositoryId],
},
States: {
Start: {
Kill: [
{
Condition: {
$and: [
{
$eq: [
"$Value.RepositoryId",
this._params.RepositoryId,
],
},
hmSuit
? {
$eq: [
"$Value.OutfitIsHitmanSuit",
true,
],
}
: {
$eq: [
"$Value.OutfitRepositoryId",
this._params.Outfit
.RepositoryId,
],
},
],
},
Transition: "Success",
},
{
Condition: {
$eq: [
"$Value.RepositoryId",
this._params.RepositoryId,
],
},
Transition: "Failure",
},
],
},
},
},
// if the required field is true, that means the user requested something OTHER than any disguise
if (params.Outfit.Required) {
const outfitSm = createRequiredOutfitObjective(params)
if (customIds?.outfit) {
outfitSm.Id = customIds.outfit
}
this._targetConds?.push({
Type: hmSuit ? "hitmansuit" : "disguise",
RepositoryId: this._params.Outfit.RepositoryId,
targetSm.TargetConditions ??= []
targetSm.TargetConditions.push({
Type: params.Outfit.IsHitmanSuit ? "hitmansuit" : "disguise",
RepositoryId: params.Outfit.RepositoryId,
// for contract creation it's always optional, only escalations set hard fail conditions
HardCondition: false,
ObjectiveId: this._outfitSm.Id,
// ioi moment
ObjectiveId: outfitSm.Id,
// "Amazing!" - Athena Savalas
KillMethod: "",
})
objectives.push(outfitSm)
}
/**
* Get the array of finalized state machines.
*
* @returns The state machines.
*/
public build(): MissionManifestObjective[] {
this._bootstrapEntrySm()
if (params.Weapon.RequiredKillMethodType !== 0) {
const weaponSm = createWeaponObjective(
params.Weapon,
params.RepositoryId,
)
if (this._params.Outfit.Required) {
// not any disguise
this._createOutfitSm(this._params.Outfit.IsHitmanSuit)
if (customIds?.kill) {
weaponSm.Id = customIds.kill
}
const values = [this._targetSm]
targetSm.TargetConditions ??= []
if (this._outfitSm) {
values.push(this._outfitSm)
}
targetSm.TargetConditions.push({
Type: "killmethod",
RepositoryId: params.Weapon.RepositoryId,
// for contract creation it's always optional, only escalations set hard fail conditions
HardCondition: false,
ObjectiveId: weaponSm.Id,
KillMethod: params.Weapon.RequiredKillMethod,
})
if (this._targetConds && Array.isArray(this._targetConds)) {
this._targetSm.TargetConditions = this._targetConds
}
objectives.push(weaponSm)
}
return values
return objectives
}
/**
* Create an objective for killing a target with a specific weapon.
* @param weapon The weapon details from the request.
* @param npcId The target NPC's repository ID.
*/
function createWeaponObjective(
weapon: Weapon,
npcId: RepositoryId,
): MissionManifestObjective {
return {
Type: "statemachine",
Id: randomUUID(),
Category: "secondary",
Definition: {
Scope: "Hit",
Context: {
Targets: [npcId],
},
States: {
Start: {
Kill: [
{
Condition: {
$and: [
{
$eq: ["$Value.RepositoryId", npcId],
},
genStateMachineKillSuccessCondition(weapon),
],
},
Transition: "Success",
},
{
Condition: {
$eq: ["$Value.RepositoryId", npcId],
},
Transition: "Failure",
},
],
},
},
},
}
}
/**
* The time limit creator API.
*
* Create a time limit objective.
* @param time The amount of time to use in seconds.
* @param optional If the objective should be optional or not.
* @returns The generated state machine.
@ -258,18 +308,14 @@ export function createTimeLimit(
Scope: "session",
States: {
Start: {
IntroCutEnd: [
{
Transition: "TimerRunning",
},
],
IntroCutEnd: {
Transition: "TimerRunning",
},
},
TimerRunning: {
exit_gate: [
{
Transition: "Success",
},
],
exit_gate: {
Transition: "Success",
},
$timer: [
{
Condition: {
@ -283,3 +329,150 @@ export function createTimeLimit(
},
}
}
/**
* These are all the possible ways to get a kill in contracts mode.
*/
export const enum ContractKillMethod {
Any,
AnyMelee,
ObjectMelee,
AnyThrown,
ObjectThrown,
Pistol,
PistolElimination,
Smg,
AssaultRifle,
Shotgun,
SniperRifle,
AnyPoison,
ConsumedPoison,
InjectedPoison,
AnyAccident,
ExplosiveDevice,
FiberWire,
UnarmedNeckSnap,
}
type Weapon = ContractCreationNpcTargetPayload["Weapon"]
type KillSuccessStateCondition = unknown
export function genStateMachineKillSuccessCondition(
weapon: Weapon,
): KillSuccessStateCondition {
const km = weaponToKillMethod(weapon)
if (km === ContractKillMethod.PistolElimination) {
return {
$any: {
"?": {
$or: [
{
$eq: ["$.#", "pistol"],
},
{
$eq: ["$.#", "close_combat_pistol_elimination"],
},
],
},
in: ["$Value.KillMethodBroad", "$Value.KillMethodStrict"],
},
}
}
if (km === ContractKillMethod.Pistol) {
return {
$any: {
"?": {
$or: [
{
$eq: ["$.#", "pistol"],
},
{
$eq: ["$.#", "close_combat_pistol_elimination"],
},
],
},
in: ["$Value.KillMethodBroad", "$Value.KillMethodStrict"],
},
}
}
return {
$any: {
"?": {
$eq: ["$.#", weapon.RequiredKillMethod],
},
in: ["$Value.KillMethodBroad", "$Value.KillMethodStrict"],
},
}
}
/**
* Get the equivalent kill method from a weapon object.
* @param weapon The weapon's details.
*/
export function weaponToKillMethod(weapon: Weapon): ContractKillMethod {
const type = weapon.RequiredKillMethodType
if (type === 0) {
return ContractKillMethod.Any
}
switch (weapon.KillMethodBroad) {
case "pistol":
return ContractKillMethod.Pistol
case "smg":
return ContractKillMethod.Smg
case "sniperrifle":
return ContractKillMethod.SniperRifle
case "assaultrifle":
return ContractKillMethod.AssaultRifle
case "shotgun":
return ContractKillMethod.Shotgun
case "close_combat_pistol_elimination":
return ContractKillMethod.PistolElimination
case "fiberwire":
return ContractKillMethod.FiberWire
case "throw": {
return type === 1
? ContractKillMethod.AnyThrown
: ContractKillMethod.ObjectThrown
}
case "melee_lethal": {
return type === 1
? ContractKillMethod.AnyMelee
: ContractKillMethod.ObjectMelee
}
case "poison": {
if (weapon.KillMethodStrict === "consumed_poison" && type === 2) {
return ContractKillMethod.ConsumedPoison
}
if (
weapon.KillMethodStrict === "injected_poison" &&
weapon.RequiredKillMethodType === 2
) {
return ContractKillMethod.InjectedPoison
}
assert(
type === 1,
`Unhandled poison: ${weapon.KillMethodStrict} ${type}`,
)
return ContractKillMethod.AnyPoison
}
case "unarmed":
return ContractKillMethod.UnarmedNeckSnap
case "accident":
return ContractKillMethod.AnyAccident
case "explosive":
return ContractKillMethod.ExplosiveDevice
default: {
assert.fail(
`Unhandled condition: ${weapon.KillMethodBroad} ${type}`,
)
}
}
}

View File

@ -19,7 +19,7 @@
import prompts from "prompts"
import { log, LogLevel } from "./loggingInterop"
import { readdir, writeFile } from "fs/promises"
import { PEACOCKVER, PEACOCKVERSTRING } from "./utils"
import { PEACOCKVERSTRING } from "./utils"
import md5File from "md5-file"
import { arch, cpus as cpuList, platform, version } from "os"
import { Controller, isPlugin } from "./controller"
@ -116,7 +116,6 @@ async function exportDebugInfo(): Promise<void> {
const data = {
version: PEACOCKVERSTRING,
ident: PEACOCKVER,
presentConfigs: Object.keys(configs),
chunkDigest: await md5File("chunk0.js"),
patcherDigest: await md5File("PeacockPatcher.exe"),

View File

@ -47,6 +47,7 @@ export interface SavedChallenge {
Xp: number
XpModifier?: unknown
DifficultyLevels: (keyof typeof gameDifficulty)[]
OrderIndex: number
Definition: MissionManifestObjective["Definition"] & {
Scope: ContextScopedStorageLocation
Repeatable?: {
@ -68,6 +69,7 @@ export interface SavedChallengeGroup {
Icon: string
CategoryId: string
Description: string
OrderIndex?: number
Challenges: SavedChallenge[]
}
@ -78,7 +80,7 @@ export interface ChallengePackage {
* The parent location.
*/
Location: string
GameVersion: GameVersion
GameVersions: GameVersion[]
}
}

View File

@ -80,7 +80,7 @@ export type MultiplayerMatchStatsQuery = Partial<{
contractSessionId: string
}>
export type LegacyGetProgressionBody = Partial<{
export type GetChallengeProgressionBody = Partial<{
profileid: string
challengeids: string[]
}>

View File

@ -27,9 +27,3 @@ declare let PEACOCK_DEV: boolean
* Injected during the build process.
*/
declare let HUMAN_VERSION: string
/**
* The revision identifier for the global version.
* Injected during the build process.
*/
declare let REV_IDENT: number

View File

@ -43,10 +43,9 @@ export type ScoringHeadline = {
scoreTotal: number
}
export type ManifestScoringModule =
| ScoringModule & {
Type: string
}
export type ManifestScoringModule = ScoringModule & {
Type: string
}
export type ManifestScoringDefinition = {
ContextListeners?: null | Record<string, IContextListener<never>>

View File

@ -18,7 +18,7 @@
import type * as core from "express-serve-static-core"
import type { IContractCreationPayload } from "../statemachines/contractCreation"
import type { ContractCreationNpcTargetPayload } from "../statemachines/contractCreation"
import { Request } from "express"
import {
ChallengeContext,
@ -192,7 +192,7 @@ export type MissionType =
/**
* The data acquired when using the "contract search" functionality.
*/
export interface ContractSearchResult {
export type ContractSearchResult = {
Data: {
Contracts: {
UserCentricContract: UserCentricContract
@ -210,9 +210,14 @@ export interface ContractSearchResult {
*
* @see ContractSession
*/
export interface ContractSessionLastKill {
export type ContractSessionLastKill = {
timestamp?: Date | number
repositoryIds?: RepositoryId[]
/**
* If the last kill was unnoticed in H2016.
* See [this video](https://www.youtube.com/watch?v=4fMDqRZg3Ik) for an explanation on how it's supposed to work.
*/
legacyIsUnnoticed?: boolean
}
/**
@ -328,7 +333,7 @@ export interface SaveFile {
*
* @see SaveFile
*/
export interface UpdateUserSaveFileTableBody {
export type UpdateUserSaveFileTableBody = {
clientSaveFileList: SaveFile[]
deletedSaveFileList: SaveFile[]
}
@ -336,12 +341,12 @@ export interface UpdateUserSaveFileTableBody {
/**
* The Hitman server version in object form.
*/
export interface ServerVersion {
readonly _Major: number
readonly _Minor: number
readonly _Build: number
readonly _Revision: number
}
export type ServerVersion = Readonly<{
_Major: number
_Minor: number
_Build: number
_Revision: number
}>
/**
* An event sent from the game client to the server.
@ -367,7 +372,7 @@ export interface S2CEventWithTimestamp<EventValue = unknown> {
/**
* A server to client push message. The message component is encoded JSON.
*/
export interface PushMessage {
export type PushMessage = {
time: number | string | bigint
message: string
}
@ -393,7 +398,7 @@ export interface ServerToClientEvent<EventValue = unknown> {
Origin?: string | null
}
export interface MissionStory {
export type MissionStory = {
CommonRepositoryId: RepositoryId
PreviouslyCompleted: boolean
IsMainOpportunity: boolean
@ -405,7 +410,7 @@ export interface MissionStory {
Image: string
}
export interface PlayerProfileView {
export type PlayerProfileView = {
SubLocationData: {
ParentLocation: Unlockable
Location: Unlockable
@ -433,34 +438,34 @@ export interface PlayerProfileView {
}
}
export interface ChallengeCompletion {
export type ChallengeCompletion = {
ChallengesCount: number
CompletedChallengesCount: number
CompletionPercent?: number
}
export interface ChallengeCategoryCompletion extends ChallengeCompletion {
export type ChallengeCategoryCompletion = ChallengeCompletion & {
Name: string
}
export interface OpportunityStatistics {
export type OpportunityStatistics = {
Count: number
Completed: number
}
export interface ContractHistory {
export type ContractHistory = {
LastPlayedAt?: number
Completed?: boolean
IsEscalation?: boolean
}
export interface ProgressionData {
export type ProgressionData = {
Xp: number
Level: number
PreviouslySeenXp: number
}
export interface UserProfile {
export type UserProfile = {
Id: string
LinkedAccounts: {
dev?: string
@ -506,10 +511,11 @@ export interface UserProfile {
*/
Total: number
Sublocations: {
Location: string
Xp: number
ActionXp: number
}[]
[location: string]: {
Xp: number
ActionXp: number
}
}
}
/**
* If the mastery location has subpackages and not drops, it will
@ -555,12 +561,12 @@ export interface UserProfile {
[opportunityId: RepositoryId]: boolean
}
CPD: CPDStore
LastOfficialSync: Date | string | null
}
ETag: string | null
Gamertag: string
DevId: string | null
SteamId: string | null
StadiaId: string | null
EpicId: string | null
NintendoId: string | null
XboxLiveId: string | null
@ -572,7 +578,7 @@ export interface UserProfile {
Version: number
}
export interface RatingKill {
export type RatingKill = {
IsHeadshot: boolean
KillClass: string
KillItemCategory: string
@ -588,7 +594,7 @@ export interface RatingKill {
OutfitRepoId: string
}
export interface NamespaceEntitlementEpic {
export type NamespaceEntitlementEpic = {
namespace: string
itemId: string
owned: boolean
@ -597,7 +603,7 @@ export interface NamespaceEntitlementEpic {
/**
* An unlockable item.
*/
export interface Unlockable {
export type Unlockable = {
Id: string
DisplayNameLocKey: string
GameAsset: string | null
@ -702,14 +708,14 @@ export interface Unlockable {
Rarity?: string | null
}
export interface ItemGameplay {
export type ItemGameplay = {
range?: number
damage?: number
clipsize?: number
rateoffire?: number
}
export interface CompletionData {
export type CompletionData = {
Level: number
MaxLevel: number
XP: number
@ -723,7 +729,7 @@ export interface CompletionData {
Name: string | null
}
export interface UserCentricContract {
export type UserCentricContract = {
Contract: MissionManifest
Data: {
IsLocked: boolean
@ -755,12 +761,27 @@ export interface UserCentricContract {
}
}
export interface TargetCondition {
Type: string
export type TargetCondition = {
/**
* The target condition type. This can be one of the following:
* - `killmethod` - A way to kill the target.
* - `hitmansuit` - Specifies the outfit must be any suit that you can start a level with which (but not a disguise).
* - `disguise` - Specifies the outfit must be a specific disguise.
*/
Type: "killmethod" | "hitmansuit" | "disguise"
RepositoryId?: RepositoryId
/**
* If the game should display the objective as optional or not.
*/
HardCondition?: boolean
/**
* The objective ID that this condition is tied to. When specified, the game can mark the condition with a check mark or X in the F1 menu.
*/
ObjectiveId?: string
KillMethod?: string
/**
* For outfit requirements, this is just an empty string. For kill methods, this is the kill method.
*/
KillMethod: "" | string
}
/**
@ -781,7 +802,7 @@ export interface HUDTemplate {
/**
* Data structure for a mission manifest's `Data.VR` bricks property.
*/
export interface VRQualityDefinition {
export type VRQualityDefinition = {
Quality: string
Bricks: string[]
}
@ -983,7 +1004,7 @@ export interface MissionManifestMetadata {
Modules?: ManifestScoringModule[] | null
}
export interface GroupObjectiveDisplayOrderItem {
export type GroupObjectiveDisplayOrderItem = {
Id: string
IsNew?: boolean
}
@ -1060,7 +1081,7 @@ export interface MissionManifest {
* A configuration that tells the game where it should connect to.
* This config is the first thing that the game asks for when logging in.
*/
export interface ServerConnectionConfig {
export type ServerConnectionConfig = {
Versions: {
Name: string
GAME_VER: string
@ -1093,7 +1114,7 @@ export interface ServerConnectionConfig {
*
* @see GameLocationsData
*/
export interface PeacockLocationsData {
export type PeacockLocationsData = {
/**
* The parent locations.
*/
@ -1125,7 +1146,7 @@ export interface GameLocationsData {
/**
* The body sent with the CreateFromParams request from the game during the final phase of contract creation.
*
* @see IContractCreationPayload
* @see ContractCreationNpcTargetPayload
*/
export interface CreateFromParamsBody {
creationData: {
@ -1133,12 +1154,12 @@ export interface CreateFromParamsBody {
Description: string
ContractId: string
ContractPublicId: string
Targets: IContractCreationPayload[]
Targets: ContractCreationNpcTargetPayload[]
ContractConditionIds: string[]
}
}
export interface SelectEntranceOrPickupData {
export type SelectEntranceOrPickupData = {
Unlocked: string[]
Contract: MissionManifest
OrderedUnlocks: Unlockable[]
@ -1150,7 +1171,7 @@ export type CompiledIoiStatemachine = unknown
/**
* The `ChallengeProgress` data for a `challengetree` context listener.
*/
export interface ChallengeProgressCTreeContextListener {
export type ChallengeProgressCTreeContextListener = {
total: number
completed: string[] | readonly string[]
missing: number
@ -1161,28 +1182,29 @@ export interface ChallengeProgressCTreeContextListener {
/**
* The `ChallengeProgress` data for a `challengecount` context listener.
*/
export interface ChallengeProgressCCountContextListener {
export type ChallengeProgressCCountContextListener = {
total: number
count: number
}
export interface CompiledChallengeTreeCategory {
export type CompiledChallengeTreeCategory = {
CategoryId: string
ChallengesCount: number
CompletedChallengesCount: number
CompletionData: CompletionData
CompletionData: CompletionData | Record<string, never>
Icon: string
Image: string
ImageLocked?: string
IsLocked: boolean
Location: Unlockable
Location: Unlockable | null
Name: string
RequiredResources: string[]
OrderIndex: number
SwitchData: {
Data: {
CategoryData: CompiledChallengeTreeCategoryInfo
Challenges: CompiledChallengeTreeData[]
CompletionData: CompletionData
CompletionData: CompletionData | Record<string, never>
HasNext: boolean
HasPrevious: boolean
NextCategoryIcon?: string
@ -1192,7 +1214,7 @@ export interface CompiledChallengeTreeCategory {
}
}
export interface CompiledChallengeTreeCategoryInfo {
export type CompiledChallengeTreeCategoryInfo = {
Name: string
Image: string
Icon: string
@ -1209,7 +1231,7 @@ export type ChallengeTreeWaterfallState =
| ChallengeProgressCCountContextListener
| null
export interface CompiledChallengeTreeData {
export type CompiledChallengeTreeData = {
CategoryName: string
ChallengeProgress?: ChallengeTreeWaterfallState
Completed: boolean
@ -1230,6 +1252,7 @@ export interface CompiledChallengeTreeData {
LocationId: string
Name: string
ParentLocationId: string
OrderIndex: number
Rewards: {
MasteryXP?: number
}
@ -1244,7 +1267,7 @@ export interface InclusionData {
GameModes?: string[]
}
export interface CompiledChallengeIngameData {
export type CompiledChallengeIngameData = {
Id: string
GroupId?: string
Name: string
@ -1268,15 +1291,15 @@ export interface CompiledChallengeIngameData {
/**
* Game-facing challenge progression data.
*/
export interface ChallengeProgressionData {
export type ChallengeProgressionData = {
ChallengeId: string
ProfileId: string
Completed: boolean
Ticked: boolean
Ticked?: boolean
ETag?: string
State: Record<string, unknown>
CompletedAt: Date | string | null
MustBeSaved: boolean
MustBeSaved?: boolean
}
export interface CompiledChallengeRuntimeData {
@ -1295,7 +1318,7 @@ export type Flags = Record<
/**
* A "hit" object.
*/
export interface IHit {
export type Hit = {
Id: string
UserCentricContract: UserCentricContract
Location: Unlockable
@ -1315,7 +1338,7 @@ export interface IHit {
* @see CampaignVideo
* @see StoryData
*/
export interface IVideo {
export type Video = {
VideoTitle: string
VideoHeader: string
VideoId: string
@ -1334,21 +1357,21 @@ export interface IVideo {
/**
* A campaign mission item.
*
* @see IHit
* @see Hit
*/
export type CampaignMission = {
Type: "Mission"
Data: IHit
Data: Hit
}
/**
* A campaign video item.
*
* @see IVideo
* @see Video
*/
export type CampaignVideo = {
Type: "Video"
Data: IVideo
Data: Video
}
export interface RegistryChallenge extends SavedChallenge {
@ -1368,7 +1391,7 @@ export type StoryData = CampaignMission | CampaignVideo
/**
* A campaign object.
*/
export interface Campaign {
export type Campaign = {
Name: string
Image: string
Type: MissionType | string
@ -1383,7 +1406,7 @@ export interface Campaign {
/**
* A loadout.
*/
export interface Loadout {
export type Loadout = {
/**
* Random ID.
*
@ -1439,25 +1462,25 @@ export type GenSingleVideoFunc = (
/**
* A "hits category" is used to display lists of contracts in-game.
*
* @see IHit
* @see Hit
*/
export interface HitsCategoryCategory {
export type HitsCategoryCategory = {
Category: string
Data: {
Type: string
Hits: IHit[]
Hits: Hit[]
Page: number
HasMore: boolean
}
CurrentSubType: string
}
export interface PlayNextCampaignDetails {
export type PlayNextCampaignDetails = {
CampaignName: string
ParentCampaignName?: string
}
export interface PlayNextGetCampaignsHookReturn {
export type PlayNextGetCampaignsHookReturn = {
/**
* The UUID of the next contract in the campaign.
*/
@ -1528,7 +1551,7 @@ export type CPDStore = Record<string, Record<string, string | number | boolean>>
export type ContractProgressionData = Record<string, string | number | boolean>
/** SMF's lastDeploy.json */
export interface SMFLastDeploy {
export type SMFLastDeploy = {
runtimePath: string
retailPath: string
skipIntro: boolean
@ -1551,3 +1574,9 @@ export interface SMFLastDeploy {
peacockPlugins?: string[]
}
}
export type OfficialSublocation = {
Location: string
Xp: number
ActionXp: number
}

View File

@ -21,6 +21,7 @@ import type { NextFunction, Response } from "express"
import type {
GameVersion,
MissionManifestObjective,
OfficialSublocation,
PeacockLocationsData,
RepositoryId,
RequestWithJwt,
@ -33,6 +34,7 @@ import { log, LogLevel } from "./loggingInterop"
import { writeFileSync } from "fs"
import { getFlag } from "./flags"
import { getConfig, getVersionedConfig } from "./configSwizzleManager"
import { compare } from "semver"
/**
* True if the server is being run by the launcher, false otherwise.
@ -41,12 +43,11 @@ export const IS_LAUNCHER = process.env.IS_PEACOCK_LAUNCHER === "true"
export const ServerVer: ServerVersion = {
_Major: 8,
_Minor: 14,
_Minor: 15,
_Build: 0,
_Revision: 0,
}
export const PEACOCKVER = REV_IDENT
export const PEACOCKVERSTRING = HUMAN_VERSION
export const uuidRegex =
@ -54,6 +55,9 @@ export const uuidRegex =
export const contractTypes = ["featured", "usercreated"]
/**
* A list of game versions, except scpc.
*/
export const versions: Exclude<GameVersion, "scpc">[] = ["h1", "h2", "h3"]
export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb"
@ -63,7 +67,7 @@ export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb"
*
* See docs/USER_PROFILES.md for more.
*/
export const LATEST_PROFILE_VERSION = 1
export const LATEST_PROFILE_VERSION = 2
export async function checkForUpdates(): Promise<void> {
if (getFlag("updateChecking") === false) {
@ -71,29 +75,38 @@ export async function checkForUpdates(): Promise<void> {
}
try {
const res = await fetch(
"https://backend.rdil.rocks/peacock/latest-version",
)
const current = parseInt(await res.text(), 10)
type VersionCheckResponse = { id: string; channel: string }
if (PEACOCKVER < 0 && current < -PEACOCKVER) {
const res = await fetch(
"https://backend.rdil.rocks/peacock/latest-version/data",
)
const data = (await res.json()) as VersionCheckResponse
const current = compare(PEACOCKVERSTRING, data.id)
const isNewer = current === 1
if (isNewer) {
log(
LogLevel.INFO,
`Thank you for trying out this testing version of Peacock! Please report any bugs by posting in the #help channel on Discord or by submitting an issue on GitHub.`,
`You're ahead of the latest release! The latest version of Peacock (${data.id}) is older than this build.`,
"updates",
)
} else if (PEACOCKVER > 0 && current === PEACOCKVER) {
log(LogLevel.DEBUG, "Peacock is up to date.")
} else if (current === 0) {
log(LogLevel.DEBUG, "Peacock is up to date.", "updates")
} else {
log(
LogLevel.WARN,
`Peacock is out-of-date! Check the Discord for the latest release.`,
"updates",
)
}
} catch (e) {
log(LogLevel.WARN, "Failed to check for updates!")
log(LogLevel.WARN, "Failed to check for updates!", "updates")
}
}
export * from "semver"
export function getRemoteService(gameVersion: GameVersion): string | undefined {
switch (gameVersion) {
case "h3":
@ -181,14 +194,12 @@ export const EVERGREEN_LEVEL_INFO: number[] = [
export function evergreenLevelForXp(xp: number): number {
for (let i = 1; i < EVERGREEN_LEVEL_INFO.length; i++) {
if (xp >= EVERGREEN_LEVEL_INFO[i]) {
continue
if (xp < EVERGREEN_LEVEL_INFO[i]) {
return i
}
return i
}
return 1
return EVERGREEN_LEVEL_INFO.length
}
/**
@ -209,14 +220,12 @@ export const SNIPER_LEVEL_INFO: number[] = [
export function sniperLevelForXp(xp: number): number {
for (let i = 1; i < SNIPER_LEVEL_INFO.length; i++) {
if (xp >= SNIPER_LEVEL_INFO[i]) {
continue
if (xp < SNIPER_LEVEL_INFO[i]) {
return i
}
return i
}
return 1
return SNIPER_LEVEL_INFO.length
}
/**
@ -241,7 +250,6 @@ export function clampValue(value: number, min: number, max: number) {
*
* @param profile The user profile to update
* @param gameVersion The game version
* @returns The updated user profile.
*/
function updateUserProfile(
profile: UserProfile,
@ -258,6 +266,29 @@ function updateUserProfile(
case LATEST_PROFILE_VERSION:
// This profile updated to the latest version, we're done.
return
case 1: {
/* ////// VERSION 2 ////// */
const sublocations = profile.Extensions.progression.PlayerProfileXP
.Sublocations as unknown as OfficialSublocation[]
profile.Extensions.progression.PlayerProfileXP.Sublocations =
Object.fromEntries(
sublocations.map((value) => [
value.Location,
{
Xp: value.Xp,
ActionXp: value.ActionXp,
},
]),
)
profile.Extensions.LastOfficialSync = null
profile.Version = 2
return updateUserProfile(profile, gameVersion)
}
default: {
// Check that the profile version is indeed undefined. If it isn't,
// we've forgotten to add a version to the switch.
@ -647,14 +678,14 @@ export function handleAxiosError(error: AxiosError): void {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
log(LogLevel.DEBUG, `code ${error.response.status}`)
log(LogLevel.DEBUG, `Request error: code ${error.response.status}`)
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of http.ClientRequest
log(LogLevel.DEBUG, `bad fetch`)
log(LogLevel.DEBUG, `Request error: bad fetch`)
} else {
// Something happened in setting up the request that triggered an Error
log(LogLevel.DEBUG, `generic`)
log(LogLevel.DEBUG, `Request error: something went wrong`)
}
}
@ -718,3 +749,26 @@ export function isSuit(repoId: string): boolean {
? suitsToTypeMap[repoId] !== "disguise"
: false
}
type SublocationMap = {
[parentId: string]: string[]
}
export function getSublocations(gameVersion: GameVersion): SublocationMap {
const sublocations: SublocationMap = {}
const locations = getVersionedConfig<PeacockLocationsData>(
"LocationsData",
gameVersion,
false,
)
for (const child of Object.values(locations.children)) {
if (!child.Properties.ParentLocation) continue
sublocations[child.Properties.ParentLocation] ??= []
sublocations[child.Properties.ParentLocation].push(child.Id)
}
return sublocations
}

View File

@ -18,14 +18,44 @@
import { NextFunction, Request, Response, Router } from "express"
import { getConfig } from "./configSwizzleManager"
import { readFileSync } from "atomically"
import { GameVersion, UserProfile } from "./types/types"
import { readdir, readFile } from "fs/promises"
import {
ChallengeProgressionData,
GameVersion,
HitsCategoryCategory,
OfficialSublocation,
ProgressionData,
UserProfile,
} from "./types/types"
import { join } from "path"
import { uuidRegex, versions } from "./utils"
import {
getRemoteService,
getSublocations,
isSniperLocation,
levelForXp,
uuidRegex,
versions,
} from "./utils"
import { getUserData, loadUserData, writeUserData } from "./databaseHandler"
import { readdirSync } from "fs"
import { controller } from "./controller"
import { log, LogLevel } from "./loggingInterop"
import { OfficialServerAuth, userAuths } from "./officialServerAuth"
import { AxiosError } from "axios"
import { SNIPER_UNLOCK_TO_LOCATION } from "./menuData"
type OfficialProfileResponse = UserProfile & {
Extensions: {
progression: {
Unlockables: {
[unlockableId: string]: ProgressionData
}
}
}
}
type SubPackageData = {
[id: string]: ProgressionData
}
const webFeaturesRouter = Router()
@ -34,9 +64,9 @@ if (PEACOCK_DEV) {
res.set("Access-Control-Allow-Origin", "*")
res.set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
"GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
)
res.set("Access-Control-Allow-Headers", "Content-Type")
res.set("Access-Control-Allow-Headers", "content-type")
next()
})
}
@ -86,8 +116,13 @@ webFeaturesRouter.get("/codenames", (_, res) => {
res.json(getConfig("EscalationCodenames", false))
})
webFeaturesRouter.get("/local-users", (req: CommonRequest, res) => {
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
webFeaturesRouter.get("/local-users", async (req: CommonRequest, res) => {
// Validate that gv is h1, h2, or h3
function validateGv(gv: unknown): gv is "h1" | "h2" | "h3" {
return versions.includes(gv as Exclude<GameVersion, "scpc">)
}
if (!validateGv(req.query.gv)) {
res.json([])
return
}
@ -100,21 +135,35 @@ webFeaturesRouter.get("/local-users", (req: CommonRequest, res) => {
dir = join("userdata", req.query.gv, "users")
}
const files: string[] = readdirSync(dir).filter(
const files: string[] = (await readdir(dir)).filter(
(name) => name !== "lop.json",
)
const result = []
/**
* Sync this type with `webui/src/utils`!
*/
type BasicUser = Readonly<{
id: string
name: string
platform: string
lastOfficialSync: string | null
}>
const result: BasicUser[] = []
for (const file of files) {
if (file === "lop.json") continue
const read = JSON.parse(
readFileSync(join(dir, file)).toString(),
(await readFile(join(dir, file))).toString(),
) as UserProfile
result.push({
id: read.Id,
name: read.Gamertag,
platform: read.EpicId ? "Epic" : "Steam",
lastOfficialSync:
read.Extensions.LastOfficialSync?.toString() || null,
})
}
@ -213,4 +262,360 @@ webFeaturesRouter.get(
},
)
type EscalationData = {
PeacockEscalations: {
[escalationId: string]: number
}
PeacockCompletedEscalations: string[]
}
type OfficialHitsCategory = {
data: HitsCategoryCategory
}
async function getHitsCategory(
auth: OfficialServerAuth,
remoteService: string,
category: string,
page: number,
): Promise<[results: EscalationData, hasMore: boolean]> {
const data: EscalationData = {
PeacockEscalations: {},
PeacockCompletedEscalations: [],
}
const hits = await auth._useService<OfficialHitsCategory>(
`https://${remoteService}.hitman.io/profiles/page/HitsCategory?page=${page}&type=${category}&mode=dataonly`,
true,
)
for (const hit of hits.data.data.Data.Hits) {
data.PeacockEscalations[hit.Id] =
hit.UserCentricContract.Data.EscalationCompletedLevels! + 1
if (hit.UserCentricContract.Data.EscalationCompleted)
data.PeacockCompletedEscalations.push(hit.Id)
}
return [data, hits.data.data.Data.HasMore]
}
async function getAllHitsCategory(
auth: OfficialServerAuth,
remoteService: string,
category: string,
): Promise<EscalationData> {
const data: EscalationData = {
PeacockEscalations: {},
PeacockCompletedEscalations: [],
}
let page = 0
let hasMore = true
while (hasMore) {
const [results, more] = await getHitsCategory(
auth,
remoteService,
category,
page,
)
data.PeacockEscalations = {
...data.PeacockEscalations,
...results.PeacockEscalations,
}
data.PeacockCompletedEscalations = [
...data.PeacockCompletedEscalations,
...results.PeacockCompletedEscalations,
]
page++
hasMore = more
}
return data
}
webFeaturesRouter.post(
"/sync-progress",
commonValidationMiddleware,
async (req: CommonRequest, res) => {
const remoteService = getRemoteService(req.query.gv)
const auth = userAuths.get(req.query.user)
if (!auth) {
formErrorMessage(
res,
"Failed to get official authentication data. Please connect to Peacock first.",
)
return
}
const userdata = getUserData(req.query.user, req.query.gv)
try {
// Challenge Progression
log(LogLevel.DEBUG, "Getting challenge progression...")
const challengeProgression = await auth._useService<
ChallengeProgressionData[]
>(
`https://${remoteService}.hitman.io/authentication/api/userchannel/ChallengesService/GetProgression`,
false,
{
profileid: req.query.user,
challengeids: controller.challengeService.getChallengeIds(
req.query.gv,
),
},
)
userdata.Extensions.ChallengeProgression = Object.fromEntries(
challengeProgression.data.map((data) => {
return [
data.ChallengeId,
{
Ticked: data.Completed,
Completed: data.Completed,
CurrentState:
(data.State["CurrentState"] as string) ??
"Start",
State: data.State,
},
]
}),
)
// Profile Progression
log(LogLevel.DEBUG, "Getting profile progression...")
const exts = await auth._useService<OfficialProfileResponse>(
`https://${remoteService}.hitman.io/authentication/api/userchannel/ProfileService/GetProfile`,
false,
{
id: req.query.user,
extensions: [
"achievements",
"friends",
"gameclient",
"gamepersistentdata",
"opportunityprogression",
"progression",
"defaultloadout",
],
},
)
if (req.query.gv !== "h1") {
log(LogLevel.DEBUG, "Processing PlayerProfileXP...")
const sublocations = exts.data.Extensions.progression
.PlayerProfileXP
.Sublocations as unknown as OfficialSublocation[]
userdata.Extensions.progression.PlayerProfileXP = {
...userdata.Extensions.progression.PlayerProfileXP,
Total: exts.data.Extensions.progression.PlayerProfileXP
.Total,
ProfileLevel: levelForXp(
exts.data.Extensions.progression.PlayerProfileXP.Total,
),
Sublocations: Object.fromEntries(
sublocations.map((value) => [
value.Location,
{
Xp: value.Xp,
ActionXp: value.ActionXp,
},
]),
),
}
log(LogLevel.DEBUG, "Processing opportunity progression...")
userdata.Extensions.opportunityprogression = Object.fromEntries(
Object.keys(
exts.data.Extensions.opportunityprogression || {},
).map((value) => [value, true]),
)
if (exts.data.Extensions.progression.Unlockables) {
log(LogLevel.DEBUG, "Processing unlockables...")
for (const [unlockId, data] of Object.entries(
exts.data.Extensions.progression.Unlockables,
)) {
const unlockableId = unlockId.toUpperCase()
if (!(unlockableId in SNIPER_UNLOCK_TO_LOCATION))
continue
;(
userdata.Extensions.progression.Locations[
SNIPER_UNLOCK_TO_LOCATION[unlockableId]
] as SubPackageData
)[unlockableId] = {
Xp: data.Xp,
Level: data.Level,
PreviouslySeenXp: data.PreviouslySeenXp,
}
}
}
}
userdata.Extensions.gamepersistentdata =
exts.data.Extensions.gamepersistentdata || {}
const sublocations = getSublocations(req.query.gv)
userdata.Extensions.defaultloadout ??= {}
if (exts.data.Extensions.defaultloadout) {
for (const [parent, loadout] of Object.entries(
exts.data.Extensions.defaultloadout,
)) {
for (const child of sublocations[parent]) {
userdata.Extensions.defaultloadout[child] = loadout
}
}
}
userdata.Extensions.achievements =
exts.data.Extensions.achievements || []
for (const [locId, data] of Object.entries(
exts.data.Extensions.progression.Locations,
)) {
const location = (
locId.startsWith("location_parent")
? locId
: locId.replace("location_", "location_parent_")
).toUpperCase()
if (isSniperLocation(location)) continue
if (req.query.gv === "h1") {
const parent = location.endsWith("PRO1")
? location.substring(0, location.length - 5)
: location
const packageId: string = location.endsWith("PRO1")
? "pro1"
: "normal"
;(
userdata.Extensions.progression.Locations[
parent
] as SubPackageData
)[packageId] = {
Xp: data.Xp as number,
Level: data.Level as number,
PreviouslySeenXp: data.Xp as number,
}
} else {
userdata.Extensions.progression.Locations[location] = {
Xp: data.Xp as number,
Level: data.Level as number,
PreviouslySeenXp: data.PreviouslySeenXp as number,
}
}
}
// Escalation & Arcade Progression
log(
LogLevel.DEBUG,
`Getting escalation${req.query.gv === "h3" ? " & arcade" : ""} progression...`,
)
const escalations = await getAllHitsCategory(
auth,
remoteService!,
"ContractAttack",
)
const arcade =
req.query.gv === "h3"
? await getAllHitsCategory(auth, remoteService!, "Arcade")
: {
PeacockEscalations: {},
PeacockCompletedEscalations: [],
}
userdata.Extensions.PeacockEscalations = {
...userdata.Extensions.PeacockEscalations,
...escalations.PeacockEscalations,
...arcade.PeacockEscalations,
}
userdata.Extensions.PeacockCompletedEscalations = [
...userdata.Extensions.PeacockCompletedEscalations,
...escalations.PeacockCompletedEscalations,
...arcade.PeacockCompletedEscalations,
]
for (const id of userdata.Extensions.PeacockCompletedEscalations) {
userdata.Extensions.PeacockPlayedContracts[id] = {
LastPlayedAt: new Date().getTime(),
Completed: true,
IsEscalation: true,
}
}
// Freelancer Progression
// TODO: Try and see if there is a less intensive way to do this
// GetForPlay2 is quite intensive on IOI's side as it starts a session
if (req.query.gv === "h3") {
log(LogLevel.DEBUG, "Getting freelancer progression...")
await auth._useService(
`https://${remoteService}.hitman.io/authentication/api/configuration/Init?configName=pc-prod&lockedContentDisabled=false&isFreePrologueUser=false&isIntroPackUser=false&isFullExperienceUser=true`,
true,
)
const freelancerSession = await auth._useService<{
ContractProgressionData: Record<
string,
string | number | boolean
>
}>(
`https://${remoteService}.hitman.io/authentication/api/userchannel/ContractsService/GetForPlay2`,
false,
{
id: "f8ec92c2-4fa2-471e-ae08-545480c746ee",
locationId: "",
extraGameChangerIds: [],
difficultyLevel: 0,
},
)
userdata.Extensions.CPD[
"f8ec92c2-4fa2-471e-ae08-545480c746ee"
] = freelancerSession.data.ContractProgressionData
}
userdata.Extensions.LastOfficialSync = new Date().toISOString()
writeUserData(req.query.user, req.query.gv)
} catch (error) {
if (error instanceof AxiosError) {
formErrorMessage(
res,
`Failed to sync official data: got ${error.response?.status} ${error.response?.statusText}.`,
)
return
} else {
formErrorMessage(
res,
`Failed to sync official data: got ${JSON.stringify(error)}.`,
)
return
}
}
res.json({
success: true,
})
},
)
export { webFeaturesRouter }

View File

@ -113,7 +113,8 @@
}
],
"Bricks": [],
"GameChangers": []
"GameChangers": [],
"EnableSaving": false
},
"Metadata": {
"TileImage": "images/contracts/escalation/contractescalation_pontus.jpg",

View File

@ -142,7 +142,8 @@
}
],
"Bricks": [],
"GameChangers": ["07b1bc1d-f52b-4004-a760-846c4bc3f172"]
"GameChangers": ["07b1bc1d-f52b-4004-a760-846c4bc3f172"],
"EnableSaving": false
},
"Metadata": {
"TileImage": "images/contracts/escalation/contractescalation_pontus.jpg",

View File

@ -205,7 +205,8 @@
}
],
"Bricks": [],
"GameChangers": ["07b1bc1d-f52b-4004-a760-846c4bc3f172"]
"GameChangers": ["07b1bc1d-f52b-4004-a760-846c4bc3f172"],
"EnableSaving": false
},
"Metadata": {
"TileImage": "images/contracts/escalation/contractescalation_pontus.jpg",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_ROCKY",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "2638c6ab-20e5-4f5f-bcbb-2f940c7adee3",
@ -962,6 +963,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "ad549a16-c8c7-40af-9265-f5c42065be87",
@ -1671,6 +1673,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "8d27e376-c658-4f1a-8b97-a76e99575539",
@ -2103,6 +2106,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "b5213925-b20e-4ce9-9c70-d60b2771d541",
@ -2196,6 +2200,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "425a7a49-1892-4a24-aa40-f46cdb8d6c7f",
@ -3047,6 +3052,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "argon-pack",
"Description": "",
"OrderIndex": 6.6,
"Challenges": [
{
"Id": "0409d6ed-bb3b-4628-a9d4-90cbfb7c1793",
@ -3084,6 +3090,142 @@
"Tags": ["argon-pack", "story", "live", "medium"]
}
]
},
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Image": "images/challenges/categories/packcheesecake/tile.jpg",
"Icon": "challenge_category_feats",
"CategoryId": "cheesecake-pack",
"Description": "",
"OrderIndex": 6.1,
"Challenges": [
{
"Id": "b5386255-0e7e-2bd7-6c28-c1096d4902c5",
"Name": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_AMBROSEHOOK_NAME",
"ImageName": "images/challenges/categories/packcheesecake/cheesecake_ambrosehook.jpg",
"Description": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_AMBROSEHOOK_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": ["PROP_MELEE_BUTCHERS_MEATHOOK"],
"IsPlayable": true,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Icon": "challenge_category_feats",
"LocationId": "LOCATION_PARENT_ROCKY",
"ParentLocationId": "LOCATION_PARENT_ROCKY",
"Type": "parentlocation",
"DifficultyLevels": [],
"OrderIndex": 100004,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Scope": "session",
"States": {
"Start": {
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.RepositoryId",
"1312f620-bf61-4b7f-8f5c-ea4e07763a98"
]
},
{
"$eq": [
"$Value.KillMethodStrict",
"accident_burn"
]
}
]
},
"Transition": "SmithDown"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.RepositoryId",
"9454339d-8f29-4ae6-97f4-96523a48bf08"
]
},
{
"$eq": [
"$Value.KillItemRepositoryId",
"58a036dc-79d4-4d64-8bf5-3faafa3cfead"
]
}
]
},
"Transition": "MateDown"
}
]
},
"SmithDown": {
"Kill": {
"Condition": {
"$and": [
{
"$eq": [
"$Value.RepositoryId",
"9454339d-8f29-4ae6-97f4-96523a48bf08"
]
},
{
"$eq": [
"$Value.KillItemRepositoryId",
"58a036dc-79d4-4d64-8bf5-3faafa3cfead"
]
}
]
},
"Transition": "CheckShower"
}
},
"MateDown": {
"Kill": {
"Condition": {
"$and": [
{
"$eq": [
"$Value.RepositoryId",
"1312f620-bf61-4b7f-8f5c-ea4e07763a98"
]
},
{
"$eq": [
"$Value.KillMethodStrict",
"accident_burn"
]
}
]
},
"Transition": "CheckShower"
}
},
"CheckShower": {
"setpieces": {
"Condition": {
"$eq": [
"$Value.RepositoryId",
"168b0d90-2b0b-44ec-98fb-bc728cfbc12b"
]
},
"Transition": "Success"
}
}
}
},
"Tags": ["cheesecake-pack", "story", "live", "medium"],
"InclusionData": {
"ContractIds": ["b2aac100-dfc7-4f85-b9cd-528114436f6c"]
}
}
]
}
]
}

View File

@ -0,0 +1,40 @@
{
"Data": {
"EnableSaving": false,
"Objectives": [],
"Bricks": [],
"VR": [
{
"Quality": "base",
"Bricks": ["assembly:/_pro/Scenes/Bricks/vr_setup.brick"]
}
],
"GameChangers": [],
"GameChangerReferences": []
},
"Metadata": {
"Id": "de9788cc-b9c4-47fc-b5df-86451cd82c43",
"IsPublished": true,
"CreationTimestamp": "2024-03-22T10:54:18.3690079Z",
"CreatorUserId": "fadb923c-e6bb-4283-a537-eb4d1150262e",
"TileImage": "images/contracts/arcade/Arcade_AniseStar_Group.jpg",
"Title": "UI_CONTRACT_ANISESTAR_GROUP_TITLE",
"Description": "UI_CONTRACT_ANISESTAR_GROUP_DESC",
"CodeName_Hint": "Arcade AniseStar - Group",
"Location": "LOCATION_MIAMI",
"Type": "arcade",
"Release": "3.180.0 Arcade",
"ScenePath": "assembly:/_pro/scenes/missions/miami/scene_et_sambuca.entity",
"Entitlements": ["H3_ET_SAMBUCA"],
"GroupDefinition": {
"Type": "arcade",
"Order": [
"a07b7a11-f318-4905-9826-4fc10505bef6",
"8e5a8e33-6f23-4a2f-8e4a-402c296dacc8",
"8e7e9f08-94c5-4667-ac8a-61aa15b74c0a"
]
},
"PublicId": "011709683047"
},
"UserData": {}
}

View File

@ -0,0 +1,99 @@
{
"Data": {
"EnableSaving": false,
"Objectives": [
{
"Id": "c2b3f8e3-0eaf-41b3-8b8c-3cf012a1d0bc",
"Primary": true,
"IsHidden": true,
"SuccessEvent": {
"EventName": "Kill",
"EventValues": {
"RepositoryId": "63283fb2-a653-486d-a505-e471efbc8c54"
}
}
},
{
"_comment": "----- Eliminate Faba with a Pen -----",
"Id": "2428f45d-51f2-4043-9d9f-237ee856ccd8",
"Primary": false,
"ObjectiveType": "custom",
"ForceShowOnLoadingScreen": true,
"ExcludeFromScoring": true,
"OnActive": {
"IfCompleted": {
"Visible": true
}
},
"Image": "images/challenges/elusive_target/et_sambuca_started_pen.jpg",
"BriefingName": "$loc UI_CONTRACT_ANISESTAR_PENKILL_NAME",
"BriefingText": "$loc UI_CONTRACT_ANISESTAR_PENKILL_DESC",
"HUDTemplate": {
"display": "$loc UI_CONTRACT_ANISESTAR_PENKILL_NAME",
"iconType": 17
},
"Type": "statemachine",
"Definition": {
"display": {
"iconType": 17
},
"Scope": "session",
"States": {
"Start": {
"Faba_Pen_Fail": {
"Transition": "Failure"
},
"Faba_Pen_Completed": {
"Transition": "Success"
}
}
}
}
}
],
"Entrances": ["f1de2373-eebe-4448-808a-8c2764b2c931"],
"Bricks": [
"assembly:/_pro/scenes/missions/miami/mission_sambuca.brick"
],
"VR": [
{
"Quality": "base",
"Bricks": [
"assembly:/_pro/Scenes/Bricks/vr_setup.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_flamingo.brick",
"assembly:/_PRO/scenes/missions/miami/vr_overrides_ps4perf.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_sambuca.brick"
]
},
{
"Quality": "better",
"Bricks": [
"assembly:/_pro/Scenes/Bricks/vr_setup.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_flamingo.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_sambuca.brick"
]
}
],
"GameChangers": []
},
"Metadata": {
"Id": "a07b7a11-f318-4905-9826-4fc10505bef6",
"Title": "UI_CONTRACT_ANISESTAR_LEVEL01_NAME",
"CodeName_Hint": "Arcade AniseStar Sambuca",
"Description": "UI_CONTRACT_SAMBUCA_DESC",
"BriefingVideo": "briefing_sambuca",
"DebriefingVideo": "debriefing_sambuca",
"ScenePath": "assembly:/_pro/scenes/missions/miami/scene_et_sambuca.entity",
"TileImage": "images/contracts/elusive/046_sambuca/Title.jpg",
"InGroup": "de9788cc-b9c4-47fc-b5df-86451cd82c43",
"Location": "LOCATION_MIAMI",
"IsPublished": true,
"LastUpdate": "2024-02-02T12:00:00.441Z",
"CreationTimestamp": "2024-03-22T10:54:18.9583011Z",
"CreatorUserId": "fadb923c-e6bb-4283-a537-eb4d1150262e",
"Type": "arcade",
"Release": "3.180.0 Arcade",
"Entitlements": ["H3_ET_SAMBUCA"],
"PublicId": "011002667847"
}
}

View File

@ -0,0 +1,63 @@
{
"Data": {
"GameChangers": [],
"Bricks": [
"assembly:/_pro/scenes/missions/miami/scenario_sambuca2.brick"
],
"VR": [
{
"Quality": "base",
"Bricks": [
"assembly:/_pro/Scenes/Bricks/vr_setup.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_flamingo.brick",
"assembly:/_PRO/scenes/missions/miami/vr_overrides_ps4perf.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_sambuca.brick"
]
},
{
"Quality": "better",
"Bricks": [
"assembly:/_pro/Scenes/Bricks/vr_setup.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_flamingo.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_sambuca.brick"
]
}
],
"EnableSaving": false,
"Entrances": ["f1de2373-eebe-4448-808a-8c2764b2c931"],
"Objectives": [
{
"Primary": true,
"SuccessEvent": {
"EventValues": {
"RepositoryId": "0eda939e-db2b-4344-9c3f-9d00eb2fb1bc"
},
"EventName": "Kill"
},
"Id": "9eaf0208-4736-4554-aeed-cfc871efab40",
"IsHidden": true
}
]
},
"Metadata": {
"DebriefingVideo": "debriefing_sambuca2",
"BriefingVideo": "briefing_sambuca2",
"Title": "UI_CONTRACT_ANISESTAR_LEVEL02_NAME",
"Id": "8e5a8e33-6f23-4a2f-8e4a-402c296dacc8",
"ScenePath": "assembly:/_pro/scenes/missions/miami/scene_et_sambuca.entity",
"CreationTimestamp": "2024-03-22T10:54:19.018087Z",
"IsPublished": true,
"Type": "arcade",
"LastUpdate": "2015-03-10T12:00:00.441Z",
"TileImage": "images/contracts/elusive/s2_sambuca2/title.jpg",
"Release": "3.180.0 Arcade",
"InGroup": "de9788cc-b9c4-47fc-b5df-86451cd82c43",
"CodeName_Hint": "Arcade AniseStar Sambuca2",
"Description": "UI_CONTRACT_SAMBUCA2_DESC",
"CreatorUserId": "fadb923c-e6bb-4283-a537-eb4d1150262e",
"Location": "LOCATION_MIAMI",
"Entitlements": ["H3_ET_SAMBUCA"],
"PublicId": "011330641347"
},
"UserData": {}
}

View File

@ -0,0 +1,66 @@
{
"Data": {
"GameChangers": [
"63055f1a-bcd2-4e0f-8caf-b446f01d02f3",
"9f409781-0a06-4748-b08d-784e78c6d481"
],
"Bricks": [
"assembly:/_pro/scenes/missions/miami/scenario_sambuca2.brick"
],
"VR": [
{
"Quality": "base",
"Bricks": [
"assembly:/_pro/Scenes/Bricks/vr_setup.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_flamingo.brick",
"assembly:/_PRO/scenes/missions/miami/vr_overrides_ps4perf.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_sambuca.brick"
]
},
{
"Quality": "better",
"Bricks": [
"assembly:/_pro/Scenes/Bricks/vr_setup.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_flamingo.brick",
"assembly:/_pro/scenes/missions/miami/vr_overrides_sambuca.brick"
]
}
],
"EnableSaving": false,
"Entrances": ["f1de2373-eebe-4448-808a-8c2764b2c931"],
"Objectives": [
{
"Primary": true,
"SuccessEvent": {
"EventValues": {
"RepositoryId": "0eda939e-db2b-4344-9c3f-9d00eb2fb1bc"
},
"EventName": "Kill"
},
"Id": "9eaf0208-4736-4554-aeed-cfc871efab40",
"IsHidden": true
}
]
},
"Metadata": {
"DebriefingVideo": "debriefing_sambuca2",
"BriefingVideo": "briefing_sambuca2",
"Title": "UI_CONTRACT_ANISESTAR_LEVEL03_NAME",
"Id": "8e7e9f08-94c5-4667-ac8a-61aa15b74c0a",
"ScenePath": "assembly:/_pro/scenes/missions/miami/scene_et_sambuca.entity",
"CreationTimestamp": "2024-02-02T12:12:12.743Z",
"IsPublished": true,
"Type": "arcade",
"LastUpdate": "2024-03-22T13:30:07.5899447Z",
"TileImage": "images/contracts/elusive/s2_sambuca2/title.jpg",
"Release": "3.180.0 Arcade",
"InGroup": "de9788cc-b9c4-47fc-b5df-86451cd82c43",
"CodeName_Hint": "Arcade AniseStar Sambuca2",
"Description": "UI_CONTRACT_SAMBUCA2_DESC",
"CreatorUserId": "fadb923c-e6bb-4283-a537-eb4d1150262e",
"Location": "LOCATION_MIAMI",
"Entitlements": [],
"PublicId": "011545095147"
},
"UserData": {}
}

View File

@ -52,8 +52,8 @@
"Entitlements": ["LOCATION_GOLDEN"],
"InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f",
"GroupObjectiveDisplayOrder": [
{ "Id": "611cbf7d-7871-4a02-843a-06d523563519" },
{ "Id": "b43f84f7-6c26-4adf-b74a-6d598f03cbe3", "IsNew": true }
{ "Id": "b43f84f7-6c26-4adf-b74a-6d598f03cbe3", "IsNew": true },
{ "Id": "611cbf7d-7871-4a02-843a-06d523563519" }
]
},
"UserData": {}

View File

@ -53,8 +53,8 @@
"Entitlements": ["LOCATION_GOLDEN"],
"InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f",
"GroupObjectiveDisplayOrder": [
{ "Id": "c2b3f8e3-0eaf-41b3-8b8c-3cf012a1d0bc" },
{ "Id": "b43f84f7-6c26-4adf-b74a-6d598f03cbe3" }
{ "Id": "b43f84f7-6c26-4adf-b74a-6d598f03cbe3" },
{ "Id": "c2b3f8e3-0eaf-41b3-8b8c-3cf012a1d0bc" }
]
},
"UserData": {}

View File

@ -59,8 +59,8 @@
"Entitlements": ["LOCATION_GOLDEN"],
"InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f",
"GroupObjectiveDisplayOrder": [
{ "Id": "3b6a356b-550f-4097-98c2-59b250136443" },
{ "Id": "b43f84f7-6c26-4adf-b74a-6d598f03cbe3" }
{ "Id": "b43f84f7-6c26-4adf-b74a-6d598f03cbe3" },
{ "Id": "3b6a356b-550f-4097-98c2-59b250136443" }
]
},
"UserData": {}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "GLOBAL_ARCADE_CHALLENGES",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,7 +10,51 @@
"Icon": "arcademode",
"CategoryId": "arcade",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ARCADE",
"OrderIndex": 6,
"Challenges": [
{
"Id": "9e91db0f-8b9e-469c-bed0-163404ec1eff",
"Name": "UI_ANISESTAR_COMPLETION_CHALLENGE_NAME",
"ImageName": "images/contracts/arcade/Arcade_AniseStar_Group.jpg",
"Description": "UI_ANISESTAR_COMPLETION_CHALLENGE_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": true,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ARCADE",
"Icon": "arcademode",
"LocationId": "LOCATION_MIAMI",
"ParentLocationId": "LOCATION_PARENT_MIAMI",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$eq": [
"$ContractId",
"8e7e9f08-94c5-4667-ac8a-61aa15b74c0a"
]
},
"Transition": "Success"
}
}
}
},
"Tags": ["arcade", "hard"],
"InclusionData": {
"ContractIds": ["de9788cc-b9c4-47fc-b5df-86451cd82c43"]
}
},
{
"Id": "d00653ca-d00d-4b30-bff9-3c03c358adc8",
"Name": "UI_APPLE_COMPLETION_CHALLENGE_NAME",
@ -664,7 +708,7 @@
"Rewards": {
"MasteryXP": 4000
},
"Drops": ["TOKEN_OUTFIT_HERO_BUTCHER_SUIT"],
"Drops": ["TOKEN_OUTFIT_HERO_PURPLESPECIAL_SUIT"],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,921 @@
{
"meta": {
"Location": "LOCATION_PARENT_BANGKOK",
"GameVersions": ["h2", "h3"]
},
"groups": [
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"OrderIndex": 5,
"Challenges": [
{
"Id": "3151f909-0a87-4e71-81f6-02b52405a8f1",
"Name": "UI_CHALLENGES_ET_BRASSMONKEY_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_brassmonkey_M_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_BRASSMONKEY_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_BANGKOK",
"ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": [
"$Value.RepositoryId",
"7387e648-ad6b-408d-a0ee-3b3943767e78"
]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["b0bed170-8652-4188-8b9a-92caf9f97e5b"]
}
},
{
"Id": "c2275eac-daad-429d-88ef-453eeaab33b6",
"Name": "UI_CHALLENGES_ET_BRASSMONKEY_F_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_brassmonkey_F_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_BRASSMONKEY_F_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_BANGKOK",
"ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": [
"$Value.RepositoryId",
"978ad630-8d31-4416-8976-8ed1009a4dbd"
]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["b0bed170-8652-4188-8b9a-92caf9f97e5b"]
}
},
{
"Id": "2f57c52c-cd9e-4193-9060-aa4c496ce30e",
"Name": "UI_CHALLENGES_ET_BRASSMONKEY_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_brassmonkey_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_BRASSMONKEY_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_BANGKOK",
"ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["b0bed170-8652-4188-8b9a-92caf9f97e5b"]
}
},
{
"Id": "9262f862-0960-4ade-8491-eecb522d80e2",
"Name": "UI_CHALLENGES_ET_MARTINI_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_martini_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_MARTINI_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_BANGKOK",
"ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["b0b8995c-7b3f-4fa6-91a2-be4bc8edc046"]
}
},
{
"Id": "1661ba81-60eb-4973-9a2e-e17b62483cfc",
"Name": "UI_CHALLENGES_ET_MARTINI_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_martini_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_MARTINI_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_BANGKOK",
"ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["b0b8995c-7b3f-4fa6-91a2-be4bc8edc046"]
}
},
{
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Description": "UI_CHALLENGES_ET_BLOODYMARY_TARGETDOWN_DESC",
"DifficultyLevels": [],
"Drops": [],
"HideProgression": false,
"Icon": "elusive",
"Id": "9b6e65c2-22cd-4637-8ab1-e48e07acdedc",
"ImageName": "images/challenges/elusive_target/et_bloodymary_targetdown.jpg",
"InclusionData": {
"ContractIds": ["87f8293a-29cd-4cb1-ade7-dd6bb056d38e"]
},
"IsLocked": false,
"IsPlayable": false,
"LocationId": "LOCATION_BANGKOK",
"Name": "UI_CHALLENGES_ET_BLOODYMARY_TARGETDOWN_NAME",
"OrderIndex": 10000,
"ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Rewards": {
"MasteryXP": 2000
},
"RuntimeType": "Hit",
"Tags": ["story", "medium", "elusive"],
"Type": "contract",
"XpModifier": {}
},
{
"Id": "265ff025-9e07-4224-b6db-675e44bd62b5",
"Name": "UI_CHALLENGES_ET_BLOODYMARY_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_bloodymary_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_BLOODYMARY_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_BANGKOK",
"ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["87f8293a-29cd-4cb1-ade7-dd6bb056d38e"]
}
},
{
"Id": "12937274-4d85-477d-b20d-fddb408a1015",
"Name": "UI_CHALLENGES_ELUSIVE_TARGET_BANGKOK_NAME",
"ImageName": "images/challenges/profile_challenges/elusive_target_bangkok.jpg",
"Description": "UI_CHALLENGES_ELUSIVE_TARGET_BANGKOK_DESC",
"Rewards": {
"MasteryXP": 0
},
"Drops": ["TOKEN_OUTFIT_HERO_BANGKOKSUITANDGLOVES"],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_BANGKOK",
"ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"ContractStart": {
"Condition": {
"$eq": [
"$Value.ContractType",
"elusive"
]
},
"Transition": "State_ValidContract"
}
},
"State_ValidContract": {
"ContractEnd": [
{
"Transition": "Success"
}
]
}
}
},
"Tags": ["story", "elusive"],
"InclusionData": {
"ContractTypes": ["elusive"]
}
}
]
}
]
}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_BANGKOK",
"GameVersion": "h2"
"GameVersions": ["h2"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "43737b81-d253-44c8-aac8-f64d6711bdaf",
@ -2066,6 +2067,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "8c613c59-8b59-46ac-8d4a-1f9516f9c790",
@ -2832,6 +2834,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "148e8d0e-7676-46a2-8d55-3f4680cfce58",
@ -4007,6 +4010,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "07736d0b-3c6b-48c6-9ff0-6d951cd07ee7",
@ -4100,6 +4104,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "2db8ca18-bd44-483d-b7d9-eb726b259583",
@ -8201,6 +8206,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "horror-pack",
"Description": "",
"OrderIndex": 10000,
"Challenges": [
{
"Id": "16d4b17d-ccc5-4d51-a27c-3d25c1b36fbb",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_BANGKOK",
"GameVersion": "h1"
"GameVersions": ["h1"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "701ba2b9-4611-472f-9a30-124e44b2b7ae",
@ -2708,6 +2709,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "8c613c59-8b59-46ac-8d4a-1f9516f9c790",
@ -3898,6 +3900,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "b73d4e03-d0a3-4882-b372-1ab606bf897f",
@ -5198,6 +5201,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "5dbd7d1d-3f16-45ce-994e-646247a8e61f",
@ -5293,6 +5297,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "horror-pack",
"Description": "",
"OrderIndex": 10000,
"Challenges": [
{
"Id": "8faaac29-153f-44c3-8c00-2ef8db4183c7",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_EDGY",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "2f812cf3-8ff2-4fbe-a74d-1f589cd86083",
@ -872,6 +873,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "2113732a-0518-4b50-b490-45422d9fcf29",
@ -1539,6 +1541,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "1ae5e7db-7cec-468c-bb98-0335836e8d8d",
@ -1982,6 +1985,71 @@
"ContractIds": ["12d83cb0-a2d6-4c01-b9d8-675ac635ee61"]
}
},
{
"Id": "7423cc98-31d0-4e91-943b-ceb62d0b595b",
"Name": "UI_CHALLENGE_GRASSSNAKE_EGGHUNT_NAME",
"ImageName": "images/contracts/escalation/ContractEscalation_Edgy_Grassnake_Egghunt.jpg",
"Description": "UI_CHALLENGE_GRASSSNAKE_EGGHUNT_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": ["PROP_MELEE_BLUE_EASTEREGG_PACIFYGAS"],
"IsPlayable": true,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_COMMUNITY",
"Icon": "challenge_category_feats",
"LocationId": "LOCATION_PARENT_EDGY",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Scope": "hit",
"States": {
"Start": {
"BlueEgg": {
"Transition": "CheckKill"
}
},
"CheckKill": {
"$timer": {
"Condition": {
"$after": 15
},
"Transition": "Start"
},
"Pacify": {
"Condition": {
"$and": [
{
"$eq": ["$Value.IsTarget", true]
},
{
"$any": {
"?": {
"$eq": [
"$.#",
"Sedated"
]
},
"in": "$Value.DamageEvents"
}
}
]
},
"Transition": "Success"
}
}
}
},
"Tags": ["hard", "feats"],
"InclusionData": {
"ContractIds": ["9d88605f-6871-46a8-bd46-9804ea04fca9"]
}
},
{
"Id": "a015ff25-f9e8-481e-ab60-203eb8ecac40",
"Name": "UI_CONTRACT_GRASSSNAKE_GROUP_TITLE",
@ -2070,8 +2138,8 @@
"MasteryXP": 1000
},
"Drops": [
"PROP_DEVICE_REMOTE_EXPLOSIVE_LUST",
"FIREARMS_PISTOL_DARTGUN_BLINDING_LUST"
"FIREARMS_PISTOL_DARTGUN_BLINDING_LUST",
"PROP_DEVICE_REMOTE_EXPLOSIVE_LUST"
],
"IsPlayable": true,
"IsLocked": false,
@ -2312,8 +2380,8 @@
"MasteryXP": 1000
},
"Drops": [
"PROP_MELEE_LEATHERBELT_ASYLUM",
"FIREARMS_PISTOL_DARTGUN_SEDATIVE_ASYLUM"
"FIREARMS_PISTOL_DARTGUN_SEDATIVE_ASYLUM",
"PROP_MELEE_LEATHERBELT_ASYLUM"
],
"IsPlayable": true,
"IsLocked": false,
@ -2632,7 +2700,7 @@
{
"Id": "54ddadb4-edc7-47eb-a442-9c9536626168",
"Name": "UI_PEACOCK_SHANGRILA",
"ImageName": "images/contracts/escalation/contractescalation-shangrila.png",
"ImageName": "images/contracts/escalation/contractescalation_shangrila.jpg",
"Description": "UI_CHALLENGES_ESCLATION_COMPLETE_DESC",
"Rewards": {
"MasteryXP": 4000
@ -2680,6 +2748,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "36c368c2-5808-40b5-8805-f62e458ac87c",
@ -3192,6 +3261,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "0b63d39b-c53e-4fa4-a2a3-310a973eb819",
@ -4038,585 +4108,109 @@
]
},
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Image": "images/challenges/categories/packargentum/tile.jpg",
"Icon": "challenge_category_feats",
"CategoryId": "argentum-pack",
"Description": "",
"OrderIndex": 6.5,
"Challenges": [
{
"Id": "c2ae25d2-e4d0-4125-928d-44632ff2f7d1",
"Name": "UI_CHALLENGES_ET_RADLER_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_radler_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_RADLER_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["3f0b8f19-d5d4-4611-ac8f-480f81c18f54"]
}
},
{
"Id": "00d2fefd-6bdb-4450-81bd-45a4bb3c3887",
"Name": "UI_CHALLENGES_ET_RADLER_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_radler_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_RADLER_SILENT_ASSASSIN_DESC",
"Id": "2b9f479e-6789-4f23-9eef-e17f268a2b11",
"Name": "CHALLENGEPACK_ARGENTUM_FOXEXPLODER_NAME",
"ImageName": "images/challenges/Categories/PackArgentum/Argentum_FoxExploder.jpg",
"Description": "CHALLENGEPACK_ARGENTUM_FOXEXPLODER_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsPlayable": true,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Icon": "challenge_category_feats",
"LocationId": "LOCATION_PARENT_EDGY",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"OrderIndex": 100004,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Constants": {
"Goal": 5
},
"ContextListeners": {
"Count": {
"type": "challengecounter",
"count": "$.Count",
"total": "$.Goal"
}
},
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
"Count": 0,
"Targets": [
"8b29da09-461f-44d7-9042-d4fde829b9f2",
"b8e7e65b-587e-471b-894d-282cda6614d4",
"922deccd-7fb4-45d9-ae3d-2cf11915c403",
"28cb7e91-bf9c-46ee-a371-1bd1448f1994",
"633398ac-c4b4-4441-852d-ae6460172025",
"1305c2e4-6394-4cfa-b873-22adbd0c9702",
"abd1c0e7-e406-43bd-9185-419029c5bf3d",
"eb024a5e-9580-49dc-a519-bb92c886f3b1",
"252428ca-3f8e-4477-b2b9-58f18cff3e44",
"2ab07903-e958-4af6-b01c-b62058745ce1",
"f83376a4-6e56-4f2a-8122-151b272108fd"
]
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"FoxExploderActive": {
"Transition": "ChallengeActive"
}
},
"ChallengeActive": {
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["3f0b8f19-d5d4-4611-ac8f-480f81c18f54"]
}
},
{
"Id": "d4e7f657-dfe7-4ea4-9ab6-923e209eac7b",
"Name": "UI_CHALLENGES_ET_TOMORROWLAND_CONTRACT_STARTED_NAME",
"ImageName": "images/challenges/elusive_target/ET_Tomorrowland_ContractStarted.jpg",
"Description": "UI_CHALLENGES_ET_TOMORROWLAND_CONTRACT_STARTED_DESC",
"Rewards": {
"MasteryXP": 0
},
"Drops": ["TOKEN_OUTFIT_TOMORROWLAND_SUIT_REWARD"],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"ContractStart": {
"Transition": "Success"
}
}
}
},
"Tags": ["story", "elusive"],
"InclusionData": {
"ContractIds": ["1f0f3c70-b559-48ea-aca4-b64c8c762b69"]
}
},
{
"Id": "ff37f182-1282-4e02-9cba-10396af41e17",
"Name": "UI_CHALLENGES_ET_TOMORROWLAND_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/ET_Tomorrowland_TargetDown.jpg",
"Description": "UI_CHALLENGES_ET_TOMORROWLAND_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["1f0f3c70-b559-48ea-aca4-b64c8c762b69"]
}
},
{
"Id": "caa00727-6063-44ec-ba2d-e62d42d50977",
"Name": "UI_CHALLENGES_ET_TOMORROWLAND_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/ET_Tomorrowland_SilentAssassin.jpg",
"Description": "UI_CHALLENGES_ET_TOMORROWLAND_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0.0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
"$inarray": {
"in": "$.Targets",
"?": {
"$eq": [
"$.#",
"$Value.RepositoryId"
]
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
"$Value.KillClass",
"explosion"
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
"$inc": "Count"
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
"$eq": ["$.Count", "$.Goal"]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
"Transition": "Success"
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"Tags": ["argentum-pack", "story", "live", "hard"],
"InclusionData": {
"ContractIds": ["1f0f3c70-b559-48ea-aca4-b64c8c762b69"]
"ContractIds": ["ebcd14b2-0786-4ceb-a2a4-e771f60d0125"]
}
}
]

View File

@ -0,0 +1,593 @@
{
"meta": {
"Location": "LOCATION_PARENT_EDGY",
"GameVersions": ["h3"]
},
"groups": [
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"OrderIndex": 5,
"Challenges": [
{
"Id": "c2ae25d2-e4d0-4125-928d-44632ff2f7d1",
"Name": "UI_CHALLENGES_ET_RADLER_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_radler_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_RADLER_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["3f0b8f19-d5d4-4611-ac8f-480f81c18f54"]
}
},
{
"Id": "00d2fefd-6bdb-4450-81bd-45a4bb3c3887",
"Name": "UI_CHALLENGES_ET_RADLER_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_radler_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_RADLER_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["3f0b8f19-d5d4-4611-ac8f-480f81c18f54"]
}
},
{
"Id": "d4e7f657-dfe7-4ea4-9ab6-923e209eac7b",
"Name": "UI_CHALLENGES_ET_TOMORROWLAND_CONTRACT_STARTED_NAME",
"ImageName": "images/challenges/elusive_target/ET_Tomorrowland_ContractStarted.jpg",
"Description": "UI_CHALLENGES_ET_TOMORROWLAND_CONTRACT_STARTED_DESC",
"Rewards": {
"MasteryXP": 0
},
"Drops": ["TOKEN_OUTFIT_TOMORROWLAND_SUIT_REWARD"],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"ContractStart": {
"Transition": "Success"
}
}
}
},
"Tags": ["story", "elusive"],
"InclusionData": {
"ContractIds": ["1f0f3c70-b559-48ea-aca4-b64c8c762b69"]
}
},
{
"Id": "ff37f182-1282-4e02-9cba-10396af41e17",
"Name": "UI_CHALLENGES_ET_TOMORROWLAND_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/ET_Tomorrowland_TargetDown.jpg",
"Description": "UI_CHALLENGES_ET_TOMORROWLAND_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["1f0f3c70-b559-48ea-aca4-b64c8c762b69"]
}
},
{
"Id": "caa00727-6063-44ec-ba2d-e62d42d50977",
"Name": "UI_CHALLENGES_ET_TOMORROWLAND_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/ET_Tomorrowland_SilentAssassin.jpg",
"Description": "UI_CHALLENGES_ET_TOMORROWLAND_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_EDGY_FOX",
"ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0.0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["1f0f3c70-b559-48ea-aca4-b64c8c762b69"]
}
}
]
}
]
}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_TRAPPED",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "3d0ff5c3-6e51-4827-951f-d45d8d21b3ea",
@ -101,6 +102,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "18636f91-e1a9-4091-935c-adbf6cc3de55",
@ -587,6 +589,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "22070cef-2ecb-4a65-ab16-cb4bfd63785e",
@ -676,8 +679,8 @@
"MasteryXP": 1000
},
"Drops": [
"PROP_MELEE_KATANA_WHITE_NINJA",
"FIREARMS_SNIPER_SIEGER_300_WHITE_NINJA"
"FIREARMS_SNIPER_SIEGER_300_WHITE_NINJA",
"PROP_MELEE_KATANA_WHITE_NINJA"
],
"IsPlayable": true,
"IsLocked": false,
@ -1146,6 +1149,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "3210fa62-67a5-4e1f-ba50-02c451d47875",
@ -1200,6 +1204,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "92643706-6f98-4b1c-9700-ee4819aaadb4",
@ -1725,6 +1730,145 @@
}
}
]
},
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Image": "images/challenges/categories/packargentum/tile.jpg",
"Icon": "challenge_category_feats",
"CategoryId": "argentum-pack",
"Description": "",
"OrderIndex": 6.5,
"Challenges": [
{
"Id": "1bcab2a7-626e-4bf2-a9af-7adb823cdd4a",
"Name": "CHALLENGEPACK_ARGENTUM_WOLVERINEFINISHER_NAME",
"ImageName": "images/challenges/Categories/PackArgentum/Argentum_WolverineFinisher.jpg",
"Description": "CHALLENGEPACK_ARGENTUM_WOLVERINEFINISHER_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": true,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Icon": "challenge_category_feats",
"LocationId": "LOCATION_PARENT_TRAPPED",
"ParentLocationId": "LOCATION_PARENT_TRAPPED",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 100005,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Constants": {
"Goal": 47
},
"ContextListeners": {
"Count": {
"type": "challengecounter",
"count": "$.Count",
"total": "$.Goal"
}
},
"Context": {
"Count": 0,
"Targets": [
"006bff27-d8b9-480e-9308-fac9a447d038",
"0f8fd017-f30f-48aa-bb95-f0eed8f99eb0",
"1063ec04-ae7f-47ce-9f67-e3ed87b910fb",
"10a1344f-25b5-4d9b-99df-cc1a3e132701",
"16bc3b56-63a5-48fd-beab-d4d2adf13b29",
"17aa42ce-9de5-42c3-a0d7-42650aeb5ccb",
"28a8ac36-edba-4b02-81f9-4d8e85a6e37c",
"29f7350c-f28d-4756-b03b-8158806c1a19",
"2db48fc3-1f43-4681-82b4-26289c7373bc",
"3354e12a-3e7b-4693-bb63-a73d12a682e3",
"49c4b42b-773d-416a-a48d-5cb7f6254290",
"4c549646-3605-406a-8eb6-a56da52045a1",
"56ba1725-4d2b-4ba0-9842-79457af2eca3",
"5a62d8d2-1208-4f11-b95d-8f6cb11cd02b",
"5b82b41f-f95e-4403-919c-6a67522fc94e",
"606d9bbb-76b7-4e0f-a24f-e5b7a1257313",
"6504cb26-c5d0-4c99-9aa2-7df9d18c5201",
"6c232a4d-cd64-42d8-a78f-e0570dae7cd2",
"6ecd7f77-bebc-4f7a-939d-7e16f4b678b1",
"71854eb2-8e6c-4d19-83d2-fbdc7879bf6f",
"759c67d0-18e2-49b2-a417-5daa9a450819",
"796b0ee9-9261-4e55-ab6f-5a4c53f86e4e",
"7bba60be-4b86-4a3c-be67-3937ab982ef2",
"8255c9aa-1a59-4750-89f1-46542207e00d",
"82615a87-f968-4be8-82d9-eb8089ad92ee",
"8d6aebab-d94a-4adf-8770-5985a3809ece",
"9830d3c6-93e3-49c8-b0c8-abdf3dc018ce",
"a0766e5c-fadb-4900-8cb1-c776b56fabf5",
"a3570e03-43f8-46ae-896a-e0a5803cfb6f",
"a6f7e9f3-e25c-48c6-8f0b-755a2e864123",
"aaafb8c3-8697-4121-8c97-1ec2c3502742",
"b3840fb0-e732-4f35-b858-fc4bed036e46",
"bb3efb54-cb83-4c63-adda-12b7da545a77",
"bb7b6b31-adce-4a50-91fe-7447128b874b",
"bbfbf4da-e6e1-4f14-b2ee-9325f9615408",
"bdf572b3-7e6e-46f6-98b6-887ec99804de",
"c2dcbadf-4101-4172-b458-0f02b9145c0b",
"c7840085-c12a-4438-b032-f54bbe006030",
"cd6e9ee7-282c-49fa-a537-1802b4749e2c",
"ce95c773-3153-4909-a356-16f071297582",
"ced15cfa-21f7-4078-9ee7-f8b3ef1b1b59",
"cf3c9e6e-4e3c-43dd-a663-897ffaa165f2",
"d47a96d9-91cb-42cf-8193-6bccc74a775e",
"d4fea004-6a7d-409f-bf4b-b229ca3cb355",
"dca83dc1-c96e-4b75-b252-8abddd08617d",
"f423f166-ed6d-4c3b-8d55-cd1c8a8b84ac",
"38e6e74d-fe56-4ae8-8194-d5ec44c4d335"
]
},
"Scope": "session",
"States": {
"Start": {
"WolverineFinisherActive": {
"Transition": "ChallengeActive"
}
},
"ChallengeActive": {
"Kill": [
{
"Condition": {
"$inarray": {
"in": "$.Targets",
"?": {
"$eq": [
"$.#",
"$Value.RepositoryId"
]
}
}
},
"Actions": {
"$inc": "Count"
}
},
{
"Condition": {
"$eq": ["$.Count", "$.Goal"]
},
"Transition": "AllKilled"
}
]
},
"AllKilled": {
"EndingTriggered": {
"Transition": "Success"
}
}
}
},
"Tags": ["argentum-pack", "story", "live", "hard"],
"InclusionData": {
"ContractIds": ["a3e19d55-64a6-4282-bb3c-d18c3f3e6e29"]
}
}
]
}
]
}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_WET",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "00fcea23-9659-447a-a717-9502810e2930",
@ -880,6 +881,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "04ef301f-a62d-47f6-8cb8-3b8697e0c06c",
@ -1709,6 +1711,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "1a68c357-02a0-40a2-99e7-73d76ec5095d",
@ -3379,6 +3382,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "09675ea9-4cf3-4b80-a722-329b8e48e687",
@ -3472,6 +3476,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "0af3980d-6bcb-42f6-ac4c-d0241606500a",
@ -4318,31 +4323,32 @@
]
},
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Image": "images/challenges/categories/packargentum/tile.jpg",
"Icon": "challenge_category_feats",
"CategoryId": "argentum-pack",
"Description": "",
"OrderIndex": 6.5,
"Challenges": [
{
"Id": "cfd77118-ee34-49b0-b2e6-0fcbb69948f6",
"Name": "UI_CHALLENGES_ET_REDSNAPPER_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_redsnapper_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_REDSNAPPER_TARGETDOWN_DESC",
"Id": "f8ba2bd3-dcd7-453f-aa64-c0ed6992e24b",
"Name": "CHALLENGEPACK_ARGENTUM_RATSNIPER_NAME",
"ImageName": "images/challenges/Categories/PackArgentum/Argentum_RatSniper.jpg",
"Description": "CHALLENGEPACK_ARGENTUM_RATSNIPER_DESC",
"Rewards": {
"MasteryXP": 2000
"MasteryXP": 1000
},
"Drops": [],
"IsPlayable": false,
"IsPlayable": true,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Icon": "challenge_category_feats",
"LocationId": "LOCATION_WET_RAT",
"ParentLocationId": "LOCATION_PARENT_WET",
"Type": "contract",
"Type": "location",
"DifficultyLevels": [],
"OrderIndex": 10000,
"OrderIndex": 100001,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
@ -4350,248 +4356,125 @@
"Scope": "session",
"States": {
"Start": {
"EnterARGENTUM_Volume": {
"Transition": "InPosition"
}
},
"InPosition": {
"ExitARGENTUM_Volume": {
"Transition": "Start"
},
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
"$and": [
{
"$eq": ["$Value.IsTarget", true]
},
{
"$eq": [
"$Value.KillItemCategory",
"sniperrifle"
]
},
{
"$eq": [
"$Value.IsHeadshot",
true
]
}
]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["6fad7901-279f-45df-ab8d-087a3cb06dcc"]
}
},
"Tags": ["argentum-pack", "story", "live", "easy"]
}
]
},
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Image": "images/challenges/categories/packcheesecake/tile.jpg",
"Icon": "challenge_category_feats",
"CategoryId": "cheesecake-pack",
"Description": "",
"OrderIndex": 6.1,
"Challenges": [
{
"Id": "c823eaa7-aaec-42e5-a160-7b7d7b3c719e",
"Name": "UI_CHALLENGES_ET_REDSNAPPER_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_redsnapper_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_REDSNAPPER_SILENT_ASSASSIN_DESC",
"Id": "eff06c02-2410-4226-1abc-076a2e71ee97",
"Name": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_CHONGQINGMARINATE_NAME",
"ImageName": "images/challenges/categories/packcheesecake/cheesecake_chongqingmarinate.jpg",
"Description": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_CHONGQINGMARINATE_DESC",
"Rewards": {
"MasteryXP": 4000
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsPlayable": true,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_WET_RAT",
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Icon": "challenge_category_feats",
"LocationId": "LOCATION_PARENT_WET",
"ParentLocationId": "LOCATION_PARENT_WET",
"Type": "contract",
"Type": "parentlocation",
"DifficultyLevels": [],
"OrderIndex": 10000,
"OrderIndex": 100003,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"Context": { "PoisonedTargets": [] },
"States": {
"Start": {
"ContractEnd": {
"Kill": {
"Condition": {
"$and": [
{
"$eq": ["$Value.IsTarget", true]
},
{
"$eq": [
true,
"$.RecordingDestroyed"
"$Value.KillClass",
"poison"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
"$eq": [
"$Value.OutfitRepositoryId",
"c5f6dd2a-3600-40be-9a82-bbf5d360c379"
]
}
]
},
"Actions": {
"$pushunique": [
"PoisonedTargets",
"$Value.RepositoryId"
]
},
"Transition": "CheckDumpInOcean"
}
},
"CheckDumpInOcean": {
"DumpInOcean": {
"Condition": {
"$inarray": {
"in": "$.PoisonedTargets",
"?": {
"$eq": [
"$.#",
"$Value.RepositoryId"
]
}
}
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["6fad7901-279f-45df-ab8d-087a3cb06dcc"]
}
"Tags": ["cheesecake-pack", "story", "live", "medium"]
}
]
}

View File

@ -0,0 +1,287 @@
{
"meta": {
"Location": "LOCATION_PARENT_WET",
"GameVersions": ["h3"]
},
"groups": [
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"OrderIndex": 5,
"Challenges": [
{
"Id": "cfd77118-ee34-49b0-b2e6-0fcbb69948f6",
"Name": "UI_CHALLENGES_ET_REDSNAPPER_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_redsnapper_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_REDSNAPPER_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_WET_RAT",
"ParentLocationId": "LOCATION_PARENT_WET",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["6fad7901-279f-45df-ab8d-087a3cb06dcc"]
}
},
{
"Id": "c823eaa7-aaec-42e5-a160-7b7d7b3c719e",
"Name": "UI_CHALLENGES_ET_REDSNAPPER_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_redsnapper_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_REDSNAPPER_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_WET_RAT",
"ParentLocationId": "LOCATION_PARENT_WET",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["6fad7901-279f-45df-ab8d-087a3cb06dcc"]
}
}
]
}
]
}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_COLORADO",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "8ade8bd8-f23d-4c7a-b453-231c3c93f8f0",
@ -2424,6 +2425,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "12790e00-2e8f-4fb4-a492-074520fbae13",
@ -3057,6 +3059,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "0324a4ae-6e73-4736-8c67-09be6263f847",
@ -3963,6 +3966,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "22709cab-3fb9-423e-a297-1bde39485aad",
@ -4187,6 +4191,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "5146dec1-9b6e-4037-9462-134348b1722d",
@ -5260,603 +5265,6 @@
}
}
]
},
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"Challenges": [
{
"Id": "d8f8a8fb-4b3c-4e2a-ac20-2d864a449ebe",
"Name": "UI_CHALLENGES_ET_BUSHWACKER_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_bushwacker_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_BUSHWACKER_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["550c4d75-ca87-4be7-a18e-caf30e6c8136"]
}
},
{
"Id": "bade239d-1102-4ddf-9dfa-c848c007bf3e",
"Name": "UI_CHALLENGES_ET_BUSHWACKER_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_bushwacker_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_BUSHWACKER_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["550c4d75-ca87-4be7-a18e-caf30e6c8136"]
}
},
{
"Id": "3d2e38c4-a96c-43cb-bec1-d775e28f3fd2",
"Name": "UI_CHALLENGES_ET_FLIRTINI_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_flirtini_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_FLIRTINI_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["655c5a57-69d1-48b6-a14b-2ae396c16174"]
}
},
{
"Id": "96377c1f-e2e4-4008-bf9f-ced0e9a016e9",
"Name": "UI_CHALLENGES_ET_FLIRTINI_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_flirtini_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_FLIRTINI_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["655c5a57-69d1-48b6-a14b-2ae396c16174"]
}
},
{
"Id": "efd539cf-7b0e-4940-b8dd-a7407deea12f",
"Name": "UI_CHALLENGES_ELUSIVE_TARGET_COLORADO_NAME",
"ImageName": "images/challenges/profile_challenges/elusive_target_colorado.jpg",
"Description": "UI_CHALLENGES_ELUSIVE_TARGET_COLORADO_DESC",
"Rewards": {
"MasteryXP": 0
},
"Drops": ["TOKEN_OUTFIT_HERO_COLORADOSUIT_ALTERNATIVE"],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"ContractStart": {
"Condition": {
"$eq": [
"$Value.ContractType",
"elusive"
]
},
"Transition": "State_ValidContract"
}
},
"State_ValidContract": {
"ContractEnd": [
{
"Transition": "Success"
}
]
}
}
},
"Tags": ["story", "elusive"],
"InclusionData": {
"ContractTypes": ["elusive"]
}
}
]
}
]
}

View File

@ -0,0 +1,606 @@
{
"meta": {
"Location": "LOCATION_PARENT_COLORADO",
"GameVersions": ["h2", "h3"]
},
"groups": [
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"OrderIndex": 5,
"Challenges": [
{
"Id": "d8f8a8fb-4b3c-4e2a-ac20-2d864a449ebe",
"Name": "UI_CHALLENGES_ET_BUSHWACKER_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_bushwacker_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_BUSHWACKER_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["550c4d75-ca87-4be7-a18e-caf30e6c8136"]
}
},
{
"Id": "bade239d-1102-4ddf-9dfa-c848c007bf3e",
"Name": "UI_CHALLENGES_ET_BUSHWACKER_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_bushwacker_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_BUSHWACKER_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["550c4d75-ca87-4be7-a18e-caf30e6c8136"]
}
},
{
"Id": "3d2e38c4-a96c-43cb-bec1-d775e28f3fd2",
"Name": "UI_CHALLENGES_ET_FLIRTINI_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_flirtini_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_FLIRTINI_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["655c5a57-69d1-48b6-a14b-2ae396c16174"]
}
},
{
"Id": "96377c1f-e2e4-4008-bf9f-ced0e9a016e9",
"Name": "UI_CHALLENGES_ET_FLIRTINI_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_flirtini_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_FLIRTINI_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["655c5a57-69d1-48b6-a14b-2ae396c16174"]
}
},
{
"Id": "efd539cf-7b0e-4940-b8dd-a7407deea12f",
"Name": "UI_CHALLENGES_ELUSIVE_TARGET_COLORADO_NAME",
"ImageName": "images/challenges/profile_challenges/elusive_target_colorado.jpg",
"Description": "UI_CHALLENGES_ELUSIVE_TARGET_COLORADO_DESC",
"Rewards": {
"MasteryXP": 0
},
"Drops": ["TOKEN_OUTFIT_HERO_COLORADOSUIT_ALTERNATIVE"],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_COLORADO",
"ParentLocationId": "LOCATION_PARENT_COLORADO",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"ContractStart": {
"Condition": {
"$eq": [
"$Value.ContractType",
"elusive"
]
},
"Transition": "State_ValidContract"
}
},
"State_ValidContract": {
"ContractEnd": [
{
"Transition": "Success"
}
]
}
}
},
"Tags": ["story", "elusive"],
"InclusionData": {
"ContractTypes": ["elusive"]
}
}
]
}
]
}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_COLORADO",
"GameVersion": "h2"
"GameVersions": ["h2"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "8ade8bd8-f23d-4c7a-b453-231c3c93f8f0",
@ -1864,6 +1865,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "12790e00-2e8f-4fb4-a492-074520fbae13",
@ -2395,6 +2397,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "0324a4ae-6e73-4736-8c67-09be6263f847",
@ -3575,6 +3578,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "22709cab-3fb9-423e-a297-1bde39485aad",
@ -3752,6 +3756,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "5146dec1-9b6e-4037-9462-134348b1722d",
@ -7117,6 +7122,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "scare-pack",
"Description": "",
"OrderIndex": 10000,
"Challenges": [
{
"Id": "2f1ef5b2-2df9-428f-ad62-9df951864152",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_COLORADO",
"GameVersion": "h1"
"GameVersions": ["h1"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "5146dec1-9b6e-4037-9462-134348b1722d",
@ -2297,6 +2298,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "47b2298d-d772-4926-ac60-05085eaa17a1",
@ -3258,6 +3260,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "0324a4ae-6e73-4736-8c67-09be6263f847",
@ -4667,6 +4670,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "22709cab-3fb9-423e-a297-1bde39485aad",
@ -4848,6 +4852,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "scare-pack",
"Description": "",
"OrderIndex": 10000,
"Challenges": [
{
"Id": "ab5edd71-b1f9-416c-bd62-46129b75a1dc",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_ANCESTRAL",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "150f0c8a-34a7-4bdf-8d92-97915c8ac64d",
@ -1171,6 +1172,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "43186811-5dab-4378-be8d-3e742eac6159",
@ -2079,6 +2081,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "12e2d93c-1433-429f-a802-32e611a7ba64",
@ -4587,7 +4590,7 @@
{
"Id": "2934a0ea-8e0a-4aee-bc38-f3a015274746",
"Name": "UI_PEACOCK_ROSEBUSH",
"ImageName": "images/contracts/escalation/contractescalation_rosebush.png",
"ImageName": "images/contracts/escalation/contractescalation_rosebush.jpg",
"Description": "UI_CHALLENGES_ESCLATION_COMPLETE_DESC",
"Rewards": {
"MasteryXP": 4000
@ -4635,6 +4638,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "1524234b-6f85-426d-92d6-a2dc6d42cfbd",
@ -4686,6 +4690,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "647278f4-e20f-4c42-8a8b-5d2cf30591c0",
@ -5530,623 +5535,6 @@
}
}
]
},
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"Challenges": [
{
"Id": "aed4cac6-0f11-4e74-9a7c-90748b3c7384",
"Name": "UI_CHALLENGES_ET_BRAMBLE_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_bramble_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_BRAMBLE_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["92951377-419d-4c31-aa21-2a3f03ef82d0"]
}
},
{
"Id": "5c9ebf15-0f66-4a3c-8117-b0515b81f6f1",
"Name": "UI_CHALLENGES_ET_BRAMBLE_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_bramble_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_BRAMBLE_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["92951377-419d-4c31-aa21-2a3f03ef82d0"]
}
},
{
"Id": "009da118-9bbd-4378-a3af-7eb6f65467e0",
"Name": "UI_CHALLENGES_ET_BRAMBLE_RETRIEVEPAINTING_NAME",
"ImageName": "images/challenges/elusive_target/et_bramble_retrievedpainting.jpg",
"Description": "UI_CHALLENGES_ET_BRAMBLE_RETRIEVEPAINTING_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Constants": {
"Target": "49853af0-d50b-4959-a446-15429b1f4530"
},
"Scope": "session",
"States": {
"Start": {
"ItemPickedUp": {
"Condition": {
"$eq": [
"$.Target",
"$Value.RepositoryId"
]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["92951377-419d-4c31-aa21-2a3f03ef82d0"]
}
},
{
"Id": "e33b5539-b15e-4652-be2b-f154f12336fd",
"Name": "UI_CHALLENGES_ET_VESPER_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_vesper_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_VESPER_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Constants": {
"RequiredTargetCount": 2
},
"Context": {
"KilledTargetCount": 0
},
"ContextListeners": {
"KilledTargetCount": {
"type": "challengecounter",
"count": "$.KilledTargetCount",
"total": "$.RequiredTargetCount"
}
},
"Scope": "session",
"States": {
"Start": {
"Kill": [
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$inc": "KilledTargetCount"
}
},
{
"Condition": {
"$eq": [
"$.KilledTargetCount",
"$.RequiredTargetCount"
]
},
"Transition": "Success"
}
]
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["1fcaff1b-7fa3-4b9f-a586-9c7a1689b48d"]
}
},
{
"Id": "4dfe894f-a690-4d7e-bcf7-778fe8cf0647",
"Name": "UI_CHALLENGES_ET_VESPER_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_vesper_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_VESPER_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["1fcaff1b-7fa3-4b9f-a586-9c7a1689b48d"]
}
}
]
}
]
}

View File

@ -0,0 +1,626 @@
{
"meta": {
"Location": "LOCATION_PARENT_ANCESTRAL",
"GameVersions": ["h3"]
},
"groups": [
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"OrderIndex": 5,
"Challenges": [
{
"Id": "aed4cac6-0f11-4e74-9a7c-90748b3c7384",
"Name": "UI_CHALLENGES_ET_BRAMBLE_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_bramble_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_BRAMBLE_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["92951377-419d-4c31-aa21-2a3f03ef82d0"]
}
},
{
"Id": "5c9ebf15-0f66-4a3c-8117-b0515b81f6f1",
"Name": "UI_CHALLENGES_ET_BRAMBLE_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_bramble_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_BRAMBLE_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["92951377-419d-4c31-aa21-2a3f03ef82d0"]
}
},
{
"Id": "009da118-9bbd-4378-a3af-7eb6f65467e0",
"Name": "UI_CHALLENGES_ET_BRAMBLE_RETRIEVEPAINTING_NAME",
"ImageName": "images/challenges/elusive_target/et_bramble_retrievedpainting.jpg",
"Description": "UI_CHALLENGES_ET_BRAMBLE_RETRIEVEPAINTING_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Constants": {
"Target": "49853af0-d50b-4959-a446-15429b1f4530"
},
"Scope": "session",
"States": {
"Start": {
"ItemPickedUp": {
"Condition": {
"$eq": [
"$.Target",
"$Value.RepositoryId"
]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["92951377-419d-4c31-aa21-2a3f03ef82d0"]
}
},
{
"Id": "e33b5539-b15e-4652-be2b-f154f12336fd",
"Name": "UI_CHALLENGES_ET_VESPER_TARGETDOWN_NAME",
"ImageName": "images/challenges/elusive_target/et_vesper_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_VESPER_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Constants": {
"RequiredTargetCount": 2
},
"Context": {
"KilledTargetCount": 0
},
"ContextListeners": {
"KilledTargetCount": {
"type": "challengecounter",
"count": "$.KilledTargetCount",
"total": "$.RequiredTargetCount"
}
},
"Scope": "session",
"States": {
"Start": {
"Kill": [
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$inc": "KilledTargetCount"
}
},
{
"Condition": {
"$eq": [
"$.KilledTargetCount",
"$.RequiredTargetCount"
]
},
"Transition": "Success"
}
]
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["1fcaff1b-7fa3-4b9f-a586-9c7a1689b48d"]
}
},
{
"Id": "4dfe894f-a690-4d7e-bcf7-778fe8cf0647",
"Name": "UI_CHALLENGES_ET_VESPER_SILENT_ASSASSIN_NAME",
"ImageName": "images/challenges/elusive_target/et_vesper_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_VESPER_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_ANCESTRAL_BULLDOG",
"ParentLocationId": "LOCATION_PARENT_ANCESTRAL",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["1fcaff1b-7fa3-4b9f-a586-9c7a1689b48d"]
}
}
]
}
]
}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_GOLDEN",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "02eed2d0-872d-4c42-a813-f7083686a8c1",
@ -953,6 +954,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "0d160e0a-99ad-4ec3-9865-30d8da8076df",
@ -2062,6 +2064,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "0c668dd3-bd73-4246-adf0-87b4c8e38bda",
@ -3265,6 +3268,7 @@
"Icon": "challenge_category_targets",
"CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [
{
"Id": "16b2d5e1-43a2-4cea-adf1-1d6af2fe8df6",
@ -3358,6 +3362,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "2694b2ff-224c-4bde-858d-1671a1dbc580",
@ -4204,280 +4209,112 @@
]
},
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Image": "images/challenges/categories/packcheesecake/tile.jpg",
"Icon": "challenge_category_feats",
"CategoryId": "cheesecake-pack",
"Description": "",
"OrderIndex": 6.1,
"Challenges": [
{
"Id": "3beb74aa-45f7-4b70-bbf0-75045f6c525a",
"Name": "UI_CHALLENGES_ET_GIBSON_TARGETDOWN_TITLE",
"ImageName": "images/challenges/elusive_target/et_gibson_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_GIBSON_TARGETDOWN_DESC",
"Id": "c098a7cf-8c49-02ce-475e-20beaed99712",
"Name": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_DUBAIPANPACIFY_NAME",
"ImageName": "images/challenges/categories/packcheesecake/cheesecake_dubaipanpacify.jpg",
"Description": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_DUBAIPANPACIFY_DESC",
"Rewards": {
"MasteryXP": 2000
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsPlayable": true,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_GOLDEN_GECKO",
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Icon": "challenge_category_feats",
"LocationId": "LOCATION_PARENT_GOLDEN",
"ParentLocationId": "LOCATION_PARENT_GOLDEN",
"Type": "contract",
"Type": "parentlocation",
"DifficultyLevels": [],
"OrderIndex": 10000,
"OrderIndex": 100005,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"Constants": {
"Goal": 2
},
"Context": {
"PacifiedTargets": 0
},
"States": {
"Start": {
"Kill": {
"setpieces": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
"$eq": [
"$Value.RepositoryId",
"fbfa76d6-9f9b-40dd-869a-2b3fc2361ce5"
]
},
"Transition": "MeetingRoomLocked"
}
},
"MeetingRoomLocked": {
"Pacify": {
"Condition": {
"$and": [
{
"$or": [
{
"$eq": [
"$Value.RepositoryId",
"bd0689d6-07b4-4757-b8ee-cac19f1c9e16"
]
},
{
"$eq": [
"$Value.RepositoryId",
"9571d196-8d67-4d94-8dad-6e2d970d7a91"
]
}
]
},
{
"$eq": [
"$Value.KillItemRepositoryId",
"bce6ce09-6ead-4d72-8438-2c7780770e70"
]
}
]
},
"Actions": {
"$inc": "PacifiedTargets"
},
"Transition": "CheckCount"
}
},
"CheckCount": {
"-": [
{
"Condition": {
"$eq": [
"$.PacifiedTargets",
"$.Goal"
]
},
"Transition": "AwaitingContractEnd"
},
{
"Transition": "MeetingRoomLocked"
}
]
},
"AwaitingContractEnd": {
"ContractEnd": {
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["b2c0251e-1803-4e12-b860-b9fa6ce5c004"]
}
},
{
"Id": "05efca7c-7184-4792-8d40-fb1d0912ab68",
"Name": "UI_CHALLENGES_ET_GIBSON_SILENT_ASSASSIN_TITLE",
"ImageName": "images/challenges/elusive_target/et_gibson_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_GIBSON_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_GOLDEN_GECKO",
"ParentLocationId": "LOCATION_PARENT_GOLDEN",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["b2c0251e-1803-4e12-b860-b9fa6ce5c004"]
}
"Tags": ["cheesecake-pack", "story", "live", "medium"]
}
]
}

View File

@ -0,0 +1,287 @@
{
"meta": {
"Location": "LOCATION_PARENT_GOLDEN",
"GameVersions": ["h3"]
},
"groups": [
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Image": "images/challenges/categories/elusive/tile.jpg",
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"OrderIndex": 5,
"Challenges": [
{
"Id": "3beb74aa-45f7-4b70-bbf0-75045f6c525a",
"Name": "UI_CHALLENGES_ET_GIBSON_TARGETDOWN_TITLE",
"ImageName": "images/challenges/elusive_target/et_gibson_targetdown.jpg",
"Description": "UI_CHALLENGES_ET_GIBSON_TARGETDOWN_DESC",
"Rewards": {
"MasteryXP": 2000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_GOLDEN_GECKO",
"ParentLocationId": "LOCATION_PARENT_GOLDEN",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {},
"Scope": "session",
"States": {
"Start": {
"Kill": {
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Transition": "Success"
}
}
}
},
"Tags": ["story", "medium", "elusive"],
"InclusionData": {
"ContractIds": ["b2c0251e-1803-4e12-b860-b9fa6ce5c004"]
}
},
{
"Id": "05efca7c-7184-4792-8d40-fb1d0912ab68",
"Name": "UI_CHALLENGES_ET_GIBSON_SILENT_ASSASSIN_TITLE",
"ImageName": "images/challenges/elusive_target/et_gibson_silentassassin.jpg",
"Description": "UI_CHALLENGES_ET_GIBSON_SILENT_ASSASSIN_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": [],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
"Icon": "elusive",
"LocationId": "LOCATION_GOLDEN_GECKO",
"ParentLocationId": "LOCATION_PARENT_GOLDEN",
"Type": "contract",
"DifficultyLevels": [],
"OrderIndex": 10000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session",
"States": {
"Start": {
"ContractEnd": {
"Condition": {
"$and": [
{
"$eq": [
true,
"$.RecordingDestroyed"
]
},
{
"$all": {
"in": "$.Witnesses",
"?": {
"$any": {
"in": "$.KilledTargets",
"?": {
"$eq": [
"$.#",
"$.##"
]
}
}
}
}
}
]
},
"Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Spotted": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
}
}
}
},
"Kill": [
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$not": {
"$eq": [
"$Value.KillContext",
1
]
}
}
]
},
"Transition": "Failure"
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsTarget",
false
]
},
{
"$eq": [
"$Value.KillContext",
1
]
}
]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
},
{
"Condition": {
"$eq": ["$Value.IsTarget", true]
},
"Actions": {
"$pushunique": [
"KilledTargets",
"$Value.RepositoryId"
]
}
}
],
"CrowdNPC_Died": {
"Transition": "Failure"
},
"MurderedBodySeen": [
{
"Condition": {
"$eq": [
"$Value.IsWitnessTarget",
true
]
},
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{
"$eq": [
"$Value.IsWitnessTarget",
false
]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
}
]
},
"Transition": "Failure"
}
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
]
},
"Condition": {
"$eq": ["$Value.event", "spotted"]
}
},
{
"Actions": {
"$set": ["RecordingDestroyed", true]
},
"Condition": {
"$or": [
{
"$eq": [
"$Value.event",
"erased"
]
},
{
"$eq": [
"$Value.event",
"destroyed"
]
}
]
}
}
]
}
}
},
"Tags": ["story", "hard", "elusive"],
"InclusionData": {
"ContractIds": ["b2c0251e-1803-4e12-b860-b9fa6ce5c004"]
}
}
]
}
]
}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_ICA_FACILITY",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "0b0289de-73c9-42e5-8133-b2dbe46b454b",
@ -600,6 +601,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "5b630176-2d35-4328-b1b2-51df4c2c0bb3",
@ -1088,6 +1090,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "18f5e1fd-736c-44de-a4e1-c3e70357ec7c",
@ -2290,6 +2293,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "85bbb639-40e3-4aa5-87aa-388a1cad2404",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_ICA_FACILITY",
"GameVersion": "h2"
"GameVersions": ["h2"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "0b0289de-73c9-42e5-8133-b2dbe46b454b",
@ -590,6 +591,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "210f75bf-8094-4a4a-90f3-da23b4e73ad9",
@ -1109,6 +1111,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "18f5e1fd-736c-44de-a4e1-c3e70357ec7c",
@ -2216,6 +2219,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "068dc309-f4e7-4551-9719-31e064f4cc6e",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "LOCATION_PARENT_ICA_FACILITY",
"GameVersion": "h1"
"GameVersions": ["h1"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination",
"CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [
{
"Id": "eed87874-5a23-4230-a5ba-220e90372721",
@ -974,6 +975,7 @@
"Icon": "challenge_category_discovery",
"CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [
{
"Id": "72db2871-a24c-4fa9-8f78-25c9a004ee02",
@ -1523,6 +1525,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "c984bb30-920c-4d0e-9088-06f7c9b4629e",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "GLOBAL_FEATURED_CHALLENGES",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "featured",
"CategoryId": "featured_hm1_hm2",
"Description": "",
"OrderIndex": 9,
"Challenges": [
{
"Id": "3d6eb80b-16af-4504-b23a-3465578b2cf9",
@ -1216,6 +1217,7 @@
"Icon": "featured",
"CategoryId": "featured_hm3",
"Description": "",
"OrderIndex": 10,
"Challenges": [
{
"Id": "2f58dac5-8170-4e85-a1b8-1c1cdca9cbd3",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "GLOBAL_FEATURED_CHALLENGES",
"GameVersion": "h2"
"GameVersions": ["h2"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "featured",
"CategoryId": "featured",
"Description": "",
"OrderIndex": 11,
"Challenges": [
{
"Id": "31e8e58f-86c1-4f1b-9341-d312cd9f28f8",

View File

@ -0,0 +1,109 @@
{
"meta": {
"Location": "GLOBAL_ARGENTUM_CHALLENGES",
"GameVersions": ["h3"]
},
"groups": [
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Image": "images/challenges/categories/packargentum/tile.jpg",
"Icon": "challenge_category_feats",
"CategoryId": "argentum-pack",
"Description": "",
"OrderIndex": 6.5,
"Challenges": [
{
"Id": "eca7ab97-c312-4d71-81ce-45146dd19123",
"Name": "CHALLENGEPACK_ARGENTUM_WRAPPER_NAME",
"ImageName": "images/challenges/Categories/PackArgentum/Argentum_Wrapper.jpg",
"Description": "CHALLENGEPACK_ARGENTUM_WRAPPER_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": ["TOKEN_OUTFIT_REWARD_HERO_LEGACY47_SUIT"],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Icon": "challenge_category_feats",
"LocationId": "",
"ParentLocationId": "",
"Type": "global",
"DifficultyLevels": [],
"OrderIndex": 1,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Scope": "profile",
"Constants": {
"RequiredChallenges": [
"f8ba2bd3-dcd7-453f-aa64-c0ed6992e24b",
"86e11742-f0e6-4594-b914-9ab0d8f7ab1b",
"5b943968-7142-406e-bd6c-9b9018554667",
"2b9f479e-6789-4f23-9eef-e17f268a2b11",
"1bcab2a7-626e-4bf2-a9af-7adb823cdd4a"
]
},
"Context": {
"CompletedChallenges": []
},
"ContextListeners": {
"CompletedChallenges": {
"comparand": "$.RequiredChallenges",
"type": "challengetree"
}
},
"States": {
"Start": {
"ChallengeCompleted": [
{
"Condition": {
"$any": {
"?": {
"$eq": [
"$.#",
"$Value.ChallengeId"
]
},
"in": "$.RequiredChallenges"
}
},
"$pushunique": [
"CompletedChallenges",
"$Value.ChallengeId"
]
},
{
"Condition": {
"$eq": [
"($.CompletedChallenges).Count",
"($.RequiredChallenges).Count"
]
},
"Transition": "Success"
}
]
}
}
},
"Tags": ["argentum-pack", "story", "hard"],
"InclusionData": {
"ContractIds": [
"ebcd14b2-0786-4ceb-a2a4-e771f60d0125",
"a3e19d55-64a6-4282-bb3c-d18c3f3e6e29"
],
"ContractTypes": null,
"Locations": [
"LOCATION_WET_RAT",
"LOCATION_COLOMBIA",
"LOCATION_COLOMBIA_ANACONDA",
"LOCATION_BANGKOK",
"LOCATION_BANGKOK_ZIKA"
],
"GameModes": null
}
}
]
}
]
}

View File

@ -0,0 +1,109 @@
{
"meta": {
"Location": "GLOBAL_CHEESECAKE_CHALLENGES",
"GameVersions": ["h3"]
},
"groups": [
{
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Image": "images/challenges/categories/packcheesecake/tile.jpg",
"Icon": "challenge_category_feats",
"CategoryId": "cheesecake-pack",
"Description": "",
"OrderIndex": 6.1,
"Challenges": [
{
"Id": "0e08ee97-8f70-c82e-f04a-9d2cd60ae5b5",
"Name": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_WRAPPER_NAME",
"ImageName": "images/challenges/Categories/PackCheesecake/tile.jpg",
"Description": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_WRAPPER_DESC",
"Rewards": {
"MasteryXP": 4000
},
"Drops": ["TOKEN_OUTFIT_HERO_BUTCHER_SUIT"],
"IsPlayable": false,
"IsLocked": false,
"HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Icon": "challenge_category_feats",
"LocationId": "",
"ParentLocationId": "",
"Type": "global",
"DifficultyLevels": [],
"OrderIndex": 100000,
"XpModifier": {},
"RuntimeType": "Hit",
"Definition": {
"Scope": "profile",
"Constants": {
"RequiredChallenges": [
"0570b6e5-3c27-e715-042a-59313b0a0916",
"15dfb94e-72cc-9f43-5281-a0f7e5180f24",
"eff06c02-2410-4226-1abc-076a2e71ee97",
"b5386255-0e7e-2bd7-6c28-c1096d4902c5",
"c098a7cf-8c49-02ce-475e-20beaed99712"
]
},
"Context": {
"CompletedChallenges": []
},
"ContextListeners": {
"CompletedChallenges": {
"comparand": "$.RequiredChallenges",
"type": "challengetree"
}
},
"States": {
"Start": {
"ChallengeCompleted": [
{
"Condition": {
"$any": {
"?": {
"$eq": [
"$.#",
"$Value.ChallengeId"
]
},
"in": "$.RequiredChallenges"
}
},
"$pushunique": [
"CompletedChallenges",
"$Value.ChallengeId"
]
},
{
"Condition": {
"$eq": [
"($.CompletedChallenges).Count",
"($.RequiredChallenges).Count"
]
},
"Transition": "Success"
}
]
}
}
},
"Tags": ["cheesecake-pack", "story", "hard"],
"InclusionData": {
"ContractIds": [
"179563a4-727a-4072-b354-c9fff4e8bff0",
"b2aac100-dfc7-4f85-b9cd-528114436f6c"
],
"ContractTypes": null,
"Locations": [
"LOCATION_COASTALTOWN",
"LOCATION_COASTALTOWN_NIGHT",
"LOCATION_COASTALTOWN_MOVIESET",
"LOCATION_WET",
"LOCATION_GOLDEN"
],
"GameModes": null
}
}
]
}
]
}

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "GLOBAL_CLASSIC_CHALLENGES",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "1c7d6618-26c2-4b9a-86f0-c3afa127fc8d",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "GLOBAL_ELUSIVES_CHALLENGES",
"GameVersion": "h3"
"GameVersions": ["h2", "h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "elusive",
"CategoryId": "elusive",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE",
"OrderIndex": 5,
"Challenges": [
{
"Id": "1cb6662f-b471-427d-af62-14906ea8f2ed",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "GLOBAL_ESCALATION_CHALLENGES",
"GameVersion": "h3"
"GameVersions": ["h3"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "featured",
"CategoryId": "escalation_hm1",
"Description": "",
"OrderIndex": 7,
"Challenges": [
{
"Id": "7c4e7906-ad19-46c0-96f4-4fc301a562b4",
@ -871,6 +872,7 @@
"Icon": "featured",
"CategoryId": "escalation_hm2",
"Description": "",
"OrderIndex": 8,
"Challenges": [
{
"Id": "c573a9aa-73cf-4efb-acc6-f391239cbeb5",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "GLOBAL_CLASSIC_CHALLENGES",
"GameVersion": "h2"
"GameVersions": ["h2"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "profile",
"CategoryId": "classic",
"Description": "",
"OrderIndex": 4,
"Challenges": [
{
"Id": "24946af7-cfd3-4fd3-8aba-aabad50bce14",

View File

@ -1,7 +1,7 @@
{
"meta": {
"Location": "GLOBAL_ESCALATION_CHALLENGES",
"GameVersion": "h2"
"GameVersions": ["h2"]
},
"groups": [
{
@ -10,6 +10,7 @@
"Icon": "challenge_category_feats",
"CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [
{
"Id": "239dd317-ea34-4689-b730-0726b51f71bd",

Some files were not shown because too many files have changed in this diff Show More