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
*Plugin.js *Plugin.js
packaging/livesplit-node-client/build 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: on:
push: push:
branches: ["v*"] branches: ["master"]
paths: ["resources/locale.json", ".github/workflows/locale.yml"] paths: ["resources/locale.json", ".github/workflows/locale.yml"]
workflow_dispatch: workflow_dispatch:
@ -18,13 +18,6 @@ jobs:
token: ${{ secrets.PEACOCKBOT_TOKEN }} token: ${{ secrets.PEACOCKBOT_TOKEN }}
path: "./Peacock" path: "./Peacock"
- name: Checkout Peacock Strings
uses: actions/checkout@v4
with:
token: ${{ secrets.PEACOCKBOT_TOKEN }}
repository: thepeacockproject/peacock-strings
path: "./PeacockStrings"
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@ -47,7 +40,7 @@ jobs:
- name: Download RPKG-CLI - name: Download RPKG-CLI
id: rpkgcli id: rpkgcli
uses: robinraju/release-downloader@v1.9 uses: robinraju/release-downloader@v1.10
with: with:
repository: "glacier-modding/RPKG-Tool" repository: "glacier-modding/RPKG-Tool"
latest: true latest: true
@ -56,7 +49,7 @@ jobs:
- name: Download HMLanguageTools - name: Download HMLanguageTools
id: hmlt id: hmlt
uses: robinraju/release-downloader@v1.9 uses: robinraju/release-downloader@v1.10
with: with:
repository: "AnthonyFuller/TonyTools" repository: "AnthonyFuller/TonyTools"
latest: true latest: true
@ -84,16 +77,3 @@ jobs:
author_name: PeacockBot author_name: PeacockBot
author_email: admin@thepeacockproject.org author_email: admin@thepeacockproject.org
message: "[skip ci] Update locale packages" 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"

2
.gitignore vendored
View File

@ -20,6 +20,7 @@ resources/**/*.*
!resources/locale.json !resources/locale.json
!resources/dynamic_resources_*/*.meta !resources/dynamic_resources_*/*.meta
!resources/dynamic_resources_*.rpkg !resources/dynamic_resources_*.rpkg
!resources/dynamic_resources_*/*.JSON
!resources/rebuildLocale.cjs !resources/rebuildLocale.cjs
components/contracts.json components/contracts.json
@ -59,3 +60,4 @@ DEBUG_PROFILE.zip
packaging/add_itemsize/* 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 /workspace.xml
# Editor-based HTTP Client requests # Editor-based HTTP Client requests
/httpRequests/ /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$/resources/challenges" />
<excludeFolder url="file://$MODULE_DIR$/patcher/.idea/.idea.HitmanPatcher/.idea/shelf" /> <excludeFolder url="file://$MODULE_DIR$/patcher/.idea/.idea.HitmanPatcher/.idea/shelf" />
<excludeFolder url="file://$MODULE_DIR$/images" /> <excludeFolder url="file://$MODULE_DIR$/images" />
<excludeFolder url="file://$MODULE_DIR$/logs" />
<excludePattern pattern="chunk*.js" /> <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> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View File

@ -1,12 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GithubSharedProjectSettings">
<option name="branchProtectionPatterns">
<list>
<option value="main" />
</list>
</option>
</component>
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
</component> </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 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "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", "type": "node",
"request": "launch", "request": "launch",

View File

@ -1,10 +1,9 @@
{ {
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": false "source.organizeImports": "never"
}, },
"typescript.enablePromptUseWorkspaceTsdk": true, "typescript.enablePromptUseWorkspaceTsdk": true,
"npm.packageManager": "yarn", "npm.packageManager": "yarn",
"eslint.packageManager": "yarn",
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.format.enable": true, "eslint.format.enable": true,
"search.exclude": { "search.exclude": {
@ -14,5 +13,8 @@
"yarn.lock": true, "yarn.lock": true,
"**/.yarn": 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 - path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
spec: "https://mskelton.dev/yarn-outdated/v2" 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

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

View File

@ -79,6 +79,14 @@ type GroupIndexedChallengeLists = {
[groupId: string]: RegistryChallenge[] [groupId: string]: RegistryChallenge[]
} }
export type ChallengePack = {
Name: string
Description: string
GameVersions: GameVersion[]
Image: string
Icon: string
}
/** /**
* A base class providing challenge registration support. * A base class providing challenge registration support.
*/ */
@ -99,7 +107,7 @@ export abstract class ChallengeRegistry {
/** /**
* @Key1 Game version. * @Key1 Game version.
* @Key2 The parent location Id. * @Key2 The parent location Id.
* @Key3 The group Id. * @Key3 The group's categoryId.
* @Value A `SavedChallengeGroup` object. * @Value A `SavedChallengeGroup` object.
*/ */
protected groups: Record< protected groups: Record<
@ -145,6 +153,39 @@ export abstract class ChallengeRegistry {
protected constructor(protected readonly controller: Controller) {} 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( registerChallenge(
challenge: RegistryChallenge, challenge: RegistryChallenge,
groupId: string, groupId: string,
@ -192,6 +233,23 @@ export abstract class ChallengeRegistry {
return this.challenges[gameVersion].get(challengeId) 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. * 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). * 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 return mainGroup
} }
public getChallengesForGroup(
groupId: string,
gameVersion: GameVersion,
): GroupIndexedChallengeLists {
return {
[groupId]: Array.from(this.challenges[gameVersion].values()).filter(
(value) => value.inGroup === groupId,
),
}
}
public getGroupContentByIdLoc( public getGroupContentByIdLoc(
groupId: string, groupId: string,
location: 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, * Filter all challenges in a parent location using a given filter, sort them into groups,
* and write them into the `challenges` array provided. * and write them into the `challenges` array provided.
@ -587,6 +656,13 @@ export class ChallengeService extends ChallengeRegistry {
): GroupIndexedChallengeLists { ): GroupIndexedChallengeLists {
let challenges: [string, RegistryChallenge[]][] = [] let challenges: [string, RegistryChallenge[]][] = []
// Challenge packs ignore the filter
if (this.challengePacks.has(location)) {
challenges.push([
location,
this.getChallengesForGroup(location, gameVersion)[location],
])
} else {
this.getGroupedChallengesByLoc( this.getGroupedChallengesByLoc(
filter, filter,
location, location,
@ -594,7 +670,10 @@ export class ChallengeService extends ChallengeRegistry {
gameVersion, gameVersion,
) )
if (filter.type === ChallengeFilterType.Contract && filter.isFeatured) { if (
filter.type === ChallengeFilterType.Contract &&
filter.isFeatured
) {
this.getGroupedChallengesByLoc( this.getGroupedChallengesByLoc(
filter, filter,
"GLOBAL_FEATURED_CHALLENGES", "GLOBAL_FEATURED_CHALLENGES",
@ -619,6 +698,7 @@ export class ChallengeService extends ChallengeRegistry {
gameVersion, gameVersion,
) )
} }
}
// remove empty groups // remove empty groups
challenges = challenges.filter( challenges = challenges.filter(
@ -989,6 +1069,7 @@ export class ChallengeService extends ChallengeRegistry {
forContract, forContract,
userId, userId,
gameVersion, gameVersion,
false,
subLocation, subLocation,
) )
} }
@ -999,7 +1080,8 @@ export class ChallengeService extends ChallengeRegistry {
gameVersion: GameVersion, gameVersion: GameVersion,
compiler: Compiler, compiler: Compiler,
): CompiledChallengeTreeData[] { ): CompiledChallengeTreeData[] {
return challenges.map((challengeData) => { return challenges
.map((challengeData) => {
return compiler( return compiler(
challengeData, challengeData,
this.getPersistentChallengeProgression( this.getPersistentChallengeProgression(
@ -1011,6 +1093,7 @@ export class ChallengeService extends ChallengeRegistry {
userId, userId,
) )
}) })
.sort((a, b) => a.OrderIndex - b.OrderIndex)
} }
private getChallengeDependencyData( private getChallengeDependencyData(
@ -1103,43 +1186,27 @@ export class ChallengeService extends ChallengeRegistry {
forLocation, forLocation,
userId, userId,
gameVersion, gameVersion,
locationData,
true, true,
locationData,
) )
} }
getChallengeDataForLocation( getChallengeDataForCategory(
locationId: string, categoryId: string | null,
location: Unlockable | undefined,
gameVersion: GameVersion, gameVersion: GameVersion,
userId: string, userId: string,
): CompiledChallengeTreeCategory[] { ): CompiledChallengeTreeCategory[] {
const locationsData = getVersionedConfig<PeacockLocationsData>( const challenges = location
"LocationsData", ? this.getChallengesForLocation(location.Id, gameVersion)
gameVersion, : this.getChallengesForGroup(categoryId!, 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,
)
return this.reBatchIntoSwitchedData( return this.reBatchIntoSwitchedData(
forLocation, challenges,
userId, userId,
gameVersion, gameVersion,
locationData,
true, true,
location,
) )
} }
@ -1148,33 +1215,35 @@ export class ChallengeService extends ChallengeRegistry {
* @param challengeLists The challenge lists to use. * @param challengeLists The challenge lists to use.
* @param userId The id of the user. * @param userId The id of the user.
* @param gameVersion The current game version. * @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 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. * @returns An array of `CompiledChallengeTreeCategory` objects.
*/ */
private reBatchIntoSwitchedData( private reBatchIntoSwitchedData(
challengeLists: GroupIndexedChallengeLists, challengeLists: GroupIndexedChallengeLists,
userId: string, userId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
location: Unlockable, isDestination: boolean,
isDestination = false, location?: Unlockable,
): CompiledChallengeTreeCategory[] { ): CompiledChallengeTreeCategory[] {
const entries = Object.entries(challengeLists) const entries = Object.entries(challengeLists)
const compiler = isDestination const compiler = isDestination
? this.compileRegistryDestinationChallengeData.bind(this) ? this.compileRegistryDestinationChallengeData.bind(this)
: this.compileRegistryChallengeTreeData.bind(this) : this.compileRegistryChallengeTreeData.bind(this)
const completion = generateCompletionData( const completion = location
location?.Id, ? generateCompletionData(location?.Id, userId, gameVersion)
userId, : {}
gameVersion,
)
return entries const groups = entries
.map(([groupId, challenges], index) => { .map(([groupId, challenges], index) => {
if (challenges.length === 0) {
return undefined
}
const groupData = this.getGroupByIdLoc( const groupData = this.getGroupByIdLoc(
groupId, groupId,
location.Properties.ParentLocation ?? location.Id, challenges[0].ParentLocationId,
gameVersion, gameVersion,
) )
@ -1191,16 +1260,20 @@ export class ChallengeService extends ChallengeRegistry {
), ),
) )
const lastGroup = this.getGroupByIdLoc( const lastGroup = location
? this.getGroupByIdLoc(
Object.keys(challengeLists)[index - 1], Object.keys(challengeLists)[index - 1],
location.Properties.ParentLocation ?? location.Id, location.Properties.ParentLocation ?? location.Id,
gameVersion, gameVersion,
) )
const nextGroup = this.getGroupByIdLoc( : undefined
const nextGroup = location
? this.getGroupByIdLoc(
Object.keys(challengeLists)[index + 1], Object.keys(challengeLists)[index + 1],
location.Properties.ParentLocation ?? location.Id, location.Properties.ParentLocation ?? location.Id,
gameVersion, gameVersion,
) )
: undefined
return { return {
Name: groupData.Name, Name: groupData.Name,
@ -1214,9 +1287,11 @@ export class ChallengeService extends ChallengeRegistry {
).length, ).length,
CompletionData: completion, CompletionData: completion,
Location: location, Location: location,
IsLocked: location.Properties.IsLocked || false, IsLocked: location?.Properties.IsLocked || false,
ImageLocked: location.Properties.LockedIcon || "", ImageLocked: location?.Properties.LockedIcon || "",
RequiredResources: location.Properties.RequiredResources!, RequiredResources:
location?.Properties.RequiredResources || [],
OrderIndex: groupData.OrderIndex ?? 10000,
SwitchData: { SwitchData: {
Data: { Data: {
Challenges: this.mapSwitchChallenges( Challenges: this.mapSwitchChallenges(
@ -1253,6 +1328,10 @@ export class ChallengeService extends ChallengeRegistry {
} }
}) })
.filter(Boolean) as CompiledChallengeTreeCategory[] .filter(Boolean) as CompiledChallengeTreeCategory[]
return groups.sort((a, b) => {
return a.OrderIndex - b.OrderIndex
})
} }
compileRegistryChallengeTreeData( compileRegistryChallengeTreeData(
@ -1297,6 +1376,7 @@ export class ChallengeService extends ChallengeRegistry {
userId, userId,
gameVersion, gameVersion,
), ),
OrderIndex: challenge.OrderIndex ?? 10000,
DifficultyLevels: challenge.DifficultyLevels ?? [], DifficultyLevels: challenge.DifficultyLevels ?? [],
// Only include CompletionData if ParentLocationId is not an empty string // Only include CompletionData if ParentLocationId is not an empty string
...(challenge.ParentLocationId !== "" && { ...(challenge.ParentLocationId !== "" && {

View File

@ -21,7 +21,7 @@ import {
getSubLocationByName, getSubLocationByName,
} from "../contracts/dataGen" } from "../contracts/dataGen"
import { log, LogLevel } from "../loggingInterop" import { log, LogLevel } from "../loggingInterop"
import { getVersionedConfig } from "../configSwizzleManager" import { getConfig } from "../configSwizzleManager"
import { getUserData } from "../databaseHandler" import { getUserData } from "../databaseHandler"
import { import {
LocationMasteryData, LocationMasteryData,
@ -265,12 +265,9 @@ export class MasteryService {
// TODO: Refactor this into the new inventory system? // TODO: Refactor this into the new inventory system?
const name = isSniper const name = isSniper
? getVersionedConfig<Unlockable[]>( ? getConfig<Unlockable[]>("SniperUnlockables", false).find(
"SniperUnlockables", (unlockable) => unlockable.Id === subPackageId,
gameVersion, )?.Properties.Name
false,
).find((unlockable) => unlockable.Id === subPackageId)?.Properties
.Name
: undefined : undefined
return { return {

View File

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

View File

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

View File

@ -833,85 +833,3 @@ export function complications(timeString: string) {
targetsOnly, 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 = [ export const orderedETAs = [
"797e204a-ef3d-463b-a386-57df0fe29b8f", "797e204a-ef3d-463b-a386-57df0fe29b8f",
"de9788cc-b9c4-47fc-b5df-86451cd82c43",
"ff5b4e53-49ea-4d85-b94e-d3c8b3fc7ab3", "ff5b4e53-49ea-4d85-b94e-d3c8b3fc7ab3",
"80cf04de-8e0b-4f38-b094-600753e2ac24", "80cf04de-8e0b-4f38-b094-600753e2ac24",
"85a67f31-75ce-40f5-a281-7765791f58ca", "85a67f31-75ce-40f5-a281-7765791f58ca",

View File

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

View File

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

View File

@ -18,19 +18,6 @@
import { ContractSession } from "../types/types" 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)[] = [ const SESSION_SET_PROPS: (keyof ContractSession)[] = [
"targetKills", "targetKills",
"npcKills", "npcKills",
@ -76,7 +63,7 @@ export function serializeSession(session: ContractSession): unknown {
if (session[key as K] instanceof Set) { if (session[key as K] instanceof Set) {
// @ts-expect-error Type mismatch. // @ts-expect-error Type mismatch.
o[key] = normalizeSet(session[key]) o[key] = Array.from(session[key])
continue continue
} }
@ -109,7 +96,7 @@ export function deserializeSession(
} }
for (const map of SESSION_MAP_PROPS) { for (const map of SESSION_MAP_PROPS) {
if (Object.hasOwn(session, map)) { if (session[map]) {
// @ts-expect-error Type mismatch. // @ts-expect-error Type mismatch.
session[map] = new Map(session[map]) session[map] = new Map(session[map])
} }

View File

@ -31,7 +31,7 @@ import type {
GameVersion, GameVersion,
GenSingleMissionFunc, GenSingleMissionFunc,
GenSingleVideoFunc, GenSingleVideoFunc,
IHit, Hit,
MissionManifest, MissionManifest,
PeacockLocationsData, PeacockLocationsData,
PlayNextGetCampaignsHookReturn, PlayNextGetCampaignsHookReturn,
@ -359,6 +359,7 @@ export class Controller {
PlayNextGetCampaignsHookReturn | undefined PlayNextGetCampaignsHookReturn | undefined
> >
onMissionEnd: SyncHook<[/** session */ ContractSession]> onMissionEnd: SyncHook<[/** session */ ContractSession]>
onEscalationReset: SyncHook<[/** groupId */ string]>
} }
public configManager: typeof configManagerType = { public configManager: typeof configManagerType = {
getConfig, getConfig,
@ -402,9 +403,49 @@ export class Controller {
getSearchResults: new AsyncSeriesHook(), getSearchResults: new AsyncSeriesHook(),
getNextCampaignMission: new SyncBailHook(), getNextCampaignMission: new SyncBailHook(),
onMissionEnd: new SyncHook(), 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. * Starts the service and loads in all contracts.
* *
@ -434,6 +475,16 @@ export class Controller {
this._getETALocations() this._getETALocations()
this.index() 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 const deployPath = SMFSupport.modFrameworkDataPath
if (typeof deployPath === "string") { if (typeof deployPath === "string") {
@ -447,16 +498,6 @@ export class Controller {
} }
this.hooks.serverStart.call() 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 { private _getETALocations(): void {
@ -730,9 +771,7 @@ export class Controller {
) )
await this.commitNewContract(contractData) 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 return contractData
} }
@ -825,7 +864,6 @@ export class Controller {
/** /**
* Get all global challenges and register a simplified version of them. * Get all global challenges and register a simplified version of them.
* @param gameVersion A GameVersion object representing the version of the game. * @param gameVersion A GameVersion object representing the version of the game.
*
*/ */
private registerGlobalChallenges(gameVersion: GameVersion) { private registerGlobalChallenges(gameVersion: GameVersion) {
const regGlobalChallenges: RegistryChallenge[] = getVersionedConfig< const regGlobalChallenges: RegistryChallenge[] = getVersionedConfig<
@ -856,7 +894,7 @@ export class Controller {
], ],
meta: { meta: {
Location: "GLOBAL", Location: "GLOBAL",
GameVersion: gameVersion, GameVersions: [gameVersion],
}, },
}) })
} }
@ -914,11 +952,12 @@ export class Controller {
} }
private _handleChallengeResources(data: ChallengePackage): void { private _handleChallengeResources(data: ChallengePackage): void {
for (const version of data.meta.GameVersions) {
for (const group of data.groups) { for (const group of data.groups) {
this.challengeService.registerGroup( this.challengeService.registerGroup(
group, group,
data.meta.Location, data.meta.Location,
data.meta.GameVersion, version,
) )
for (const challenge of group.Challenges) { for (const challenge of group.Challenges) {
@ -926,11 +965,12 @@ export class Controller {
challenge, challenge,
group.CategoryId, group.CategoryId,
data.meta.Location, data.meta.Location,
data.meta.GameVersion, version,
) )
} }
} }
} }
}
private _handleMasteryResources(data: MasteryPackage): void { private _handleMasteryResources(data: MasteryPackage): void {
this.masteryService.registerMasteryData(data) this.masteryService.registerMasteryData(data)
@ -1035,6 +1075,7 @@ export class Controller {
module: { exports: {} }, module: { exports: {} },
exports: {}, exports: {},
process, process,
fetch,
require: createPeacockRequire(pluginName), require: createPeacockRequire(pluginName),
}) })
@ -1175,7 +1216,7 @@ export function contractIdToHitObject(
contractId: string, contractId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
userId: string, userId: string,
): IHit | undefined { ): Hit | undefined {
const contract = controller.resolveContract(contractId) const contract = controller.resolveContract(contractId)
if (!contract) { if (!contract) {

View File

@ -16,56 +16,115 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { readFile, writeFile } from "atomically"
import { join } from "path" import { join } from "path"
import type { ContractSession, GameVersion, UserProfile } from "./types/types" 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 { castUserProfile } from "./utils"
import { log, LogLevel } from "./loggingInterop" 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, * The fs implementation that this system uses.
* which could otherwise break if writing partial data. */
export type DataStorageFs = NodeUnlinkMkdirReaddir & ExistsPromise
/**
* Handles the dispatching of user data in a way that avoids FS operations unless absolutely needed.
*/ */
class AsyncUserDataGuard { class AsyncUserDataGuard {
private readonly userData: Record<string, UserProfile> = {} /** @internal */
private readonly dirtyProfiles: Set<string> = new Set() 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 { /** If true, none of the background tasks will attempt to write. */
return this.userData[id] #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 { addLoadedProfile(id: string, profile: UserProfile): void {
if (!this.userData[id]) { if (!this.userData.has(id) && !this.tasks.has(id)) {
setInterval(() => { const interval = setInterval(() => {
if (!this.dirtyProfiles.has(id)) { if (!this.dirtyProfiles.has(id) || this.#paused) {
return return
} }
this.save(id)
}, 3000)
this.tasks.set(id, interval.unref())
}
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.dirtyProfiles.delete(id)
this.write(id) this.write(id)
.then(() => undefined) .then(() => undefined)
.catch((e) => { .catch((e) => {
log( log(LogLevel.ERROR, `Failed to write user profile ${id}: ${e}`)
LogLevel.ERROR,
`Failed to write user profile ${id}: ${e}`,
)
}) })
}, 3000).unref()
}
this.userData[id] = profile
// just in case
this.dirtyProfiles.delete(id)
} }
markDirty(id: string): void { markDirty(id: string): void {
this.dirtyProfiles.add(id) this.dirtyProfiles.add(id)
} }
private async write(versionedId: string): Promise<void> { /** @internal */
async write(versionedId: string): Promise<void> {
let path let path
const [id, gameVersion] = versionedId.split(".") const [id, gameVersion] = versionedId.split(".")
@ -76,24 +135,58 @@ class AsyncUserDataGuard {
path = join("userdata", "users", `${id}.json`) 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. * Gets a user's profile data.
* *
* @param userId The user's ID. * @param userId The user's ID.
* @param gameVersion The game's version. * @param gameVersion The game's version.
* @returns The user's profile * @returns The user's profile, OR UNDEFINED if not loaded.
*/ */
export function getUserData( export function getUserData(
userId: string, userId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): UserProfile { ): 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 // NOTE: ProfileLevel always starts at 1
if (data?.Extensions?.progression?.PlayerProfileXP?.ProfileLevel === 0) { 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 userId The user's ID.
* @param gameVersion The game's version. * @param gameVersion The game's version.
@ -150,7 +243,7 @@ export async function loadUserData(
} }
const userProfile = castUserProfile( const userProfile = castUserProfile(
JSON.parse((await readFile(path)).toString()), JSON.parse((await asyncGuard.getFs().readFile(path)).toString()),
gameVersion, gameVersion,
path, path,
) )
@ -199,16 +292,18 @@ export async function getExternalUserData(
externalFolder: string, externalFolder: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): Promise<string> { ): Promise<string> {
const fs = asyncGuard.getFs()
if (["scpc", "h1", "h2"].includes(gameVersion)) { if (["scpc", "h1", "h2"].includes(gameVersion)) {
return ( return (
await readFile( await fs.readFile(
join("userdata", gameVersion, externalFolder, `${userId}.json`), join("userdata", gameVersion, externalFolder, `${userId}.json`),
) )
).toString() ).toString()
} }
return ( return (
await readFile(join("userdata", externalFolder, `${userId}.json`)) await fs.readFile(join("userdata", externalFolder, `${userId}.json`))
).toString() ).toString()
} }
@ -226,14 +321,16 @@ export async function writeExternalUserData(
userData: string, userData: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): Promise<void> { ): Promise<void> {
const fs = asyncGuard.getFs()
if (["scpc", "h1", "h2"].includes(gameVersion)) { if (["scpc", "h1", "h2"].includes(gameVersion)) {
return await writeFile( return await fs.writeFile(
join("userdata", gameVersion, externalFolder, `${userId}.json`), join("userdata", gameVersion, externalFolder, `${userId}.json`),
userData, userData,
) )
} }
return await writeFile( return await fs.writeFile(
join("userdata", externalFolder, `${userId}.json`), join("userdata", externalFolder, `${userId}.json`),
userData, userData,
) )
@ -248,7 +345,8 @@ export async function writeExternalUserData(
export async function getContractSession( export async function getContractSession(
identifier: string, identifier: string,
): Promise<ContractSession> { ): 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`)) const filtered = files.filter((fn) => fn.endsWith(`_${identifier}.json`))
if (filtered.length === 0) { 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 // 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( return deserializeSession(
JSON.parse( 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, identifier: string,
session: ContractSession, session: ContractSession,
): Promise<void> { ): Promise<void> {
return await writeFile( const fs = asyncGuard.getFs()
return await fs.writeFile(
join("contractSessions", `${identifier}.json`), join("contractSessions", `${identifier}.json`),
JSON.stringify(serializeSession(session)), JSON.stringify(serializeSession(session)),
) )
@ -287,5 +389,46 @@ export async function writeContractSession(
* @throws ENOENT if the file is not found. * @throws ENOENT if the file is not found.
*/ */
export async function deleteContractSession(fileName: string): Promise<void> { 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { RPCClient } from "./discord/client" import { RPCClient } from "./client"
import { getConfig } from "./configSwizzleManager" import { getConfig } from "../configSwizzleManager"
import type { GameVersion, MissionType } from "./types/types" import type { GameVersion, MissionType } from "../types/types"
import { getFlag } from "./flags" import { getFlag } from "../flags"
import { log, LogLevel } from "./loggingInterop" import { log, LogLevel } from "../loggingInterop"
let rpcClient: undefined | RPCClient let rpcClient: undefined | RPCClient
/*@__NOINLINE__*/ /*@__NOINLINE__*/
const processStartTime = Math.round(Date.now() / 1000) const processStartTime = Math.round(Date.now() / 1000)
export function initRp(): void { 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) { 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. // creates a new rp client, pretty self-explanatory.
rpcClient = new RPCClient() rpcClient = new RPCClient()
// connects to the Peacock discord developer app, which contains the images for the rp. // connects to the Peacock discord developer app, which contains the images for the rp.
rpcClient.login({ void rpcClient.login({
clientId: "846361353027584013", clientId: "846361353027584013",
}) })
@ -143,7 +143,7 @@ type BrickDataMap = Record<string, [string, string, string]>
export function scenePathToRpAsset( export function scenePathToRpAsset(
scenePath: string, scenePath: string,
bricks: string[], bricks: string[],
): string[] | undefined { ): [string, string, string] | undefined {
const brickAssetsMap = getConfig<BrickDataMap>( const brickAssetsMap = getConfig<BrickDataMap>(
"DiscordRichAssetsForBricks", "DiscordRichAssetsForBricks",
false, false,
@ -220,11 +220,16 @@ export function scenePathToRpAsset(
// dartmoor // dartmoor
case "assembly:/_pro/scenes/missions/ancestral/scene_bulldog.entity": 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"] return ["dartmoordeathoffamily", "Death in the Family", "Dartmoor"]
case "assembly:/_pro/scenes/missions/ancestral/scene_smoothsnake.entity": case "assembly:/_pro/scenes/missions/ancestral/scene_smoothsnake.entity":
return ["dartmoorgardenshow", "Dartmoor Garden Show", "Dartmoor"] return ["dartmoorgardenshow", "Dartmoor Garden Show", "Dartmoor"]
case "assembly:/_pro/scenes/missions/ancestral/scene_ancestral_vesper.entity": case "assembly:/_pro/scenes/missions/ancestral/scene_ancestral_vesper.entity":
return ["elusivevesper", "The Procurers", "Dartmoor"] 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 // columbia
case "assembly:/_pro/scenes/missions/colombia/mission_millipede/scene_millipede.entity": case "assembly:/_pro/scenes/missions/colombia/mission_millipede/scene_millipede.entity":
@ -258,6 +263,8 @@ export function scenePathToRpAsset(
return ["berlinegghunt", "Berlin Egg Hunt", "Berlin"] return ["berlinegghunt", "Berlin Egg Hunt", "Berlin"]
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_tomorrowland.entity": case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_tomorrowland.entity":
return ["elusivetomorrowland", "The Drop", "Berlin"] return ["elusivetomorrowland", "The Drop", "Berlin"]
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_ambrosia.entity":
return ["ambrosia", "The Lust Assignation", "Berlin"]
// mendozer // mendozer
case "assembly:/_pro/scenes/missions/elegant/scene_llama_elusive_clerico.entity": 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_level2.entity":
case "assembly:/_pro/scenes/missions/elegant/scene_whitedryas_level3.entity": case "assembly:/_pro/scenes/missions/elegant/scene_whitedryas_level3.entity":
return ["mendozafarewell", "The Farewell", "Mendoza"] return ["mendozafarewell", "The Farewell", "Mendoza"]
case "assembly:/_pro/scenes/missions/elegant/scene_frangipani.entity":
return ["frangipani", "The Envy Contention", "Mendoza"]
// dubai // dubai
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gecko_angelica.entity": 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"] return ["chongqingendofanera", "End Of An Era", "Chongqing"]
case "assembly:/_pro/scenes/missions/wet/scene_rat_elusive_redsnapper.entity": case "assembly:/_pro/scenes/missions/wet/scene_rat_elusive_redsnapper.entity":
return ["elusiveredsnapper", "The Rage", "Chongqing"] return ["elusiveredsnapper", "The Rage", "Chongqing"]
case "assembly:/_pro/scenes/missions/wet/scene_wet_azalea.entity":
return ["azalea", "The Gluttony Gobble", "Chongqing"]
// training // training
case "assembly:/_pro/scenes/missions/thefacility/_scene_polarbear_005.entity": case "assembly:/_pro/scenes/missions/thefacility/_scene_polarbear_005.entity":
@ -366,6 +377,10 @@ export function scenePathToRpAsset(
return ["austria", "The Last Yardbird", "Austria"] return ["austria", "The Last Yardbird", "Austria"]
case "assembly:/_pro/scenes/missions/salty/mission_seagull/scene_seagull.entity": case "assembly:/_pro/scenes/missions/salty/mission_seagull/scene_seagull.entity":
return ["hantuport", "The Pen and the Sword", "Hantu Port"] 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 return undefined

View File

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

View File

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

View File

@ -22,6 +22,10 @@ import { ContractProgressionData } from "./types/types"
import { getFlag } from "./flags" import { getFlag } from "./flags"
import { EVERGREEN_LEVEL_INFO } from "./utils" import { EVERGREEN_LEVEL_INFO } from "./utils"
type DefaultCpdConfigs = {
[cpdId: string]: ContractProgressionData
}
export function setCpd( export function setCpd(
data: ContractProgressionData, data: ContractProgressionData,
uID: string, uID: string,
@ -42,15 +46,22 @@ export function getCpd(uID: string, cpdID: string): ContractProgressionData {
if (!Object.keys(userData.Extensions.CPD).includes(cpdID)) { if (!Object.keys(userData.Extensions.CPD).includes(cpdID)) {
const defaultCPD = getConfig( const defaultCPD = getConfig(
"DefaultCpdConfig", "DefaultCpdConfigs",
false, 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 // 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"] = userData.Extensions.CPD[cpdID]["EvergreenLevel"] =
EVERGREEN_LEVEL_INFO.length 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!)", 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, 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: { jokes: {
desc: "[Services] The Peacock server window will tell you a joke on startup if this is set to true.", desc: "[Services] The Peacock server window will tell you a joke on startup if this is set to true.",
default: false, default: false,

View File

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

View File

@ -16,15 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 express, { Request, Router } from "express"
import http from "http" import http from "http"
import { import {
@ -33,7 +24,6 @@ import {
handleAxiosError, handleAxiosError,
IS_LAUNCHER, IS_LAUNCHER,
jokes, jokes,
PEACOCKVER,
PEACOCKVERSTRING, PEACOCKVERSTRING,
ServerVer, ServerVer,
} from "./utils" } from "./utils"
@ -49,8 +39,7 @@ import type {
S2CEventWithTimestamp, S2CEventWithTimestamp,
ServerConnectionConfig, ServerConnectionConfig,
} from "./types/types" } from "./types/types"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" import { readFileSync } from "fs"
import { join } from "path"
import { import {
errorLoggingMiddleware, errorLoggingMiddleware,
log, log,
@ -73,7 +62,7 @@ import {
import { legacyProfileRouter } from "./2016/legacyProfileRouter" import { legacyProfileRouter } from "./2016/legacyProfileRouter"
import { legacyMenuDataRouter } from "./2016/legacyMenuData" import { legacyMenuDataRouter } from "./2016/legacyMenuData"
import { legacyContractRouter } from "./2016/legacyContractHandler" import { legacyContractRouter } from "./2016/legacyContractHandler"
import { initRp } from "./discordRp" import { initRp } from "./discord/discordRp"
import random from "random" import random from "random"
import { generateUserCentric } from "./contracts/dataGen" import { generateUserCentric } from "./contracts/dataGen"
import { json as jsonMiddleware, urlencoded } from "body-parser" import { json as jsonMiddleware, urlencoded } from "body-parser"
@ -82,15 +71,12 @@ import { setupHotListener } from "./hotReloadService"
import type { AxiosError } from "axios" import type { AxiosError } from "axios"
import serveStatic from "serve-static" import serveStatic from "serve-static"
import { webFeaturesRouter } from "./webFeatures" import { webFeaturesRouter } from "./webFeatures"
import { toolsMenu } from "./tools"
import picocolors from "picocolors" import picocolors from "picocolors"
import { multiplayerRouter } from "./multiplayer/multiplayerService" import { multiplayerRouter } from "./multiplayer/multiplayerService"
import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData" import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData"
import { pack, unpack } from "msgpackr"
import { liveSplitManager } from "./livesplit/liveSplitManager" import { liveSplitManager } from "./livesplit/liveSplitManager"
import { cheapLoadUserData } from "./databaseHandler" import { cheapLoadUserData, setupFileStructure } from "./databaseHandler"
import { getFlag } from "./flags"
loadFlags()
const host = process.env.HOST || "0.0.0.0" const host = process.env.HOST || "0.0.0.0"
const port = process.env.PORT || 80 const port = process.env.PORT || 80
@ -115,6 +101,10 @@ function uncaught(error: Error): void {
LogLevel.ERROR, LogLevel.ERROR,
` - Your user account doesn't have permission (firewall can block it)`, ` - 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) process.exit(1)
} }
@ -317,7 +307,7 @@ app.use(
break break
case "fghi4567xQOCheZIin0pazB47qGUvZw4": case "fghi4567xQOCheZIin0pazB47qGUvZw4":
case STEAM_NAMESPACE_2021: case STEAM_NAMESPACE_2021:
req.serverVersion = "8-14" req.serverVersion = "8-15"
break break
default: default:
res.status(400).json({ message: "no game data" }) res.status(400).json({ message: "no game data" })
@ -508,7 +498,7 @@ app.use(
} }
if ( if (
["6-74", "7-3", "7-17", "8-14"].includes( ["6-74", "7-3", "7-17", "8-15"].includes(
<string>req.serverVersion, <string>req.serverVersion,
) )
) { ) {
@ -527,11 +517,10 @@ app.all("*", (req, res) => {
app.use(errorLoggingMiddleware) app.use(errorLoggingMiddleware)
program.description( export async function startServer(options: {
"The Peacock Project is a HITMAN™ World of Assassination Trilogy server replacement.", hmr: boolean
) pluginDevHost: boolean
}): Promise<void> {
function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
void checkForUpdates() void checkForUpdates()
if (!IS_LAUNCHER) { if (!IS_LAUNCHER) {
@ -551,10 +540,9 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
log( log(
LogLevel.INFO, 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) { if (getFlag("jokes") === true) {
log( log(
LogLevel.INFO, LogLevel.INFO,
@ -564,49 +552,23 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
) )
} }
try {
// make sure required folder structure is in place // make sure required folder structure is in place
for (const dir of [ await setupFileStructure()
"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
}
log(LogLevel.DEBUG, `Creating missing directory ${dir}`)
mkdirSync(dir, { recursive: true })
}
if (options.hmr) { if (options.hmr) {
log(LogLevel.DEBUG, "Experimental HMR enabled.")
void setupHotListener("contracts", () => { void setupHotListener("contracts", () => {
log(LogLevel.INFO, "Detected a change in contracts! Re-indexing...") log(
LogLevel.INFO,
"Detected a change in contracts! Re-indexing...",
)
controller.index() controller.index()
}) })
} }
// once contracts directory is present, we are clear to boot // once contracts directory is present, we are clear to boot
loadouts.init() await loadouts.init()
void controller.boot(options.pluginDevHost) await controller.boot(options.pluginDevHost)
const httpServer = http.createServer(app) const httpServer = http.createServer(app)
@ -619,62 +581,11 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
} }
// initialize livesplit // initialize livesplit
void liveSplitManager.init() await liveSplitManager.init()
return
} catch (e) {
log(LogLevel.ERROR, "Critical error during bootstrap!")
log(LogLevel.ERROR, e)
}
} }
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, H1_REQUIEM_UNLOCKABLES,
H2_RACCOON_STINGRAY_UNLOCKABLES, H2_RACCOON_STINGRAY_UNLOCKABLES,
MAKESHIFT_UNLOCKABLES, MAKESHIFT_UNLOCKABLES,
SAMBUCA_UNLOCKABLES,
SIN_ENVY_UNLOCKABLES, SIN_ENVY_UNLOCKABLES,
SIN_GLUTTONY_UNLOCKABLES, SIN_GLUTTONY_UNLOCKABLES,
SIN_GREED_UNLOCKABLES, SIN_GREED_UNLOCKABLES,
@ -34,6 +35,7 @@ import {
SIN_PRIDE_UNLOCKABLES, SIN_PRIDE_UNLOCKABLES,
SIN_SLOTH_UNLOCKABLES, SIN_SLOTH_UNLOCKABLES,
SIN_WRATH_UNLOCKABLES, SIN_WRATH_UNLOCKABLES,
SMART_CASUAL_UNLOCKABLES,
TRINITY_UNLOCKABLES, TRINITY_UNLOCKABLES,
WINTERSPORTS_UNLOCKABLES, WINTERSPORTS_UNLOCKABLES,
} from "./ownership" } from "./ownership"
@ -63,6 +65,7 @@ const DELUXE_DATA = [
...SIN_WRATH_UNLOCKABLES, ...SIN_WRATH_UNLOCKABLES,
...TRINITY_UNLOCKABLES, ...TRINITY_UNLOCKABLES,
...WINTERSPORTS_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)) { if (H1_REQUIEM_UNLOCKABLES.includes(id)) {
return ( return (
e.includes("e698e1a4b63947b0bc9349a5ae2dc015") || 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)) { if (SIN_GREED_UNLOCKABLES.includes(id)) {
return ( return (
e.includes("0e8632b4cdfb415e94291d97d727b98d") || 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)) { if (MAKESHIFT_UNLOCKABLES.includes(id)) {
return ( return (
e.includes("08d2bc4d20754191b6c488541d2b4fa1") || 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 return true
} }
} }

View File

@ -19,10 +19,10 @@
import { LiveSplitClient, LiveSplitResult } from "./liveSplitClient" import { LiveSplitClient, LiveSplitResult } from "./liveSplitClient"
import { log, LogLevel } from "../loggingInterop" import { log, LogLevel } from "../loggingInterop"
import { getAllCampaigns } from "../menus/campaigns" 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 { getFlag } from "../flags"
import { controller } from "../controller" import { controller } from "../controller"
import { scenePathToRpAsset } from "../discordRp" import { scenePathToRpAsset } from "../discord/discordRp"
import { LiveSplitTimeCalcEntry } from "../types/livesplit" import { LiveSplitTimeCalcEntry } from "../types/livesplit"
import assert from "assert" import assert from "assert"
@ -593,7 +593,7 @@ function getCampaignMissions(
const campaignMissionIds = (campaignStoryData: StoryData[]): string[] => { const campaignMissionIds = (campaignStoryData: StoryData[]): string[] => {
return campaignStoryData return campaignStoryData
.filter((data) => data.Type === "Mission") .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 // 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { existsSync, readFileSync, writeFileSync } from "fs"
import type { import type {
GameVersion, GameVersion,
Loadout, Loadout,
@ -25,9 +24,10 @@ import type {
} from "./types/types" } from "./types/types"
import { Request, Router } from "express" import { Request, Router } from "express"
import { json as jsonMiddleware } from "body-parser" import { json as jsonMiddleware } from "body-parser"
import { writeFile } from "atomically" import { writeFile } from "fs/promises"
import { nanoid } from "nanoid" import { nanoid } from "nanoid"
import { versions } from "./utils" import { versions } from "./utils"
import { asyncGuard } from "./databaseHandler"
const LOADOUT_PROFILES_FILE = "userdata/users/lop.json" const LOADOUT_PROFILES_FILE = "userdata/users/lop.json"
@ -50,47 +50,33 @@ const defaultValue: LoadoutFile = {
* A class for managing loadouts. * A class for managing loadouts.
*/ */
export class Loadouts { export class Loadouts {
private _loadouts!: LoadoutFile 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
}
/** /**
* Initializes the loadouts manager. * Initializes the loadouts manager.
*/ */
public init(): void { async init(): Promise<void> {
if (!existsSync(LOADOUT_PROFILES_FILE)) { const fs = asyncGuard.getFs()
this._loadouts = defaultValue
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 return
} }
this._loadouts = JSON.parse( this.loadouts = JSON.parse(
readFileSync(LOADOUT_PROFILES_FILE).toString(), (await fs.readFile(LOADOUT_PROFILES_FILE)).toString(),
) )
let dirty = false let dirty = false
// make sure they all have IDs // make sure they all have IDs
for (const gameVersion of versions) { for (const gameVersion of versions) {
for (const loadout of this._loadouts[gameVersion].loadouts) { for (const loadout of this.loadouts[gameVersion].loadouts) {
if (!loadout.id) { if (!loadout.id) {
dirty = true dirty = true
loadout.id = nanoid() 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 the selected value is null/undefined or is not length 0 or 21, it's not a valid id
if ( if (
!this._loadouts[gameVersion].selected || !this.loadouts[gameVersion].selected ||
// first condition ensures selected is truthy, but TS doesn't know // first condition ensures selected is truthy, but TS doesn't know
![0, 21].includes( ![0, 21].includes(
this._loadouts[gameVersion].selected?.length || -1, this.loadouts[gameVersion].selected?.length || -1,
) )
) { ) {
dirty = true dirty = true
// long story short: find a loadout with a name matching the selected value, // long story short: find a loadout with a name matching the selected value,
// and if found, set selected to the id // and if found, set selected to the id
this._loadouts[gameVersion].selected = this.loadouts[gameVersion].selected =
this._loadouts[gameVersion].loadouts.find( this.loadouts[gameVersion].loadouts.find(
(lo) => (lo) => lo.name === this.loadouts[gameVersion].selected,
lo.name === this._loadouts[gameVersion].selected,
)?.id || "" )?.id || ""
} }
} }
if (dirty) { 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". * @param name The optional name for the new loadout set, defaults to "Unnamed loadout set".
* @returns The Loadout object. * @returns The Loadout object.
*/ */
public createDefault( createDefault(
gameVersion: GameVersion, gameVersion: GameVersion,
name = "Unnamed loadout set", name = "Unnamed loadout set",
): Loadout { ): Loadout {
@ -143,8 +131,8 @@ export class Loadouts {
data: {}, data: {},
} }
this._loadouts[gameVersion].loadouts.push(l) this.loadouts[gameVersion].loadouts.push(l)
this._loadouts[gameVersion].selected = l.id this.loadouts[gameVersion].selected = l.id
return l return l
} }
@ -155,12 +143,12 @@ export class Loadouts {
* @param gameVersion The game version. * @param gameVersion The game version.
* @returns The loadout profile or undefined if one isn't selected or none exist. * @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") { if (gameVersion === "scpc") {
gameVersion = "h1" 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) 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. * Saves the loadout data to the Peacock userdata/users folder.
*/ */
public async save(): Promise<void> { 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. * 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() export const loadouts = new Loadouts()

View File

@ -95,28 +95,46 @@ import { getPlayerProfileData } from "./menus/playerProfile"
const menuDataRouter = Router() 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/ // /profiles/page/
menuDataRouter.get( menuDataRouter.get(
"/ChallengeLocation", "/ChallengeLocation",
// @ts-expect-error Jwt props. // @ts-expect-error Jwt props.
(req: RequestWithJwt<ChallengeLocationQuery>, res) => { (req: RequestWithJwt<ChallengeLocationQuery>, res) => {
const pack = controller.challengeService.challengePacks.get(
req.query.locationId,
)
const location = getVersionedConfig<PeacockLocationsData>( const location = getVersionedConfig<PeacockLocationsData>(
"LocationsData", "LocationsData",
req.gameVersion, req.gameVersion,
true, true,
).children[req.query.locationId] ).children[req.query.locationId]
if (!location) { if (!pack && !location) {
res.status(400).send("Invalid locationId") res.status(400).send("Invalid locationId")
return return
} }
const data = { const data = {
Name: location.DisplayNameLocKey, Name: pack ? pack.Name : location.DisplayNameLocKey,
Location: location, Location: location,
Children: controller.challengeService.getChallengeDataForLocation( Children: controller.challengeService.getChallengeDataForCategory(
req.query.locationId, pack ? req.query.locationId : null,
pack ? undefined : location,
req.gameVersion, req.gameVersion,
req.jwt.unique_name, req.jwt.unique_name,
), ),
@ -1395,25 +1413,12 @@ menuDataRouter.get(
"/GetMasteryCompletionDataForUnlockable", "/GetMasteryCompletionDataForUnlockable",
// @ts-expect-error Has jwt props. // @ts-expect-error Has jwt props.
(req: RequestWithJwt<GetMasteryCompletionDataForUnlockableQuery>, res) => { (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({ res.json({
template: null, template: null,
data: { data: {
CompletionData: controller.masteryService.getLocationCompletion( CompletionData: controller.masteryService.getLocationCompletion(
unlockToLoc[req.query.unlockableId], SNIPER_UNLOCK_TO_LOCATION[req.query.unlockableId],
unlockToLoc[req.query.unlockableId], SNIPER_UNLOCK_TO_LOCATION[req.query.unlockableId],
req.gameVersion, req.gameVersion,
req.jwt.unique_name, req.jwt.unique_name,
"sniper", "sniper",

View File

@ -23,7 +23,7 @@ import type {
GenSingleMissionFunc, GenSingleMissionFunc,
CampaignMission, CampaignMission,
CampaignVideo, CampaignVideo,
IVideo, Video,
StoryData, StoryData,
} from "../types/types" } from "../types/types"
import { log, LogLevel } from "../loggingInterop" import { log, LogLevel } from "../loggingInterop"
@ -65,7 +65,7 @@ function genSingleVideo(
videoId: string, videoId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): CampaignVideo { ): 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] const video = videos[videoId]
switch (gameVersion) { switch (gameVersion) {

View File

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

View File

@ -17,12 +17,13 @@
*/ */
import type { import type {
ChallengeCompletion,
CompletionData, CompletionData,
GameVersion, GameVersion,
PeacockLocationsData, PeacockLocationsData,
Unlockable, Unlockable,
} from "../types/types" } from "../types/types"
import { swapToBrowsingMenusStatus } from "../discordRp" import { swapToBrowsingMenusStatus } from "../discord/discordRp"
import { getUserData } from "../databaseHandler" import { getUserData } from "../databaseHandler"
import { controller } from "../controller" import { controller } from "../controller"
import { contractCreationTutorialId, getMaxProfileLevel } from "../utils" import { contractCreationTutorialId, getMaxProfileLevel } from "../utils"
@ -53,7 +54,32 @@ type CareerEntryChild = {
ImageLocked: string ImageLocked: string
RequiredResources: string[] RequiredResources: string[]
IsPack?: boolean 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) { export function getHubData(gameVersion: GameVersion, userId: string) {
@ -71,17 +97,33 @@ export function getHubData(gameVersion: GameVersion, userId: string) {
gameVersion, gameVersion,
true, true,
) )
const career: Record<string, CareerEntry> =
gameVersion === "h3" const career: Record<string, CareerEntry> = {}
? {}
: { for (const [
// 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. id,
ELUSIVES_UNSUPPORTED: { pack,
Children: [], ] of controller.challengeService.challengePacks.entries()) {
Name: "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", if (!pack.GameVersions.includes(gameVersion)) continue
Location:
career[id] = {
Children: [
generateCareerEntryChild(
locations.parents["LOCATION_PARENT_ICA_FACILITY"], 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 = [] const masteryData = []
@ -168,22 +210,14 @@ export function getHubData(gameVersion: GameVersion, userId: string) {
gameVersion, gameVersion,
) )
career[parent!]?.Children.push({ career[parent!]?.Children.push(
IsLocked: Boolean(location.Properties.IsLocked), generateCareerEntryChild(
Name: location.DisplayNameLocKey, location,
Image: location.Properties.Icon || "", challengeCompletion,
Icon: location.Type, // should be "location" for all locations child,
CompletedChallengesCount: generateCompletionData(child, userId, gameVersion),
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),
})
} }
return { return {

View File

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

View File

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

View File

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

View File

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

View File

@ -170,6 +170,12 @@ export const EXECUTIVE_UNLOCKABLES = [
"PROP_CONTAINER_SUITCASE_ICA_DELUXE", "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 = [ export const DELUXE_UNLOCKABLES = [
// dubai // dubai
"PROP_CONTAINER_SUITCASE_GOLDEN", "PROP_CONTAINER_SUITCASE_GOLDEN",
@ -271,6 +277,13 @@ export const CONCRETEART_UNLOCKABLES = [
"TOKEN_OUTFIT_HERO_CONCRETEART", "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 = [ export const brokenItems = [
// duped dart gun (thanks IOI) // duped dart gun (thanks IOI)
"835ad050-6d19-4e94-80b1-f5cec9815ba3", "835ad050-6d19-4e94-80b1-f5cec9815ba3",

View File

@ -81,6 +81,8 @@ export const H3_EPIC_ENTITLEMENTS = [
"a1e9a63fa4f3425aa66b9b8fa3c9cc35", "a1e9a63fa4f3425aa66b9b8fa3c9cc35",
// THESARAJEVOSIX: // THESARAJEVOSIX:
"28455871cd0d4ffab52f557cc012ea5e", "28455871cd0d4ffab52f557cc012ea5e",
// SAMBUCA:
"9220c020262f420da06eb46a4b1ce86f",
] ]
export const H2_STEAM_ENTITLEMENTS = [ export const H2_STEAM_ENTITLEMENTS = [
@ -138,9 +140,7 @@ export function getPlatformEntitlements(
req: RequestWithJwt, req: RequestWithJwt,
res: Response, res: Response,
): void { ): 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 const exts = getUserData(req.jwt.unique_name, req.gameVersion).Extensions
.entP .entP

View File

@ -58,7 +58,11 @@ import {
compileRuntimeChallenge, compileRuntimeChallenge,
inclusionDataCheck, inclusionDataCheck,
} from "./candle/challengeHelpers" } from "./candle/challengeHelpers"
import { LoadSaveBody, ResolveGamerTagsBody } from "./types/gameSchemas" import {
GetChallengeProgressionBody,
LoadSaveBody,
ResolveGamerTagsBody,
} from "./types/gameSchemas"
import assert from "assert" import assert from "assert"
const profileRouter = Router() const profileRouter = Router()
@ -285,7 +289,6 @@ export async function resolveProfiles(
Gamertag: null, Gamertag: null,
DevId: "IOI", DevId: "IOI",
SteamId: null, SteamId: null,
StadiaId: null,
EpicId: null, EpicId: null,
NintendoId: null, NintendoId: null,
XboxLiveId: null, XboxLiveId: null,
@ -312,7 +315,6 @@ export async function resolveProfiles(
fakePlayer.platform === "steam" fakePlayer.platform === "steam"
? fakePlayer.platformId ? fakePlayer.platformId
: null, : null,
StadiaId: null,
EpicId: EpicId:
fakePlayer.platform === "epic" fakePlayer.platform === "epic"
? fakePlayer.platformId ? 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( profileRouter.post(
"/HubPagesService/GetChallengeTreeFor", "/HubPagesService/GetChallengeTreeFor",
jsonMiddleware(), jsonMiddleware(),
@ -732,13 +782,6 @@ profileRouter.post(
"No such save detected! Might be an official servers save.", "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( log(
LogLevel.WARN, LogLevel.WARN,
"Creating a fake session to avoid problems... scoring will not work!", "Creating a fake session to avoid problems... scoring will not work!",

View File

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

View File

@ -16,19 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 { randomUUID } from "crypto"
import assert from "assert"
/** /**
* The payload provided to the game for each target in the create menu route. * The payload provided to the game for each target in the create menu route.
*/ */
export interface IContractCreationPayload { export type ContractCreationNpcTargetPayload = {
RepositoryId: string RepositoryId: string
Selected: boolean Selected: boolean
Weapon: { Weapon: {
RepositoryId: string RepositoryId: string
KillMethodBroad: string KillMethodBroad: string
KillMethodStrict: string KillMethodStrict: string
RequiredKillMethod: string
RequiredKillMethodType: number RequiredKillMethodType: number
} }
Outfit: { Outfit: {
@ -39,43 +41,24 @@ export interface IContractCreationPayload {
} }
/** /**
* The target creator API. * @internal
*/ */
export class TargetCreator { export const createTargetKillObjective = (
// @ts-expect-error TODO: type this params: ContractCreationNpcTargetPayload,
private _targetSm ): MissionManifestObjective => ({
// @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", Type: "statemachine",
Id: randomUUID(), Id: randomUUID(),
BriefingText: { BriefingText: {
$loc: { $loc: {
key: "UI_CONTRACT_GENERAL_OBJ_KILL", key: "UI_CONTRACT_GENERAL_OBJ_KILL",
data: `$($repository ${this._params.RepositoryId}).Name`, data: `$($repository ${params.RepositoryId}).Name`,
}, },
}, },
HUDTemplate: { HUDTemplate: {
display: { display: {
$loc: { $loc: {
key: "UI_CONTRACT_GENERAL_OBJ_KILL", key: "UI_CONTRACT_GENERAL_OBJ_KILL",
data: `$($repository ${this._params.RepositoryId}).Name`, data: `$($repository ${params.RepositoryId}).Name`,
}, },
}, },
}, },
@ -83,39 +66,34 @@ export class TargetCreator {
Definition: { Definition: {
Scope: "Hit", Scope: "Hit",
Context: { Context: {
Targets: [this._params.RepositoryId], Targets: [params.RepositoryId],
}, },
States: { States: {
Start: { Start: {
Kill: [ Kill: {
// this is the base state machine, we don't have fail states
{
Condition: { Condition: {
$eq: [ $eq: ["$Value.RepositoryId", params.RepositoryId],
"$Value.RepositoryId",
this._params.RepositoryId,
],
}, },
Transition: "Success", Transition: "Success",
}, },
],
}, },
}, },
}, },
} })
}
private _createOutfitSm(hmSuit: boolean): void { /**
this._requireTargetConds() * @internal
*/
this._outfitSm = { export const createRequiredOutfitObjective = (
params: ContractCreationNpcTargetPayload,
): MissionManifestObjective => ({
Type: "statemachine", Type: "statemachine",
Id: randomUUID(), Id: randomUUID(),
Category: "secondary", Category: "secondary",
Definition: { Definition: {
Scope: "Hit", Scope: "Hit",
Context: { Context: {
Targets: [this._params.RepositoryId], Targets: [params.RepositoryId],
}, },
States: { States: {
Start: { Start: {
@ -126,10 +104,10 @@ export class TargetCreator {
{ {
$eq: [ $eq: [
"$Value.RepositoryId", "$Value.RepositoryId",
this._params.RepositoryId, params.RepositoryId,
], ],
}, },
hmSuit params.Outfit.IsHitmanSuit
? { ? {
$eq: [ $eq: [
"$Value.OutfitIsHitmanSuit", "$Value.OutfitIsHitmanSuit",
@ -139,21 +117,130 @@ export class TargetCreator {
: { : {
$eq: [ $eq: [
"$Value.OutfitRepositoryId", "$Value.OutfitRepositoryId",
this._params.Outfit params.Outfit.RepositoryId,
.RepositoryId,
], ],
}, },
], ],
}, },
Transition: "Success", Transition: "Success",
}, },
// state machines fall through to the next state if the condition is not met
{ {
Condition: { Condition: {
$eq: [ $eq: ["$Value.RepositoryId", params.RepositoryId],
"$Value.RepositoryId", },
this._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
}
const objectives: MissionManifestObjective[] = [targetSm]
// 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
}
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: outfitSm.Id,
// "Amazing!" - Athena Savalas
KillMethod: "",
})
objectives.push(outfitSm)
}
if (params.Weapon.RequiredKillMethodType !== 0) {
const weaponSm = createWeaponObjective(
params.Weapon,
params.RepositoryId,
)
if (customIds?.kill) {
weaponSm.Id = customIds.kill
}
targetSm.TargetConditions ??= []
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,
})
objectives.push(weaponSm)
}
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", Transition: "Failure",
}, },
], ],
@ -161,47 +248,10 @@ export class TargetCreator {
}, },
}, },
} }
this._targetConds?.push({
Type: hmSuit ? "hitmansuit" : "disguise",
RepositoryId: this._params.Outfit.RepositoryId,
HardCondition: false,
ObjectiveId: this._outfitSm.Id,
// ioi moment
KillMethod: "",
})
} }
/** /**
* Get the array of finalized state machines. * Create a time limit objective.
*
* @returns The state machines.
*/
public build(): MissionManifestObjective[] {
this._bootstrapEntrySm()
if (this._params.Outfit.Required) {
// not any disguise
this._createOutfitSm(this._params.Outfit.IsHitmanSuit)
}
const values = [this._targetSm]
if (this._outfitSm) {
values.push(this._outfitSm)
}
if (this._targetConds && Array.isArray(this._targetConds)) {
this._targetSm.TargetConditions = this._targetConds
}
return values
}
}
/**
* The time limit creator API.
*
* @param time The amount of time to use in seconds. * @param time The amount of time to use in seconds.
* @param optional If the objective should be optional or not. * @param optional If the objective should be optional or not.
* @returns The generated state machine. * @returns The generated state machine.
@ -258,18 +308,14 @@ export function createTimeLimit(
Scope: "session", Scope: "session",
States: { States: {
Start: { Start: {
IntroCutEnd: [ IntroCutEnd: {
{
Transition: "TimerRunning", Transition: "TimerRunning",
}, },
],
}, },
TimerRunning: { TimerRunning: {
exit_gate: [ exit_gate: {
{
Transition: "Success", Transition: "Success",
}, },
],
$timer: [ $timer: [
{ {
Condition: { 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 prompts from "prompts"
import { log, LogLevel } from "./loggingInterop" import { log, LogLevel } from "./loggingInterop"
import { readdir, writeFile } from "fs/promises" import { readdir, writeFile } from "fs/promises"
import { PEACOCKVER, PEACOCKVERSTRING } from "./utils" import { PEACOCKVERSTRING } from "./utils"
import md5File from "md5-file" import md5File from "md5-file"
import { arch, cpus as cpuList, platform, version } from "os" import { arch, cpus as cpuList, platform, version } from "os"
import { Controller, isPlugin } from "./controller" import { Controller, isPlugin } from "./controller"
@ -116,7 +116,6 @@ async function exportDebugInfo(): Promise<void> {
const data = { const data = {
version: PEACOCKVERSTRING, version: PEACOCKVERSTRING,
ident: PEACOCKVER,
presentConfigs: Object.keys(configs), presentConfigs: Object.keys(configs),
chunkDigest: await md5File("chunk0.js"), chunkDigest: await md5File("chunk0.js"),
patcherDigest: await md5File("PeacockPatcher.exe"), patcherDigest: await md5File("PeacockPatcher.exe"),

View File

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

View File

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

View File

@ -27,9 +27,3 @@ declare let PEACOCK_DEV: boolean
* Injected during the build process. * Injected during the build process.
*/ */
declare let HUMAN_VERSION: string 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,8 +43,7 @@ export type ScoringHeadline = {
scoreTotal: number scoreTotal: number
} }
export type ManifestScoringModule = export type ManifestScoringModule = ScoringModule & {
| ScoringModule & {
Type: string Type: string
} }

View File

@ -18,7 +18,7 @@
import type * as core from "express-serve-static-core" 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 { Request } from "express"
import { import {
ChallengeContext, ChallengeContext,
@ -192,7 +192,7 @@ export type MissionType =
/** /**
* The data acquired when using the "contract search" functionality. * The data acquired when using the "contract search" functionality.
*/ */
export interface ContractSearchResult { export type ContractSearchResult = {
Data: { Data: {
Contracts: { Contracts: {
UserCentricContract: UserCentricContract UserCentricContract: UserCentricContract
@ -210,9 +210,14 @@ export interface ContractSearchResult {
* *
* @see ContractSession * @see ContractSession
*/ */
export interface ContractSessionLastKill { export type ContractSessionLastKill = {
timestamp?: Date | number timestamp?: Date | number
repositoryIds?: RepositoryId[] 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 * @see SaveFile
*/ */
export interface UpdateUserSaveFileTableBody { export type UpdateUserSaveFileTableBody = {
clientSaveFileList: SaveFile[] clientSaveFileList: SaveFile[]
deletedSaveFileList: SaveFile[] deletedSaveFileList: SaveFile[]
} }
@ -336,12 +341,12 @@ export interface UpdateUserSaveFileTableBody {
/** /**
* The Hitman server version in object form. * The Hitman server version in object form.
*/ */
export interface ServerVersion { export type ServerVersion = Readonly<{
readonly _Major: number _Major: number
readonly _Minor: number _Minor: number
readonly _Build: number _Build: number
readonly _Revision: number _Revision: number
} }>
/** /**
* An event sent from the game client to the server. * 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. * A server to client push message. The message component is encoded JSON.
*/ */
export interface PushMessage { export type PushMessage = {
time: number | string | bigint time: number | string | bigint
message: string message: string
} }
@ -393,7 +398,7 @@ export interface ServerToClientEvent<EventValue = unknown> {
Origin?: string | null Origin?: string | null
} }
export interface MissionStory { export type MissionStory = {
CommonRepositoryId: RepositoryId CommonRepositoryId: RepositoryId
PreviouslyCompleted: boolean PreviouslyCompleted: boolean
IsMainOpportunity: boolean IsMainOpportunity: boolean
@ -405,7 +410,7 @@ export interface MissionStory {
Image: string Image: string
} }
export interface PlayerProfileView { export type PlayerProfileView = {
SubLocationData: { SubLocationData: {
ParentLocation: Unlockable ParentLocation: Unlockable
Location: Unlockable Location: Unlockable
@ -433,34 +438,34 @@ export interface PlayerProfileView {
} }
} }
export interface ChallengeCompletion { export type ChallengeCompletion = {
ChallengesCount: number ChallengesCount: number
CompletedChallengesCount: number CompletedChallengesCount: number
CompletionPercent?: number CompletionPercent?: number
} }
export interface ChallengeCategoryCompletion extends ChallengeCompletion { export type ChallengeCategoryCompletion = ChallengeCompletion & {
Name: string Name: string
} }
export interface OpportunityStatistics { export type OpportunityStatistics = {
Count: number Count: number
Completed: number Completed: number
} }
export interface ContractHistory { export type ContractHistory = {
LastPlayedAt?: number LastPlayedAt?: number
Completed?: boolean Completed?: boolean
IsEscalation?: boolean IsEscalation?: boolean
} }
export interface ProgressionData { export type ProgressionData = {
Xp: number Xp: number
Level: number Level: number
PreviouslySeenXp: number PreviouslySeenXp: number
} }
export interface UserProfile { export type UserProfile = {
Id: string Id: string
LinkedAccounts: { LinkedAccounts: {
dev?: string dev?: string
@ -506,10 +511,11 @@ export interface UserProfile {
*/ */
Total: number Total: number
Sublocations: { Sublocations: {
Location: string [location: string]: {
Xp: number Xp: number
ActionXp: number ActionXp: number
}[] }
}
} }
/** /**
* If the mastery location has subpackages and not drops, it will * If the mastery location has subpackages and not drops, it will
@ -555,12 +561,12 @@ export interface UserProfile {
[opportunityId: RepositoryId]: boolean [opportunityId: RepositoryId]: boolean
} }
CPD: CPDStore CPD: CPDStore
LastOfficialSync: Date | string | null
} }
ETag: string | null ETag: string | null
Gamertag: string Gamertag: string
DevId: string | null DevId: string | null
SteamId: string | null SteamId: string | null
StadiaId: string | null
EpicId: string | null EpicId: string | null
NintendoId: string | null NintendoId: string | null
XboxLiveId: string | null XboxLiveId: string | null
@ -572,7 +578,7 @@ export interface UserProfile {
Version: number Version: number
} }
export interface RatingKill { export type RatingKill = {
IsHeadshot: boolean IsHeadshot: boolean
KillClass: string KillClass: string
KillItemCategory: string KillItemCategory: string
@ -588,7 +594,7 @@ export interface RatingKill {
OutfitRepoId: string OutfitRepoId: string
} }
export interface NamespaceEntitlementEpic { export type NamespaceEntitlementEpic = {
namespace: string namespace: string
itemId: string itemId: string
owned: boolean owned: boolean
@ -597,7 +603,7 @@ export interface NamespaceEntitlementEpic {
/** /**
* An unlockable item. * An unlockable item.
*/ */
export interface Unlockable { export type Unlockable = {
Id: string Id: string
DisplayNameLocKey: string DisplayNameLocKey: string
GameAsset: string | null GameAsset: string | null
@ -702,14 +708,14 @@ export interface Unlockable {
Rarity?: string | null Rarity?: string | null
} }
export interface ItemGameplay { export type ItemGameplay = {
range?: number range?: number
damage?: number damage?: number
clipsize?: number clipsize?: number
rateoffire?: number rateoffire?: number
} }
export interface CompletionData { export type CompletionData = {
Level: number Level: number
MaxLevel: number MaxLevel: number
XP: number XP: number
@ -723,7 +729,7 @@ export interface CompletionData {
Name: string | null Name: string | null
} }
export interface UserCentricContract { export type UserCentricContract = {
Contract: MissionManifest Contract: MissionManifest
Data: { Data: {
IsLocked: boolean IsLocked: boolean
@ -755,12 +761,27 @@ export interface UserCentricContract {
} }
} }
export interface TargetCondition { export type TargetCondition = {
Type: string /**
* 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 RepositoryId?: RepositoryId
/**
* If the game should display the objective as optional or not.
*/
HardCondition?: boolean 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 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. * Data structure for a mission manifest's `Data.VR` bricks property.
*/ */
export interface VRQualityDefinition { export type VRQualityDefinition = {
Quality: string Quality: string
Bricks: string[] Bricks: string[]
} }
@ -983,7 +1004,7 @@ export interface MissionManifestMetadata {
Modules?: ManifestScoringModule[] | null Modules?: ManifestScoringModule[] | null
} }
export interface GroupObjectiveDisplayOrderItem { export type GroupObjectiveDisplayOrderItem = {
Id: string Id: string
IsNew?: boolean IsNew?: boolean
} }
@ -1060,7 +1081,7 @@ export interface MissionManifest {
* A configuration that tells the game where it should connect to. * 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. * This config is the first thing that the game asks for when logging in.
*/ */
export interface ServerConnectionConfig { export type ServerConnectionConfig = {
Versions: { Versions: {
Name: string Name: string
GAME_VER: string GAME_VER: string
@ -1093,7 +1114,7 @@ export interface ServerConnectionConfig {
* *
* @see GameLocationsData * @see GameLocationsData
*/ */
export interface PeacockLocationsData { export type PeacockLocationsData = {
/** /**
* The parent locations. * 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. * The body sent with the CreateFromParams request from the game during the final phase of contract creation.
* *
* @see IContractCreationPayload * @see ContractCreationNpcTargetPayload
*/ */
export interface CreateFromParamsBody { export interface CreateFromParamsBody {
creationData: { creationData: {
@ -1133,12 +1154,12 @@ export interface CreateFromParamsBody {
Description: string Description: string
ContractId: string ContractId: string
ContractPublicId: string ContractPublicId: string
Targets: IContractCreationPayload[] Targets: ContractCreationNpcTargetPayload[]
ContractConditionIds: string[] ContractConditionIds: string[]
} }
} }
export interface SelectEntranceOrPickupData { export type SelectEntranceOrPickupData = {
Unlocked: string[] Unlocked: string[]
Contract: MissionManifest Contract: MissionManifest
OrderedUnlocks: Unlockable[] OrderedUnlocks: Unlockable[]
@ -1150,7 +1171,7 @@ export type CompiledIoiStatemachine = unknown
/** /**
* The `ChallengeProgress` data for a `challengetree` context listener. * The `ChallengeProgress` data for a `challengetree` context listener.
*/ */
export interface ChallengeProgressCTreeContextListener { export type ChallengeProgressCTreeContextListener = {
total: number total: number
completed: string[] | readonly string[] completed: string[] | readonly string[]
missing: number missing: number
@ -1161,28 +1182,29 @@ export interface ChallengeProgressCTreeContextListener {
/** /**
* The `ChallengeProgress` data for a `challengecount` context listener. * The `ChallengeProgress` data for a `challengecount` context listener.
*/ */
export interface ChallengeProgressCCountContextListener { export type ChallengeProgressCCountContextListener = {
total: number total: number
count: number count: number
} }
export interface CompiledChallengeTreeCategory { export type CompiledChallengeTreeCategory = {
CategoryId: string CategoryId: string
ChallengesCount: number ChallengesCount: number
CompletedChallengesCount: number CompletedChallengesCount: number
CompletionData: CompletionData CompletionData: CompletionData | Record<string, never>
Icon: string Icon: string
Image: string Image: string
ImageLocked?: string ImageLocked?: string
IsLocked: boolean IsLocked: boolean
Location: Unlockable Location: Unlockable | null
Name: string Name: string
RequiredResources: string[] RequiredResources: string[]
OrderIndex: number
SwitchData: { SwitchData: {
Data: { Data: {
CategoryData: CompiledChallengeTreeCategoryInfo CategoryData: CompiledChallengeTreeCategoryInfo
Challenges: CompiledChallengeTreeData[] Challenges: CompiledChallengeTreeData[]
CompletionData: CompletionData CompletionData: CompletionData | Record<string, never>
HasNext: boolean HasNext: boolean
HasPrevious: boolean HasPrevious: boolean
NextCategoryIcon?: string NextCategoryIcon?: string
@ -1192,7 +1214,7 @@ export interface CompiledChallengeTreeCategory {
} }
} }
export interface CompiledChallengeTreeCategoryInfo { export type CompiledChallengeTreeCategoryInfo = {
Name: string Name: string
Image: string Image: string
Icon: string Icon: string
@ -1209,7 +1231,7 @@ export type ChallengeTreeWaterfallState =
| ChallengeProgressCCountContextListener | ChallengeProgressCCountContextListener
| null | null
export interface CompiledChallengeTreeData { export type CompiledChallengeTreeData = {
CategoryName: string CategoryName: string
ChallengeProgress?: ChallengeTreeWaterfallState ChallengeProgress?: ChallengeTreeWaterfallState
Completed: boolean Completed: boolean
@ -1230,6 +1252,7 @@ export interface CompiledChallengeTreeData {
LocationId: string LocationId: string
Name: string Name: string
ParentLocationId: string ParentLocationId: string
OrderIndex: number
Rewards: { Rewards: {
MasteryXP?: number MasteryXP?: number
} }
@ -1244,7 +1267,7 @@ export interface InclusionData {
GameModes?: string[] GameModes?: string[]
} }
export interface CompiledChallengeIngameData { export type CompiledChallengeIngameData = {
Id: string Id: string
GroupId?: string GroupId?: string
Name: string Name: string
@ -1268,15 +1291,15 @@ export interface CompiledChallengeIngameData {
/** /**
* Game-facing challenge progression data. * Game-facing challenge progression data.
*/ */
export interface ChallengeProgressionData { export type ChallengeProgressionData = {
ChallengeId: string ChallengeId: string
ProfileId: string ProfileId: string
Completed: boolean Completed: boolean
Ticked: boolean Ticked?: boolean
ETag?: string ETag?: string
State: Record<string, unknown> State: Record<string, unknown>
CompletedAt: Date | string | null CompletedAt: Date | string | null
MustBeSaved: boolean MustBeSaved?: boolean
} }
export interface CompiledChallengeRuntimeData { export interface CompiledChallengeRuntimeData {
@ -1295,7 +1318,7 @@ export type Flags = Record<
/** /**
* A "hit" object. * A "hit" object.
*/ */
export interface IHit { export type Hit = {
Id: string Id: string
UserCentricContract: UserCentricContract UserCentricContract: UserCentricContract
Location: Unlockable Location: Unlockable
@ -1315,7 +1338,7 @@ export interface IHit {
* @see CampaignVideo * @see CampaignVideo
* @see StoryData * @see StoryData
*/ */
export interface IVideo { export type Video = {
VideoTitle: string VideoTitle: string
VideoHeader: string VideoHeader: string
VideoId: string VideoId: string
@ -1334,21 +1357,21 @@ export interface IVideo {
/** /**
* A campaign mission item. * A campaign mission item.
* *
* @see IHit * @see Hit
*/ */
export type CampaignMission = { export type CampaignMission = {
Type: "Mission" Type: "Mission"
Data: IHit Data: Hit
} }
/** /**
* A campaign video item. * A campaign video item.
* *
* @see IVideo * @see Video
*/ */
export type CampaignVideo = { export type CampaignVideo = {
Type: "Video" Type: "Video"
Data: IVideo Data: Video
} }
export interface RegistryChallenge extends SavedChallenge { export interface RegistryChallenge extends SavedChallenge {
@ -1368,7 +1391,7 @@ export type StoryData = CampaignMission | CampaignVideo
/** /**
* A campaign object. * A campaign object.
*/ */
export interface Campaign { export type Campaign = {
Name: string Name: string
Image: string Image: string
Type: MissionType | string Type: MissionType | string
@ -1383,7 +1406,7 @@ export interface Campaign {
/** /**
* A loadout. * A loadout.
*/ */
export interface Loadout { export type Loadout = {
/** /**
* Random ID. * Random ID.
* *
@ -1439,25 +1462,25 @@ export type GenSingleVideoFunc = (
/** /**
* A "hits category" is used to display lists of contracts in-game. * A "hits category" is used to display lists of contracts in-game.
* *
* @see IHit * @see Hit
*/ */
export interface HitsCategoryCategory { export type HitsCategoryCategory = {
Category: string Category: string
Data: { Data: {
Type: string Type: string
Hits: IHit[] Hits: Hit[]
Page: number Page: number
HasMore: boolean HasMore: boolean
} }
CurrentSubType: string CurrentSubType: string
} }
export interface PlayNextCampaignDetails { export type PlayNextCampaignDetails = {
CampaignName: string CampaignName: string
ParentCampaignName?: string ParentCampaignName?: string
} }
export interface PlayNextGetCampaignsHookReturn { export type PlayNextGetCampaignsHookReturn = {
/** /**
* The UUID of the next contract in the campaign. * 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> export type ContractProgressionData = Record<string, string | number | boolean>
/** SMF's lastDeploy.json */ /** SMF's lastDeploy.json */
export interface SMFLastDeploy { export type SMFLastDeploy = {
runtimePath: string runtimePath: string
retailPath: string retailPath: string
skipIntro: boolean skipIntro: boolean
@ -1551,3 +1574,9 @@ export interface SMFLastDeploy {
peacockPlugins?: string[] 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 { import type {
GameVersion, GameVersion,
MissionManifestObjective, MissionManifestObjective,
OfficialSublocation,
PeacockLocationsData, PeacockLocationsData,
RepositoryId, RepositoryId,
RequestWithJwt, RequestWithJwt,
@ -33,6 +34,7 @@ import { log, LogLevel } from "./loggingInterop"
import { writeFileSync } from "fs" import { writeFileSync } from "fs"
import { getFlag } from "./flags" import { getFlag } from "./flags"
import { getConfig, getVersionedConfig } from "./configSwizzleManager" import { getConfig, getVersionedConfig } from "./configSwizzleManager"
import { compare } from "semver"
/** /**
* True if the server is being run by the launcher, false otherwise. * 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 = { export const ServerVer: ServerVersion = {
_Major: 8, _Major: 8,
_Minor: 14, _Minor: 15,
_Build: 0, _Build: 0,
_Revision: 0, _Revision: 0,
} }
export const PEACOCKVER = REV_IDENT
export const PEACOCKVERSTRING = HUMAN_VERSION export const PEACOCKVERSTRING = HUMAN_VERSION
export const uuidRegex = export const uuidRegex =
@ -54,6 +55,9 @@ export const uuidRegex =
export const contractTypes = ["featured", "usercreated"] export const contractTypes = ["featured", "usercreated"]
/**
* A list of game versions, except scpc.
*/
export const versions: Exclude<GameVersion, "scpc">[] = ["h1", "h2", "h3"] export const versions: Exclude<GameVersion, "scpc">[] = ["h1", "h2", "h3"]
export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb" 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. * 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> { export async function checkForUpdates(): Promise<void> {
if (getFlag("updateChecking") === false) { if (getFlag("updateChecking") === false) {
@ -71,29 +75,38 @@ export async function checkForUpdates(): Promise<void> {
} }
try { try {
const res = await fetch( type VersionCheckResponse = { id: string; channel: string }
"https://backend.rdil.rocks/peacock/latest-version",
)
const current = parseInt(await res.text(), 10)
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( log(
LogLevel.INFO, 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) { } else if (current === 0) {
log(LogLevel.DEBUG, "Peacock is up to date.") log(LogLevel.DEBUG, "Peacock is up to date.", "updates")
} else { } else {
log( log(
LogLevel.WARN, LogLevel.WARN,
`Peacock is out-of-date! Check the Discord for the latest release.`, `Peacock is out-of-date! Check the Discord for the latest release.`,
"updates",
) )
} }
} catch (e) { } 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 { export function getRemoteService(gameVersion: GameVersion): string | undefined {
switch (gameVersion) { switch (gameVersion) {
case "h3": case "h3":
@ -181,14 +194,12 @@ export const EVERGREEN_LEVEL_INFO: number[] = [
export function evergreenLevelForXp(xp: number): number { export function evergreenLevelForXp(xp: number): number {
for (let i = 1; i < EVERGREEN_LEVEL_INFO.length; i++) { for (let i = 1; i < EVERGREEN_LEVEL_INFO.length; i++) {
if (xp >= EVERGREEN_LEVEL_INFO[i]) { if (xp < EVERGREEN_LEVEL_INFO[i]) {
continue
}
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 { export function sniperLevelForXp(xp: number): number {
for (let i = 1; i < SNIPER_LEVEL_INFO.length; i++) { for (let i = 1; i < SNIPER_LEVEL_INFO.length; i++) {
if (xp >= SNIPER_LEVEL_INFO[i]) { if (xp < SNIPER_LEVEL_INFO[i]) {
continue
}
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 profile The user profile to update
* @param gameVersion The game version * @param gameVersion The game version
* @returns The updated user profile.
*/ */
function updateUserProfile( function updateUserProfile(
profile: UserProfile, profile: UserProfile,
@ -258,6 +266,29 @@ function updateUserProfile(
case LATEST_PROFILE_VERSION: case LATEST_PROFILE_VERSION:
// This profile updated to the latest version, we're done. // This profile updated to the latest version, we're done.
return 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: { default: {
// Check that the profile version is indeed undefined. If it isn't, // Check that the profile version is indeed undefined. If it isn't,
// we've forgotten to add a version to the switch. // we've forgotten to add a version to the switch.
@ -647,14 +678,14 @@ export function handleAxiosError(error: AxiosError): void {
if (error.response) { if (error.response) {
// The request was made and the server responded with a status code // The request was made and the server responded with a status code
// that falls out of the range of 2xx // 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) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of http.ClientRequest // `error.request` is an instance of http.ClientRequest
log(LogLevel.DEBUG, `bad fetch`) log(LogLevel.DEBUG, `Request error: bad fetch`)
} else { } else {
// Something happened in setting up the request that triggered an Error // 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" ? suitsToTypeMap[repoId] !== "disguise"
: false : 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 { NextFunction, Request, Response, Router } from "express"
import { getConfig } from "./configSwizzleManager" import { getConfig } from "./configSwizzleManager"
import { readFileSync } from "atomically" import { readdir, readFile } from "fs/promises"
import { GameVersion, UserProfile } from "./types/types" import {
ChallengeProgressionData,
GameVersion,
HitsCategoryCategory,
OfficialSublocation,
ProgressionData,
UserProfile,
} from "./types/types"
import { join } from "path" 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 { getUserData, loadUserData, writeUserData } from "./databaseHandler"
import { readdirSync } from "fs"
import { controller } from "./controller" import { controller } from "./controller"
import { log, LogLevel } from "./loggingInterop" 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() const webFeaturesRouter = Router()
@ -34,9 +64,9 @@ if (PEACOCK_DEV) {
res.set("Access-Control-Allow-Origin", "*") res.set("Access-Control-Allow-Origin", "*")
res.set( res.set(
"Access-Control-Allow-Methods", "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() next()
}) })
} }
@ -86,8 +116,13 @@ webFeaturesRouter.get("/codenames", (_, res) => {
res.json(getConfig("EscalationCodenames", false)) res.json(getConfig("EscalationCodenames", false))
}) })
webFeaturesRouter.get("/local-users", (req: CommonRequest, res) => { webFeaturesRouter.get("/local-users", async (req: CommonRequest, res) => {
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) { // 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([]) res.json([])
return return
} }
@ -100,21 +135,35 @@ webFeaturesRouter.get("/local-users", (req: CommonRequest, res) => {
dir = join("userdata", req.query.gv, "users") dir = join("userdata", req.query.gv, "users")
} }
const files: string[] = readdirSync(dir).filter( const files: string[] = (await readdir(dir)).filter(
(name) => name !== "lop.json", (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) { for (const file of files) {
if (file === "lop.json") continue
const read = JSON.parse( const read = JSON.parse(
readFileSync(join(dir, file)).toString(), (await readFile(join(dir, file))).toString(),
) as UserProfile ) as UserProfile
result.push({ result.push({
id: read.Id, id: read.Id,
name: read.Gamertag, name: read.Gamertag,
platform: read.EpicId ? "Epic" : "Steam", 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 } export { webFeaturesRouter }

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{ {
"meta": { "meta": {
"Location": "LOCATION_PARENT_ROCKY", "Location": "LOCATION_PARENT_ROCKY",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination", "Icon": "challenge_category_assassination",
"CategoryId": "assassination", "CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [ "Challenges": [
{ {
"Id": "2638c6ab-20e5-4f5f-bcbb-2f940c7adee3", "Id": "2638c6ab-20e5-4f5f-bcbb-2f940c7adee3",
@ -962,6 +963,7 @@
"Icon": "challenge_category_discovery", "Icon": "challenge_category_discovery",
"CategoryId": "discovery", "CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [ "Challenges": [
{ {
"Id": "ad549a16-c8c7-40af-9265-f5c42065be87", "Id": "ad549a16-c8c7-40af-9265-f5c42065be87",
@ -1671,6 +1673,7 @@
"Icon": "challenge_category_feats", "Icon": "challenge_category_feats",
"CategoryId": "feats", "CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [ "Challenges": [
{ {
"Id": "8d27e376-c658-4f1a-8b97-a76e99575539", "Id": "8d27e376-c658-4f1a-8b97-a76e99575539",
@ -2103,6 +2106,7 @@
"Icon": "challenge_category_targets", "Icon": "challenge_category_targets",
"CategoryId": "targets", "CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [ "Challenges": [
{ {
"Id": "b5213925-b20e-4ce9-9c70-d60b2771d541", "Id": "b5213925-b20e-4ce9-9c70-d60b2771d541",
@ -2196,6 +2200,7 @@
"Icon": "profile", "Icon": "profile",
"CategoryId": "classic", "CategoryId": "classic",
"Description": "", "Description": "",
"OrderIndex": 4,
"Challenges": [ "Challenges": [
{ {
"Id": "425a7a49-1892-4a24-aa40-f46cdb8d6c7f", "Id": "425a7a49-1892-4a24-aa40-f46cdb8d6c7f",
@ -3047,6 +3052,7 @@
"Icon": "challenge_category_feats", "Icon": "challenge_category_feats",
"CategoryId": "argon-pack", "CategoryId": "argon-pack",
"Description": "", "Description": "",
"OrderIndex": 6.6,
"Challenges": [ "Challenges": [
{ {
"Id": "0409d6ed-bb3b-4628-a9d4-90cbfb7c1793", "Id": "0409d6ed-bb3b-4628-a9d4-90cbfb7c1793",
@ -3084,6 +3090,142 @@
"Tags": ["argon-pack", "story", "live", "medium"] "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"], "Entitlements": ["LOCATION_GOLDEN"],
"InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f", "InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f",
"GroupObjectiveDisplayOrder": [ "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": {} "UserData": {}

View File

@ -53,8 +53,8 @@
"Entitlements": ["LOCATION_GOLDEN"], "Entitlements": ["LOCATION_GOLDEN"],
"InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f", "InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f",
"GroupObjectiveDisplayOrder": [ "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": {} "UserData": {}

View File

@ -59,8 +59,8 @@
"Entitlements": ["LOCATION_GOLDEN"], "Entitlements": ["LOCATION_GOLDEN"],
"InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f", "InGroup": "797e204a-ef3d-463b-a386-57df0fe29b8f",
"GroupObjectiveDisplayOrder": [ "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": {} "UserData": {}

View File

@ -1,7 +1,7 @@
{ {
"meta": { "meta": {
"Location": "GLOBAL_ARCADE_CHALLENGES", "Location": "GLOBAL_ARCADE_CHALLENGES",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,7 +10,51 @@
"Icon": "arcademode", "Icon": "arcademode",
"CategoryId": "arcade", "CategoryId": "arcade",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ARCADE", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ARCADE",
"OrderIndex": 6,
"Challenges": [ "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", "Id": "d00653ca-d00d-4b30-bff9-3c03c358adc8",
"Name": "UI_APPLE_COMPLETION_CHALLENGE_NAME", "Name": "UI_APPLE_COMPLETION_CHALLENGE_NAME",
@ -664,7 +708,7 @@
"Rewards": { "Rewards": {
"MasteryXP": 4000 "MasteryXP": 4000
}, },
"Drops": ["TOKEN_OUTFIT_HERO_BUTCHER_SUIT"], "Drops": ["TOKEN_OUTFIT_HERO_PURPLESPECIAL_SUIT"],
"IsPlayable": false, "IsPlayable": false,
"IsLocked": false, "IsLocked": false,
"HideProgression": false, "HideProgression": false,

View File

@ -1,7 +1,7 @@
{ {
"meta": { "meta": {
"Location": "LOCATION_PARENT_BANGKOK", "Location": "LOCATION_PARENT_BANGKOK",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination", "Icon": "challenge_category_assassination",
"CategoryId": "assassination", "CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [ "Challenges": [
{ {
"Id": "43737b81-d253-44c8-aac8-f64d6711bdaf", "Id": "43737b81-d253-44c8-aac8-f64d6711bdaf",
@ -2523,6 +2524,7 @@
"Icon": "challenge_category_discovery", "Icon": "challenge_category_discovery",
"CategoryId": "discovery", "CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [ "Challenges": [
{ {
"Id": "af07c27a-5d85-47d0-b22b-daee2fed6218", "Id": "af07c27a-5d85-47d0-b22b-daee2fed6218",
@ -3461,6 +3463,7 @@
"Icon": "challenge_category_feats", "Icon": "challenge_category_feats",
"CategoryId": "feats", "CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [ "Challenges": [
{ {
"Id": "02abef2d-ca6b-4748-ac2a-42354b6a6aca", "Id": "02abef2d-ca6b-4748-ac2a-42354b6a6aca",
@ -4242,6 +4245,7 @@
"Icon": "challenge_category_targets", "Icon": "challenge_category_targets",
"CategoryId": "targets", "CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [ "Challenges": [
{ {
"Id": "07736d0b-3c6b-48c6-9ff0-6d951cd07ee7", "Id": "07736d0b-3c6b-48c6-9ff0-6d951cd07ee7",
@ -4380,6 +4384,7 @@
"Icon": "profile", "Icon": "profile",
"CategoryId": "classic", "CategoryId": "classic",
"Description": "", "Description": "",
"OrderIndex": 4,
"Challenges": [ "Challenges": [
{ {
"Id": "2db8ca18-bd44-483d-b7d9-eb726b259583", "Id": "2db8ca18-bd44-483d-b7d9-eb726b259583",
@ -5991,187 +5996,43 @@
] ]
}, },
{ {
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Image": "images/challenges/categories/elusive/tile.jpg", "Image": "images/challenges/categories/packargentum/tile.jpg",
"Icon": "elusive", "Icon": "challenge_category_feats",
"CategoryId": "elusive", "CategoryId": "argentum-pack",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE", "Description": "",
"OrderIndex": 6.5,
"Challenges": [ "Challenges": [
{ {
"Id": "3151f909-0a87-4e71-81f6-02b52405a8f1", "Id": "5b943968-7142-406e-bd6c-9b9018554667",
"Name": "UI_CHALLENGES_ET_BRASSMONKEY_TARGETDOWN_NAME", "Name": "CHALLENGEPACK_ARGENTUM_TIGERDROWNER_NAME",
"ImageName": "images/challenges/elusive_target/et_brassmonkey_M_targetdown.jpg", "ImageName": "images/challenges/Categories/PackArgentum/Argentum_TigerDrowner.jpg",
"Description": "UI_CHALLENGES_ET_BRASSMONKEY_TARGETDOWN_DESC", "Description": "CHALLENGEPACK_ARGENTUM_TIGERDROWNER_DESC",
"Rewards": { "Rewards": {
"MasteryXP": 2000 "MasteryXP": 2000
}, },
"Drops": [], "Drops": [],
"IsPlayable": false, "IsPlayable": true,
"IsLocked": false, "IsLocked": false,
"HideProgression": false, "HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Icon": "elusive", "Icon": "challenge_category_feats",
"LocationId": "LOCATION_BANGKOK", "LocationId": "LOCATION_PARENT_BANGKOK",
"ParentLocationId": "LOCATION_PARENT_BANGKOK", "ParentLocationId": "LOCATION_PARENT_BANGKOK",
"Type": "contract", "Type": "parentlocation",
"DifficultyLevels": [], "DifficultyLevels": [],
"OrderIndex": 10000, "OrderIndex": 100003,
"XpModifier": {}, "XpModifier": {},
"RuntimeType": "Hit", "RuntimeType": "Hit",
"Definition": { "Definition": {
"Context": {},
"Scope": "session", "Scope": "session",
"States": { "States": {
"Start": { "Start": {
"Kill": { "TigerDrownerActive": {
"Condition": { "Transition": "ChallengeActive"
"$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",
"$.#"
]
}
}
} }
}, },
"ChallengeActive": {
"Kill": [ "Kill": [
{ {
"Condition": { "Condition": {
@ -6179,726 +6040,54 @@
{ {
"$eq": [ "$eq": [
"$Value.IsTarget", "$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 true
] ]
}, },
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{ {
"$eq": [ "$eq": [
"$Value.IsWitnessTarget", "$Value.KillMethodStrict",
false "accident_drown"
] ]
},
{
"$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" "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": { "Condition": {
"$and": [ "$and": [
{ {
"$eq": [ "$eq": [
"$Value.IsTarget", "$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 true
] ]
}, },
"Actions": {
"$pushunique": [
"Witnesses",
"$Value.Witness"
]
}
},
{
"Condition": {
"$and": [
{ {
"$eq": [ "$eq": [
"$Value.IsWitnessTarget", "$Value.KillMethodStrict",
false "accident_push"
] ]
},
{
"$not": {
"$eq": [
"$.LastAccidentTime",
"$Timestamp"
]
}
} }
] ]
}, },
"Transition": "Failure" "Transition": "Check_Kill"
} }
],
"SecuritySystemRecorder": [
{
"Actions": {
"$set": [
"RecordingDestroyed",
false
] ]
}, },
"Check_Kill": {
"$timer": {
"Condition": { "Condition": {
"$eq": ["$Value.event", "spotted"] "$after": 2
}
}, },
{ "Transition": "ChallengeActive"
"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]
}, },
"DumpInOcean": {
"Transition": "Success" "Transition": "Success"
} }
} }
} }
}, },
"Description": "UI_CHALLENGES_ET_BLOODYMARY_TARGETDOWN_DESC", "Tags": ["argentum-pack", "story", "live", "medium"]
"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

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

View File

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

View File

@ -1,7 +1,7 @@
{ {
"meta": { "meta": {
"Location": "LOCATION_PARENT_EDGY", "Location": "LOCATION_PARENT_EDGY",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination", "Icon": "challenge_category_assassination",
"CategoryId": "assassination", "CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [ "Challenges": [
{ {
"Id": "2f812cf3-8ff2-4fbe-a74d-1f589cd86083", "Id": "2f812cf3-8ff2-4fbe-a74d-1f589cd86083",
@ -872,6 +873,7 @@
"Icon": "challenge_category_discovery", "Icon": "challenge_category_discovery",
"CategoryId": "discovery", "CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [ "Challenges": [
{ {
"Id": "2113732a-0518-4b50-b490-45422d9fcf29", "Id": "2113732a-0518-4b50-b490-45422d9fcf29",
@ -1539,6 +1541,7 @@
"Icon": "challenge_category_feats", "Icon": "challenge_category_feats",
"CategoryId": "feats", "CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [ "Challenges": [
{ {
"Id": "1ae5e7db-7cec-468c-bb98-0335836e8d8d", "Id": "1ae5e7db-7cec-468c-bb98-0335836e8d8d",
@ -1982,6 +1985,71 @@
"ContractIds": ["12d83cb0-a2d6-4c01-b9d8-675ac635ee61"] "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", "Id": "a015ff25-f9e8-481e-ab60-203eb8ecac40",
"Name": "UI_CONTRACT_GRASSSNAKE_GROUP_TITLE", "Name": "UI_CONTRACT_GRASSSNAKE_GROUP_TITLE",
@ -2070,8 +2138,8 @@
"MasteryXP": 1000 "MasteryXP": 1000
}, },
"Drops": [ "Drops": [
"PROP_DEVICE_REMOTE_EXPLOSIVE_LUST", "FIREARMS_PISTOL_DARTGUN_BLINDING_LUST",
"FIREARMS_PISTOL_DARTGUN_BLINDING_LUST" "PROP_DEVICE_REMOTE_EXPLOSIVE_LUST"
], ],
"IsPlayable": true, "IsPlayable": true,
"IsLocked": false, "IsLocked": false,
@ -2312,8 +2380,8 @@
"MasteryXP": 1000 "MasteryXP": 1000
}, },
"Drops": [ "Drops": [
"PROP_MELEE_LEATHERBELT_ASYLUM", "FIREARMS_PISTOL_DARTGUN_SEDATIVE_ASYLUM",
"FIREARMS_PISTOL_DARTGUN_SEDATIVE_ASYLUM" "PROP_MELEE_LEATHERBELT_ASYLUM"
], ],
"IsPlayable": true, "IsPlayable": true,
"IsLocked": false, "IsLocked": false,
@ -2632,7 +2700,7 @@
{ {
"Id": "54ddadb4-edc7-47eb-a442-9c9536626168", "Id": "54ddadb4-edc7-47eb-a442-9c9536626168",
"Name": "UI_PEACOCK_SHANGRILA", "Name": "UI_PEACOCK_SHANGRILA",
"ImageName": "images/contracts/escalation/contractescalation-shangrila.png", "ImageName": "images/contracts/escalation/contractescalation_shangrila.jpg",
"Description": "UI_CHALLENGES_ESCLATION_COMPLETE_DESC", "Description": "UI_CHALLENGES_ESCLATION_COMPLETE_DESC",
"Rewards": { "Rewards": {
"MasteryXP": 4000 "MasteryXP": 4000
@ -2680,6 +2748,7 @@
"Icon": "challenge_category_targets", "Icon": "challenge_category_targets",
"CategoryId": "targets", "CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [ "Challenges": [
{ {
"Id": "36c368c2-5808-40b5-8805-f62e458ac87c", "Id": "36c368c2-5808-40b5-8805-f62e458ac87c",
@ -3192,6 +3261,7 @@
"Icon": "profile", "Icon": "profile",
"CategoryId": "classic", "CategoryId": "classic",
"Description": "", "Description": "",
"OrderIndex": 4,
"Challenges": [ "Challenges": [
{ {
"Id": "0b63d39b-c53e-4fa4-a2a3-310a973eb819", "Id": "0b63d39b-c53e-4fa4-a2a3-310a973eb819",
@ -4038,585 +4108,109 @@
] ]
}, },
{ {
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Image": "images/challenges/categories/elusive/tile.jpg", "Image": "images/challenges/categories/packargentum/tile.jpg",
"Icon": "elusive", "Icon": "challenge_category_feats",
"CategoryId": "elusive", "CategoryId": "argentum-pack",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE", "Description": "",
"OrderIndex": 6.5,
"Challenges": [ "Challenges": [
{ {
"Id": "c2ae25d2-e4d0-4125-928d-44632ff2f7d1", "Id": "2b9f479e-6789-4f23-9eef-e17f268a2b11",
"Name": "UI_CHALLENGES_ET_RADLER_TARGETDOWN_NAME", "Name": "CHALLENGEPACK_ARGENTUM_FOXEXPLODER_NAME",
"ImageName": "images/challenges/elusive_target/et_radler_targetdown.jpg", "ImageName": "images/challenges/Categories/PackArgentum/Argentum_FoxExploder.jpg",
"Description": "UI_CHALLENGES_ET_RADLER_TARGETDOWN_DESC", "Description": "CHALLENGEPACK_ARGENTUM_FOXEXPLODER_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": { "Rewards": {
"MasteryXP": 4000 "MasteryXP": 4000
}, },
"Drops": [], "Drops": [],
"IsPlayable": false, "IsPlayable": true,
"IsLocked": false, "IsLocked": false,
"HideProgression": false, "HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Icon": "elusive", "Icon": "challenge_category_feats",
"LocationId": "LOCATION_EDGY_FOX", "LocationId": "LOCATION_PARENT_EDGY",
"ParentLocationId": "LOCATION_PARENT_EDGY", "ParentLocationId": "LOCATION_PARENT_EDGY",
"Type": "contract", "Type": "contract",
"DifficultyLevels": [], "DifficultyLevels": [],
"OrderIndex": 10000, "OrderIndex": 100004,
"XpModifier": {}, "XpModifier": {},
"RuntimeType": "Hit", "RuntimeType": "Hit",
"Definition": { "Definition": {
"Constants": {
"Goal": 5
},
"ContextListeners": {
"Count": {
"type": "challengecounter",
"count": "$.Count",
"total": "$.Goal"
}
},
"Context": { "Context": {
"Witnesses": [], "Count": 0,
"KilledTargets": [], "Targets": [
"RecordingDestroyed": true, "8b29da09-461f-44d7-9042-d4fde829b9f2",
"LastAccidentTime": 0 "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", "Scope": "session",
"States": { "States": {
"Start": { "Start": {
"ContractEnd": { "FoxExploderActive": {
"Condition": { "Transition": "ChallengeActive"
"$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",
"$.#"
]
}
}
} }
}, },
"ChallengeActive": {
"Kill": [ "Kill": [
{ {
"Condition": { "Condition": {
"$and": [ "$and": [
{ {
"$eq": [ "$inarray": {
"$Value.IsTarget", "in": "$.Targets",
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": [ "$eq": [
"$.#", "$.#",
"$.##" "$Value.RepositoryId"
] ]
} }
} }
} },
} {
"$eq": [
"$Value.KillClass",
"explosion"
]
} }
] ]
}, },
"Actions": {
"$inc": "Count"
}
},
{
"Condition": {
"$eq": ["$.Count", "$.Goal"]
},
"Transition": "Success" "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"], "Tags": ["argentum-pack", "story", "live", "hard"],
"InclusionData": { "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": { "meta": {
"Location": "LOCATION_PARENT_TRAPPED", "Location": "LOCATION_PARENT_TRAPPED",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination", "Icon": "challenge_category_assassination",
"CategoryId": "assassination", "CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [ "Challenges": [
{ {
"Id": "3d0ff5c3-6e51-4827-951f-d45d8d21b3ea", "Id": "3d0ff5c3-6e51-4827-951f-d45d8d21b3ea",
@ -101,6 +102,7 @@
"Icon": "challenge_category_discovery", "Icon": "challenge_category_discovery",
"CategoryId": "discovery", "CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [ "Challenges": [
{ {
"Id": "18636f91-e1a9-4091-935c-adbf6cc3de55", "Id": "18636f91-e1a9-4091-935c-adbf6cc3de55",
@ -587,6 +589,7 @@
"Icon": "challenge_category_feats", "Icon": "challenge_category_feats",
"CategoryId": "feats", "CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [ "Challenges": [
{ {
"Id": "22070cef-2ecb-4a65-ab16-cb4bfd63785e", "Id": "22070cef-2ecb-4a65-ab16-cb4bfd63785e",
@ -676,8 +679,8 @@
"MasteryXP": 1000 "MasteryXP": 1000
}, },
"Drops": [ "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, "IsPlayable": true,
"IsLocked": false, "IsLocked": false,
@ -1146,6 +1149,7 @@
"Icon": "challenge_category_targets", "Icon": "challenge_category_targets",
"CategoryId": "targets", "CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [ "Challenges": [
{ {
"Id": "3210fa62-67a5-4e1f-ba50-02c451d47875", "Id": "3210fa62-67a5-4e1f-ba50-02c451d47875",
@ -1200,6 +1204,7 @@
"Icon": "profile", "Icon": "profile",
"CategoryId": "classic", "CategoryId": "classic",
"Description": "", "Description": "",
"OrderIndex": 4,
"Challenges": [ "Challenges": [
{ {
"Id": "92643706-6f98-4b1c-9700-ee4819aaadb4", "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": { "meta": {
"Location": "LOCATION_PARENT_WET", "Location": "LOCATION_PARENT_WET",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination", "Icon": "challenge_category_assassination",
"CategoryId": "assassination", "CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [ "Challenges": [
{ {
"Id": "00fcea23-9659-447a-a717-9502810e2930", "Id": "00fcea23-9659-447a-a717-9502810e2930",
@ -880,6 +881,7 @@
"Icon": "challenge_category_discovery", "Icon": "challenge_category_discovery",
"CategoryId": "discovery", "CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [ "Challenges": [
{ {
"Id": "04ef301f-a62d-47f6-8cb8-3b8697e0c06c", "Id": "04ef301f-a62d-47f6-8cb8-3b8697e0c06c",
@ -1709,6 +1711,7 @@
"Icon": "challenge_category_feats", "Icon": "challenge_category_feats",
"CategoryId": "feats", "CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [ "Challenges": [
{ {
"Id": "1a68c357-02a0-40a2-99e7-73d76ec5095d", "Id": "1a68c357-02a0-40a2-99e7-73d76ec5095d",
@ -3379,6 +3382,7 @@
"Icon": "challenge_category_targets", "Icon": "challenge_category_targets",
"CategoryId": "targets", "CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [ "Challenges": [
{ {
"Id": "09675ea9-4cf3-4b80-a722-329b8e48e687", "Id": "09675ea9-4cf3-4b80-a722-329b8e48e687",
@ -3472,6 +3476,7 @@
"Icon": "profile", "Icon": "profile",
"CategoryId": "classic", "CategoryId": "classic",
"Description": "", "Description": "",
"OrderIndex": 4,
"Challenges": [ "Challenges": [
{ {
"Id": "0af3980d-6bcb-42f6-ac4c-d0241606500a", "Id": "0af3980d-6bcb-42f6-ac4c-d0241606500a",
@ -4318,31 +4323,32 @@
] ]
}, },
{ {
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Image": "images/challenges/categories/elusive/tile.jpg", "Image": "images/challenges/categories/packargentum/tile.jpg",
"Icon": "elusive", "Icon": "challenge_category_feats",
"CategoryId": "elusive", "CategoryId": "argentum-pack",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE", "Description": "",
"OrderIndex": 6.5,
"Challenges": [ "Challenges": [
{ {
"Id": "cfd77118-ee34-49b0-b2e6-0fcbb69948f6", "Id": "f8ba2bd3-dcd7-453f-aa64-c0ed6992e24b",
"Name": "UI_CHALLENGES_ET_REDSNAPPER_TARGETDOWN_NAME", "Name": "CHALLENGEPACK_ARGENTUM_RATSNIPER_NAME",
"ImageName": "images/challenges/elusive_target/et_redsnapper_targetdown.jpg", "ImageName": "images/challenges/Categories/PackArgentum/Argentum_RatSniper.jpg",
"Description": "UI_CHALLENGES_ET_REDSNAPPER_TARGETDOWN_DESC", "Description": "CHALLENGEPACK_ARGENTUM_RATSNIPER_DESC",
"Rewards": { "Rewards": {
"MasteryXP": 2000 "MasteryXP": 1000
}, },
"Drops": [], "Drops": [],
"IsPlayable": false, "IsPlayable": true,
"IsLocked": false, "IsLocked": false,
"HideProgression": false, "HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_ARGENTUM",
"Icon": "elusive", "Icon": "challenge_category_feats",
"LocationId": "LOCATION_WET_RAT", "LocationId": "LOCATION_WET_RAT",
"ParentLocationId": "LOCATION_PARENT_WET", "ParentLocationId": "LOCATION_PARENT_WET",
"Type": "contract", "Type": "location",
"DifficultyLevels": [], "DifficultyLevels": [],
"OrderIndex": 10000, "OrderIndex": 100001,
"XpModifier": {}, "XpModifier": {},
"RuntimeType": "Hit", "RuntimeType": "Hit",
"Definition": { "Definition": {
@ -4350,248 +4356,125 @@
"Scope": "session", "Scope": "session",
"States": { "States": {
"Start": { "Start": {
"EnterARGENTUM_Volume": {
"Transition": "InPosition"
}
},
"InPosition": {
"ExitARGENTUM_Volume": {
"Transition": "Start"
},
"Kill": { "Kill": {
"Condition": { "Condition": {
"$and": [
{
"$eq": ["$Value.IsTarget", true] "$eq": ["$Value.IsTarget", true]
}, },
{
"$eq": [
"$Value.KillItemCategory",
"sniperrifle"
]
},
{
"$eq": [
"$Value.IsHeadshot",
true
]
}
]
},
"Transition": "Success" "Transition": "Success"
} }
} }
} }
}, },
"Tags": ["story", "medium", "elusive"], "Tags": ["argentum-pack", "story", "live", "easy"]
"InclusionData": {
"ContractIds": ["6fad7901-279f-45df-ab8d-087a3cb06dcc"]
} }
]
}, },
{ {
"Id": "c823eaa7-aaec-42e5-a160-7b7d7b3c719e", "Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Name": "UI_CHALLENGES_ET_REDSNAPPER_SILENT_ASSASSIN_NAME", "Image": "images/challenges/categories/packcheesecake/tile.jpg",
"ImageName": "images/challenges/elusive_target/et_redsnapper_silentassassin.jpg", "Icon": "challenge_category_feats",
"Description": "UI_CHALLENGES_ET_REDSNAPPER_SILENT_ASSASSIN_DESC", "CategoryId": "cheesecake-pack",
"Description": "",
"OrderIndex": 6.1,
"Challenges": [
{
"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": { "Rewards": {
"MasteryXP": 4000 "MasteryXP": 2000
}, },
"Drops": [], "Drops": [],
"IsPlayable": false, "IsPlayable": true,
"IsLocked": false, "IsLocked": false,
"HideProgression": false, "HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Icon": "elusive", "Icon": "challenge_category_feats",
"LocationId": "LOCATION_WET_RAT", "LocationId": "LOCATION_PARENT_WET",
"ParentLocationId": "LOCATION_PARENT_WET", "ParentLocationId": "LOCATION_PARENT_WET",
"Type": "contract", "Type": "parentlocation",
"DifficultyLevels": [], "DifficultyLevels": [],
"OrderIndex": 10000, "OrderIndex": 100003,
"XpModifier": {}, "XpModifier": {},
"RuntimeType": "Hit", "RuntimeType": "Hit",
"Definition": { "Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session", "Scope": "session",
"Context": { "PoisonedTargets": [] },
"States": { "States": {
"Start": { "Start": {
"ContractEnd": { "Kill": {
"Condition": { "Condition": {
"$and": [ "$and": [
{
"$eq": ["$Value.IsTarget", true]
},
{ {
"$eq": [ "$eq": [
true, "$Value.KillClass",
"$.RecordingDestroyed" "poison"
] ]
}, },
{ {
"$all": { "$eq": [
"in": "$.Witnesses", "$Value.OutfitRepositoryId",
"?": { "c5f6dd2a-3600-40be-9a82-bbf5d360c379"
"$any": { ]
"in": "$.KilledTargets", }
]
},
"Actions": {
"$pushunique": [
"PoisonedTargets",
"$Value.RepositoryId"
]
},
"Transition": "CheckDumpInOcean"
}
},
"CheckDumpInOcean": {
"DumpInOcean": {
"Condition": {
"$inarray": {
"in": "$.PoisonedTargets",
"?": { "?": {
"$eq": [ "$eq": [
"$.#", "$.#",
"$.##" "$Value.RepositoryId"
] ]
} }
} }
}
}
}
]
}, },
"Transition": "Success" "Transition": "Success"
},
"AccidentBodyFound": {
"$set": ["LastAccidentTime", "$Timestamp"]
},
"Witnesses": {
"Condition": {
"$any": {
"in": "$Value",
"?": {
"$pushunique": [
"Witnesses",
"$.#"
]
} }
} }
} }
}, },
"Spotted": { "Tags": ["cheesecake-pack", "story", "live", "medium"]
"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

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

View File

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

View File

@ -1,7 +1,7 @@
{ {
"meta": { "meta": {
"Location": "LOCATION_PARENT_ANCESTRAL", "Location": "LOCATION_PARENT_ANCESTRAL",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination", "Icon": "challenge_category_assassination",
"CategoryId": "assassination", "CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [ "Challenges": [
{ {
"Id": "150f0c8a-34a7-4bdf-8d92-97915c8ac64d", "Id": "150f0c8a-34a7-4bdf-8d92-97915c8ac64d",
@ -1171,6 +1172,7 @@
"Icon": "challenge_category_discovery", "Icon": "challenge_category_discovery",
"CategoryId": "discovery", "CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [ "Challenges": [
{ {
"Id": "43186811-5dab-4378-be8d-3e742eac6159", "Id": "43186811-5dab-4378-be8d-3e742eac6159",
@ -2079,6 +2081,7 @@
"Icon": "challenge_category_feats", "Icon": "challenge_category_feats",
"CategoryId": "feats", "CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [ "Challenges": [
{ {
"Id": "12e2d93c-1433-429f-a802-32e611a7ba64", "Id": "12e2d93c-1433-429f-a802-32e611a7ba64",
@ -4587,7 +4590,7 @@
{ {
"Id": "2934a0ea-8e0a-4aee-bc38-f3a015274746", "Id": "2934a0ea-8e0a-4aee-bc38-f3a015274746",
"Name": "UI_PEACOCK_ROSEBUSH", "Name": "UI_PEACOCK_ROSEBUSH",
"ImageName": "images/contracts/escalation/contractescalation_rosebush.png", "ImageName": "images/contracts/escalation/contractescalation_rosebush.jpg",
"Description": "UI_CHALLENGES_ESCLATION_COMPLETE_DESC", "Description": "UI_CHALLENGES_ESCLATION_COMPLETE_DESC",
"Rewards": { "Rewards": {
"MasteryXP": 4000 "MasteryXP": 4000
@ -4635,6 +4638,7 @@
"Icon": "challenge_category_targets", "Icon": "challenge_category_targets",
"CategoryId": "targets", "CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [ "Challenges": [
{ {
"Id": "1524234b-6f85-426d-92d6-a2dc6d42cfbd", "Id": "1524234b-6f85-426d-92d6-a2dc6d42cfbd",
@ -4686,6 +4690,7 @@
"Icon": "profile", "Icon": "profile",
"CategoryId": "classic", "CategoryId": "classic",
"Description": "", "Description": "",
"OrderIndex": 4,
"Challenges": [ "Challenges": [
{ {
"Id": "647278f4-e20f-4c42-8a8b-5d2cf30591c0", "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": { "meta": {
"Location": "LOCATION_PARENT_GOLDEN", "Location": "LOCATION_PARENT_GOLDEN",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "challenge_category_assassination", "Icon": "challenge_category_assassination",
"CategoryId": "assassination", "CategoryId": "assassination",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_SIGNATUREKILL",
"OrderIndex": 0,
"Challenges": [ "Challenges": [
{ {
"Id": "02eed2d0-872d-4c42-a813-f7083686a8c1", "Id": "02eed2d0-872d-4c42-a813-f7083686a8c1",
@ -953,6 +954,7 @@
"Icon": "challenge_category_discovery", "Icon": "challenge_category_discovery",
"CategoryId": "discovery", "CategoryId": "discovery",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_EXPLORATION",
"OrderIndex": 1,
"Challenges": [ "Challenges": [
{ {
"Id": "0d160e0a-99ad-4ec3-9865-30d8da8076df", "Id": "0d160e0a-99ad-4ec3-9865-30d8da8076df",
@ -2062,6 +2064,7 @@
"Icon": "challenge_category_feats", "Icon": "challenge_category_feats",
"CategoryId": "feats", "CategoryId": "feats",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_COMMUNITY",
"OrderIndex": 2,
"Challenges": [ "Challenges": [
{ {
"Id": "0c668dd3-bd73-4246-adf0-87b4c8e38bda", "Id": "0c668dd3-bd73-4246-adf0-87b4c8e38bda",
@ -3265,6 +3268,7 @@
"Icon": "challenge_category_targets", "Icon": "challenge_category_targets",
"CategoryId": "targets", "CategoryId": "targets",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL", "Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_PROFESSIONAL",
"OrderIndex": 3,
"Challenges": [ "Challenges": [
{ {
"Id": "16b2d5e1-43a2-4cea-adf1-1d6af2fe8df6", "Id": "16b2d5e1-43a2-4cea-adf1-1d6af2fe8df6",
@ -3358,6 +3362,7 @@
"Icon": "profile", "Icon": "profile",
"CategoryId": "classic", "CategoryId": "classic",
"Description": "", "Description": "",
"OrderIndex": 4,
"Challenges": [ "Challenges": [
{ {
"Id": "2694b2ff-224c-4bde-858d-1671a1dbc580", "Id": "2694b2ff-224c-4bde-858d-1671a1dbc580",
@ -4204,280 +4209,112 @@
] ]
}, },
{ {
"Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "Name": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Image": "images/challenges/categories/elusive/tile.jpg", "Image": "images/challenges/categories/packcheesecake/tile.jpg",
"Icon": "elusive", "Icon": "challenge_category_feats",
"CategoryId": "elusive", "CategoryId": "cheesecake-pack",
"Description": "UI_MENU_PAGE_CHALLENGE_CATEGORY_DESCRIPTION_ELUSIVE", "Description": "",
"OrderIndex": 6.1,
"Challenges": [ "Challenges": [
{ {
"Id": "3beb74aa-45f7-4b70-bbf0-75045f6c525a", "Id": "c098a7cf-8c49-02ce-475e-20beaed99712",
"Name": "UI_CHALLENGES_ET_GIBSON_TARGETDOWN_TITLE", "Name": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_DUBAIPANPACIFY_NAME",
"ImageName": "images/challenges/elusive_target/et_gibson_targetdown.jpg", "ImageName": "images/challenges/categories/packcheesecake/cheesecake_dubaipanpacify.jpg",
"Description": "UI_CHALLENGES_ET_GIBSON_TARGETDOWN_DESC", "Description": "UI_PEACOCK_CHALLENGEPACK_CHEESECAKE_DUBAIPANPACIFY_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": { "Rewards": {
"MasteryXP": 4000 "MasteryXP": 4000
}, },
"Drops": [], "Drops": [],
"IsPlayable": false, "IsPlayable": true,
"IsLocked": false, "IsLocked": false,
"HideProgression": false, "HideProgression": false,
"CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE", "CategoryName": "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_PACK_CHEESECAKE",
"Icon": "elusive", "Icon": "challenge_category_feats",
"LocationId": "LOCATION_GOLDEN_GECKO", "LocationId": "LOCATION_PARENT_GOLDEN",
"ParentLocationId": "LOCATION_PARENT_GOLDEN", "ParentLocationId": "LOCATION_PARENT_GOLDEN",
"Type": "contract", "Type": "parentlocation",
"DifficultyLevels": [], "DifficultyLevels": [],
"OrderIndex": 10000, "OrderIndex": 100005,
"XpModifier": {}, "XpModifier": {},
"RuntimeType": "Hit", "RuntimeType": "Hit",
"Definition": { "Definition": {
"Context": {
"Witnesses": [],
"KilledTargets": [],
"RecordingDestroyed": true,
"LastAccidentTime": 0
},
"Scope": "session", "Scope": "session",
"Constants": {
"Goal": 2
},
"Context": {
"PacifiedTargets": 0
},
"States": { "States": {
"Start": { "Start": {
"ContractEnd": { "setpieces": {
"Condition": {
"$eq": [
"$Value.RepositoryId",
"fbfa76d6-9f9b-40dd-869a-2b3fc2361ce5"
]
},
"Transition": "MeetingRoomLocked"
}
},
"MeetingRoomLocked": {
"Pacify": {
"Condition": { "Condition": {
"$and": [ "$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": [ "$or": [
{ {
"$eq": [ "$eq": [
"$Value.event", "$Value.RepositoryId",
"erased" "bd0689d6-07b4-4757-b8ee-cac19f1c9e16"
] ]
}, },
{ {
"$eq": [ "$eq": [
"$Value.event", "$Value.RepositoryId",
"destroyed" "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", "hard", "elusive"], "Tags": ["cheesecake-pack", "story", "live", "medium"]
"InclusionData": {
"ContractIds": ["b2c0251e-1803-4e12-b860-b9fa6ce5c004"]
}
} }
] ]
} }

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{ {
"meta": { "meta": {
"Location": "GLOBAL_FEATURED_CHALLENGES", "Location": "GLOBAL_FEATURED_CHALLENGES",
"GameVersion": "h2" "GameVersions": ["h2"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "featured", "Icon": "featured",
"CategoryId": "featured", "CategoryId": "featured",
"Description": "", "Description": "",
"OrderIndex": 11,
"Challenges": [ "Challenges": [
{ {
"Id": "31e8e58f-86c1-4f1b-9341-d312cd9f28f8", "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": { "meta": {
"Location": "GLOBAL_CLASSIC_CHALLENGES", "Location": "GLOBAL_CLASSIC_CHALLENGES",
"GameVersion": "h3" "GameVersions": ["h3"]
}, },
"groups": [ "groups": [
{ {
@ -10,6 +10,7 @@
"Icon": "profile", "Icon": "profile",
"CategoryId": "classic", "CategoryId": "classic",
"Description": "", "Description": "",
"OrderIndex": 4,
"Challenges": [ "Challenges": [
{ {
"Id": "1c7d6618-26c2-4b9a-86f0-c3afa127fc8d", "Id": "1c7d6618-26c2-4b9a-86f0-c3afa127fc8d",

View File

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

View File

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

View File

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

View File

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

View File

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

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