mirror of
https://github.com/thepeacockproject/Peacock
synced 2025-02-23 03:35:25 +01:00
feat: Add API to simplify creation of global challenges (#459)
* feat: Add API to simplify creation of global challenges * feat: Finalize global challenge API style: Renamed `globalGroups` into `globalMergeGroups` to clarify function. docs: Provided docs on new API refactor: Moved classic, elusive and H1/H2 escalations into globalMergeGroups feat: Added `challengeService.registerChallengeList` for easier challenge group registration feat: Added `challengeService.hasGroup` method to check if group was already registered.
This commit is contained in:
parent
4dbc0b39de
commit
a306cc5643
@ -12,6 +12,6 @@ Thanks to Moo for the name suggestion.
|
|||||||
|
|
||||||
Challenge data reside in files in `contractdata` ending in `_CHALLENGES.json`. When building, the `packResources` function in `buildTasks.mjs` will take all such files and pack them into files in the `resources/challenges` folder.
|
Challenge data reside in files in `contractdata` ending in `_CHALLENGES.json`. When building, the `packResources` function in `buildTasks.mjs` will take all such files and pack them into files in the `resources/challenges` folder.
|
||||||
|
|
||||||
Then, when the server starts, challenge data is loaded from the files in the `resources/challenges` folder into the memory, via the `_loadResources` function in `controller.ts`. It calls the `_handleChallengeResources` function that in turn calls `challengeService.registerGroup` and `challengeService.registerChallenge` to register the challenge group and the respective challenges.
|
Then, when the server starts, challenge data is loaded from the files in the `resources/challenges` folder into the memory, via the `_loadResources` function in `controller.ts`. It calls the `_handleChallengeResources` function that in turn calls `challengeService.registerGroup` and `challengeService.registerChallengeList` to register the challenge group and the respective challenges.
|
||||||
|
|
||||||
These two registration functions initialize two data structures, `groups: Map<string, Map<string, SavedChallengeGroup>>`, which maps `parentLocationId`s to `Map`s of `groupId`s to `SavedChallengeGroup` objects for this group of this parent location, and `groupContents: Map<string, Map<string, Set<string>>>`, which maps `parentLocationId`s to `Map`s of `groupId`s to `Set`s of challenge Ids in this group of this parent location. All subsequent operations on challenges write to or read from these two `Map`s.
|
These two registration functions initialize two data structures, `groups: Map<string, Map<string, SavedChallengeGroup>>`, which maps `parentLocationId`s to `Map`s of `groupId`s to `SavedChallengeGroup` objects for this group of this parent location, and `groupContents: Map<string, Map<string, Set<string>>>`, which maps `parentLocationId`s to `Map`s of `groupId`s to `Set`s of challenge Ids in this group of this parent location. All subsequent operations on challenges write to or read from these two `Map`s.
|
||||||
|
@ -87,6 +87,38 @@ export type ChallengePack = {
|
|||||||
Icon: string
|
Icon: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GlobalChallengeGroup = {
|
||||||
|
/**
|
||||||
|
* ID of a challenge group that will have location and global challenges merged.
|
||||||
|
*
|
||||||
|
* Not necessarily should match the group it's being merged with,
|
||||||
|
* but it's highly advised to match them.
|
||||||
|
*/
|
||||||
|
groupId: string
|
||||||
|
/**
|
||||||
|
* The global challenge group location ID from where the global challenges are merged.
|
||||||
|
*/
|
||||||
|
location: string
|
||||||
|
/**
|
||||||
|
* Which game versions are supported by this global challenge group.
|
||||||
|
*/
|
||||||
|
gameVersions: GameVersion[]
|
||||||
|
/**
|
||||||
|
* If set, this global challenge group will be visible in all locations,
|
||||||
|
* regardless if this group has challenges in that location or not.
|
||||||
|
*
|
||||||
|
* This option is useful when challenge group has to be active for all locations,
|
||||||
|
* but doesn't have any location-specific challenges.
|
||||||
|
* However it's advised to create location-specific challenges instead,
|
||||||
|
* as this option will apply the challenge group to all locations and missions,
|
||||||
|
* including Freelancer and Sniper modes.
|
||||||
|
*
|
||||||
|
* This can be alleviated by providing valid InclusionData filter to challenges,
|
||||||
|
* in order to exclude them from locations you do not wish them to be in.
|
||||||
|
*/
|
||||||
|
allLocations?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base class providing challenge registration support.
|
* A base class providing challenge registration support.
|
||||||
*/
|
*/
|
||||||
@ -186,16 +218,56 @@ export abstract class ChallengeRegistry {
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of user-made challenge groups that span multiple locations
|
||||||
|
* and should merge their global type challenges merged with location challenges.
|
||||||
|
*
|
||||||
|
* @see GlobalChallengeGroup fields for more information.
|
||||||
|
*/
|
||||||
|
public globalMergeGroups: Map<string, GlobalChallengeGroup> = new Map([
|
||||||
|
[
|
||||||
|
"classic",
|
||||||
|
{
|
||||||
|
gameVersions: ["h1", "h2", "h3", "scpc"],
|
||||||
|
groupId: "classic",
|
||||||
|
location: "GLOBAL_CLASSIC_CHALLENGES",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"elusive",
|
||||||
|
{
|
||||||
|
gameVersions: ["h1", "h2", "h3", "scpc"],
|
||||||
|
groupId: "elusive",
|
||||||
|
location: "GLOBAL_ELUSIVES_CHALLENGES",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// H2 & H1 have the escalation challenges in "feats"
|
||||||
|
"feats",
|
||||||
|
{
|
||||||
|
gameVersions: ["h1", "h2", "scpc"],
|
||||||
|
groupId: "feats",
|
||||||
|
location: "GLOBAL_ESCALATION_CHALLENGES",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
registerChallenge(
|
registerChallenge(
|
||||||
challenge: RegistryChallenge,
|
challenge: RegistryChallenge,
|
||||||
groupId: string,
|
groupId: string,
|
||||||
location: string,
|
location: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
|
): void {
|
||||||
|
this.registerChallengeList([challenge], groupId, location, gameVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerChallengeList(
|
||||||
|
challenges: RegistryChallenge[],
|
||||||
|
groupId: string,
|
||||||
|
location: string,
|
||||||
|
gameVersion: GameVersion,
|
||||||
): void {
|
): void {
|
||||||
const gameChallenges = this.groupContents[gameVersion]
|
const gameChallenges = this.groupContents[gameVersion]
|
||||||
challenge.inGroup = groupId
|
|
||||||
challenge.inLocation = location
|
|
||||||
this.challenges[gameVersion].set(challenge.Id, challenge)
|
|
||||||
|
|
||||||
if (!gameChallenges.has(location)) {
|
if (!gameChallenges.has(location)) {
|
||||||
gameChallenges.set(location, new Map())
|
gameChallenges.set(location, new Map())
|
||||||
@ -208,9 +280,14 @@ export abstract class ChallengeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const set = locationMap.get(groupId)!
|
const set = locationMap.get(groupId)!
|
||||||
set.add(challenge.Id)
|
|
||||||
|
|
||||||
this.checkHeuristics(challenge, gameVersion)
|
for (const challenge of challenges) {
|
||||||
|
challenge.inGroup = groupId
|
||||||
|
challenge.inLocation = location
|
||||||
|
this.challenges[gameVersion].set(challenge.Id, challenge)
|
||||||
|
set.add(challenge.Id)
|
||||||
|
this.checkHeuristics(challenge, gameVersion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerGroup(
|
registerGroup(
|
||||||
@ -227,6 +304,23 @@ export abstract class ChallengeRegistry {
|
|||||||
gameGroups.get(location)?.set(group.CategoryId, group)
|
gameGroups.get(location)?.set(group.CategoryId, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if `groupId` is already registered for given `location` and `gameVersion`.
|
||||||
|
*
|
||||||
|
* @param groupId The group ID to check
|
||||||
|
* @param location The location group belongs to
|
||||||
|
* @param gameVersion The game version group works in
|
||||||
|
*/
|
||||||
|
hasGroup(
|
||||||
|
groupId: string,
|
||||||
|
location: string,
|
||||||
|
gameVersion: GameVersion,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
this.groups[gameVersion]?.get(location)?.get(groupId) !== undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
getChallengeById(
|
getChallengeById(
|
||||||
challengeId: string,
|
challengeId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
@ -290,18 +384,6 @@ export abstract class ChallengeRegistry {
|
|||||||
|
|
||||||
const mainGroup = gameGroups.get(location)?.get(groupId)
|
const mainGroup = gameGroups.get(location)?.get(groupId)
|
||||||
|
|
||||||
if (groupId === "feats" && gameVersion !== "h3") {
|
|
||||||
if (!mainGroup) {
|
|
||||||
// emergency bailout - shouldn't happen in practice
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeSavedChallengeGroups(
|
|
||||||
mainGroup,
|
|
||||||
gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupId?.includes("featured")) {
|
if (groupId?.includes("featured")) {
|
||||||
return gameGroups.get("GLOBAL_FEATURED_CHALLENGES")?.get(groupId)
|
return gameGroups.get("GLOBAL_FEATURED_CHALLENGES")?.get(groupId)
|
||||||
}
|
}
|
||||||
@ -314,32 +396,24 @@ export abstract class ChallengeRegistry {
|
|||||||
return gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId)
|
return gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Included by default. Filtered later.
|
// Global merge groups are included by default. Filtered later.
|
||||||
if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") {
|
|
||||||
if (!mainGroup) {
|
|
||||||
// emergency bailout - shouldn't happen in practice
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeSavedChallengeGroups(
|
const globalGroup = this.globalMergeGroups.get(groupId)
|
||||||
mainGroup,
|
|
||||||
gameGroups.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
groupId === "elusive" &&
|
globalGroup &&
|
||||||
location !== "GLOBAL_ELUSIVES_CHALLENGES"
|
location !== globalGroup.location &&
|
||||||
|
globalGroup.gameVersions.includes(gameVersion)
|
||||||
) {
|
) {
|
||||||
|
const globalGroupChallenges = gameGroups
|
||||||
|
.get(globalGroup.location)
|
||||||
|
?.get(globalGroup.groupId)
|
||||||
|
|
||||||
if (!mainGroup) {
|
if (!mainGroup) {
|
||||||
// emergency bailout - shouldn't happen in practice
|
return globalGroupChallenges
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeSavedChallengeGroups(
|
return mergeSavedChallengeGroups(mainGroup, globalGroupChallenges)
|
||||||
mainGroup,
|
|
||||||
gameGroups.get("GLOBAL_ELUSIVES_CHALLENGES")?.get(groupId),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mainGroup
|
return mainGroup
|
||||||
@ -363,15 +437,6 @@ export abstract class ChallengeRegistry {
|
|||||||
): Set<string> | undefined {
|
): Set<string> | undefined {
|
||||||
const gameChalGC = this.groupContents[gameVersion]
|
const gameChalGC = this.groupContents[gameVersion]
|
||||||
|
|
||||||
if (groupId === "feats" && gameVersion !== "h3") {
|
|
||||||
return new Set([
|
|
||||||
...(gameChalGC.get(location)?.get(groupId) ?? []),
|
|
||||||
...(gameChalGC
|
|
||||||
.get("GLOBAL_ESCALATION_CHALLENGES")
|
|
||||||
?.get(groupId) ?? []),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupId?.includes("featured")) {
|
if (groupId?.includes("featured")) {
|
||||||
return gameChalGC.get("GLOBAL_FEATURED_CHALLENGES")?.get(groupId)
|
return gameChalGC.get("GLOBAL_FEATURED_CHALLENGES")?.get(groupId)
|
||||||
}
|
}
|
||||||
@ -384,24 +449,20 @@ export abstract class ChallengeRegistry {
|
|||||||
return gameChalGC.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId)
|
return gameChalGC.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Included by default. Filtered later.
|
// Global merge groups are included by default. Filtered later.
|
||||||
if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") {
|
|
||||||
return new Set([
|
const globalGroup = this.globalMergeGroups.get(groupId)
|
||||||
...(gameChalGC.get(location)?.get(groupId) ?? []),
|
|
||||||
...(gameChalGC.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId) ??
|
|
||||||
[]),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
groupId === "elusive" &&
|
globalGroup &&
|
||||||
location !== "GLOBAL_ELUSIVES_CHALLENGES"
|
globalGroup.location !== location &&
|
||||||
|
globalGroup.gameVersions.includes(gameVersion)
|
||||||
) {
|
) {
|
||||||
return new Set([
|
return new Set([
|
||||||
...(gameChalGC.get(location)?.get(groupId) ?? []),
|
...(gameChalGC.get(location)?.get(groupId) ?? []),
|
||||||
...(gameChalGC
|
...(gameChalGC
|
||||||
.get("GLOBAL_ELUSIVES_CHALLENGES")
|
.get(globalGroup.location)
|
||||||
?.get(groupId) ?? []),
|
?.get(globalGroup.groupId) ?? []),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,6 +766,26 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle merge gropus with `allLocations` flag.
|
||||||
|
// Should apply only if location has no challenges in that group,
|
||||||
|
// as those are merged in `getGroupContentByIdLoc` already.
|
||||||
|
for (const globalGroup of this.globalMergeGroups.values()) {
|
||||||
|
if (
|
||||||
|
globalGroup.allLocations &&
|
||||||
|
globalGroup.gameVersions.includes(gameVersion) &&
|
||||||
|
this.groups[gameVersion]
|
||||||
|
.get(location)
|
||||||
|
?.get(globalGroup.groupId) === undefined
|
||||||
|
) {
|
||||||
|
this.getGroupedChallengesByLoc(
|
||||||
|
filter,
|
||||||
|
globalGroup.location,
|
||||||
|
challenges,
|
||||||
|
gameVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove empty groups
|
// remove empty groups
|
||||||
|
@ -959,14 +959,12 @@ export class Controller {
|
|||||||
version,
|
version,
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const challenge of group.Challenges) {
|
this.challengeService.registerChallengeList(
|
||||||
this.challengeService.registerChallenge(
|
group.Challenges,
|
||||||
challenge,
|
group.CategoryId,
|
||||||
group.CategoryId,
|
data.meta.Location,
|
||||||
data.meta.Location,
|
version,
|
||||||
version,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user