1
mirror of https://github.com/thepeacockproject/Peacock synced 2025-02-16 16:34:28 +01:00

Add mastery data for sniper missions (#148)

This commit is contained in:
moonysolari 2023-03-24 09:19:01 -04:00 committed by GitHub
parent a4114c0926
commit 6d3ef2f486
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 9284 additions and 676 deletions

1
.gitignore vendored
View File

@ -39,6 +39,7 @@ resources/contracts.br
resources/challenges
resources/mastery
plugins
/*.plugin.*
/*Plugin.*
/resources/dynamic_resources_h1/REPO/006D6B8D26B0F442.REPO

View File

@ -58,6 +58,7 @@ import {
levelForXp,
xpRequiredForEvergreenLevel,
xpRequiredForLevel,
isSniperLocation,
} from "../utils"
import {
ChallengeFilterOptions,
@ -411,12 +412,9 @@ export class ChallengeService extends ChallengeRegistry {
const location = locations.children[child]
assert.ok(location)
let contracts =
child === "LOCATION_AUSTRIA" ||
child === "LOCATION_SALTY_SEAGULL" ||
child === "LOCATION_CAGED_FALCON"
? this.controller.missionsInLocations.sniper[child]
: this.controller.missionsInLocations[child]
let contracts = isSniperLocation(child)
? this.controller.missionsInLocations.sniper[child]
: this.controller.missionsInLocations[child]
if (!contracts) {
contracts = []
}

View File

@ -30,8 +30,9 @@ import { CompletionData, GameVersion, Unlockable } from "../types/types"
import {
clampValue,
DEFAULT_MASTERY_MAXLEVEL,
xpRequiredForEvergreenLevel,
xpRequiredForLevel,
XP_PER_LEVEL,
xpRequiredForSniperLevel,
} from "../utils"
export class MasteryService {
@ -80,11 +81,67 @@ export class MasteryService {
}
}
getCompletionData(
/**
* Get generic completion data stored in a user's profile. Called by both `getLocationCompletion` and `getFirearmCompletion`.
* @param userId The id of the user.
* @param gameVersion The game version.
* @param completionId An Id used to look up completion data in the user's profile. Can be `parentLocationId` or `progressionKey`.
* @param maxLevel The max level for this progression.
*/
private getCompletionData(
userId: string,
gameVersion: GameVersion,
completionId: string,
maxLevel: number,
levelToXpRequired: (level: number) => number,
) {
//Get the user profile
const userProfile = getUserData(userId, gameVersion)
// Generate default completion before trying to acquire it
userProfile.Extensions.progression.Locations[completionId] ??= {
Xp: 0,
Level: 1,
}
const completionData =
userProfile.Extensions.progression.Locations[completionId]
const nextLevel: number = clampValue(
completionData.Level + 1,
1,
maxLevel,
)
const nextLevelXp: number = levelToXpRequired(nextLevel)
const thisLevelXp: number = levelToXpRequired(completionData.Level)
return {
Level: completionData.Level,
MaxLevel: maxLevel,
XP: completionData.Xp,
Completion:
(completionData.Xp - thisLevelXp) / (nextLevelXp - thisLevelXp),
XpLeft: nextLevelXp - completionData.Xp,
}
}
/**
* Get the completion data for a location.
* @param locationParentId The parent Id of the location.
* @param subLocationId The id of the sublocation.
* @param gameVersion The game version.
* @param userId The id of the user.
* @param contractType The type of the contract, only used to distinguish evergreen from other types (default).
* @returns The CompletionData object.
*/
getLocationCompletion(
locationParentId: string,
subLocationId: string,
gameVersion: GameVersion,
userId: string,
contractType = "mission",
): CompletionData {
//Get the mastery data
const masteryData: MasteryPackage =
@ -94,40 +151,16 @@ export class MasteryService {
return undefined
}
//Get the user profile
const userProfile = getUserData(userId, gameVersion)
//Gather all required data
const lowerCaseLocationParentId = locationParentId.toLowerCase()
userProfile.Extensions.progression.Locations[
lowerCaseLocationParentId
] ??= {
Xp: 0,
Level: 1,
}
const locationData =
userProfile.Extensions.progression.Locations[
lowerCaseLocationParentId
]
const maxLevel = masteryData.MaxLevel || DEFAULT_MASTERY_MAXLEVEL
const nextLevel: number = clampValue(
locationData.Level + 1,
1,
maxLevel,
)
const nextLevelXp: number = xpRequiredForLevel(nextLevel)
return {
Level: locationData.Level,
MaxLevel: maxLevel,
XP: locationData.Xp,
Completion:
(XP_PER_LEVEL - (nextLevelXp - locationData.Xp)) / XP_PER_LEVEL,
XpLeft: nextLevelXp - locationData.Xp,
...this.getCompletionData(
userId,
gameVersion,
locationParentId.toLowerCase(),
masteryData.MaxLevel || DEFAULT_MASTERY_MAXLEVEL,
contractType === "evergreen"
? xpRequiredForEvergreenLevel
: xpRequiredForLevel,
),
Id: masteryData.Id,
SubLocationId: subLocationId,
HideProgression: masteryData.HideProgression || false,
@ -136,6 +169,36 @@ export class MasteryService {
}
}
/**
* Get the completion data for a firearm. Used for sniper assassin mastery.
* @param progressionKey The Id of the progression. E.g. FIREARMS_SC_HERO_SNIPER_HM.
* @param unlockableName The name of the unlockable.
* @param userId The id of the user.
* @param gameVersion The game version.
* @returns The CompletionData object.
*/
getFirearmCompletion(
progressionKey: string,
unlockableName: string,
userId: string,
gameVersion: GameVersion,
): CompletionData {
return {
...this.getCompletionData(
userId,
gameVersion,
progressionKey,
DEFAULT_MASTERY_MAXLEVEL,
xpRequiredForSniperLevel,
),
Id: progressionKey,
SubLocationId: "",
HideProgression: false,
IsLocationProgression: false,
Name: unlockableName,
}
}
getMasteryPackage(locationParentId: string): MasteryPackage {
if (!this.masteryData.has(locationParentId)) {
return undefined
@ -173,7 +236,7 @@ export class MasteryService {
)
//Map all the data into a new structure
const completionData = this.getCompletionData(
const completionData = this.getLocationCompletion(
locationParentId,
locationParentId,
gameVersion,

View File

@ -79,7 +79,11 @@ contractRoutingRouter.post(
return
}
const sniperloadouts = createSniperLoadouts(contractData)
const sniperloadouts = createSniperLoadouts(
req.jwt.unique_name,
req.gameVersion,
contractData,
)
const loadoutData = {
CharacterLoadoutData:
sniperloadouts.length !== 0 ? sniperloadouts : null,

View File

@ -87,13 +87,14 @@ export function getSubLocationByName(
* @param subLocationId The ID of the targeted sub-location.
* @param userId The ID of the user.
* @param gameVersion The game's version.
* If true, the SubLocationId property will not be set.
* @param contractType The type of the contract, only used to distinguish evergreen from other types (default).
* @returns The completion data object.
*/
export function generateCompletionData(
subLocationId: string,
userId: string,
gameVersion: GameVersion,
contractType = "mission",
): CompletionData {
const subLocation = getSubLocationByName(subLocationId, gameVersion)
@ -101,19 +102,16 @@ export function generateCompletionData(
? subLocation.Properties?.ParentLocation
: subLocationId
const completionData = controller.masteryService.getCompletionData(
const completionData = controller.masteryService.getLocationCompletion(
locationId,
subLocation?.Id,
gameVersion,
userId,
contractType,
)
if (!completionData) {
log(
LogLevel.DEBUG,
`Could not get CompletionData for location ${locationId}`,
)
// Should only reach here for sniper locations.
return {
Level: 1,
MaxLevel: 1,

View File

@ -19,6 +19,7 @@
import { Response, Router } from "express"
import {
contractCreationTutorialId,
DEFAULT_MASTERY_MAXLEVEL,
gameDifficulty,
getMaxProfileLevel,
PEACOCKVERSTRING,
@ -96,6 +97,7 @@ import {
StashpointQuery,
} from "./types/gameSchemas"
import assert from "assert"
import { SniperLoadoutConfig } from "./menus/sniper"
export const preMenuDataRouter = Router()
const menuDataRouter = Router()
@ -1895,7 +1897,6 @@ menuDataRouter.get(
menuDataRouter.get(
"/MasteryUnlockable",
(req: RequestWithJwt<MasteryUnlockableQuery>, res) => {
let sniperLoadouts = getConfig("SniperLoadouts", false)
let masteryUnlockTemplate = getConfig(
"MasteryUnlockablesTemplate",
false,
@ -1914,6 +1915,11 @@ menuDataRouter.get(
}
})()
let sniperLoadout = getConfig<SniperLoadoutConfig>(
"SniperLoadouts",
false,
)[location][req.query.unlockableId]
if (req.gameVersion === "scpc") {
masteryUnlockTemplate = JSON.parse(
JSON.stringify(masteryUnlockTemplate).replace(
@ -1922,36 +1928,33 @@ menuDataRouter.get(
),
)
sniperLoadouts = JSON.parse(
JSON.stringify(sniperLoadouts).replace(/hawk\/+/g, ""),
sniperLoadout = JSON.parse(
JSON.stringify(sniperLoadout).replace(/hawk\/+/g, ""),
)
}
const unlockables = sniperLoadout.Unlockable
res.json({
template: masteryUnlockTemplate,
data: {
CompletionData: generateCompletionData(
location,
CompletionData: controller.masteryService.getFirearmCompletion(
req.query.unlockableId,
sniperLoadout.MainUnlockable.Properties.Name,
req.jwt.unique_name,
req.gameVersion,
),
Drops: [
{
IsLevelMarker: false,
Unlockable:
sniperLoadouts[location][req.query.unlockableId][
"Unlockable"
],
Level: 20,
IsLocked: false,
TypeLocaKey:
"UI_MENU_PAGE_MASTERY_UNLOCKABLE_NAME_weapon",
},
],
Unlockable:
sniperLoadouts[location][req.query.unlockableId][
"MainUnlockable"
],
Drops: unlockables.map((unlockable) => ({
IsLevelMarker: false,
Unlockable: unlockable,
Level:
unlockable.Properties.UnlockOrder ??
DEFAULT_MASTERY_MAXLEVEL,
// TODO: Everything is unlocked. Change this when adding sniper progression
IsLocked: false,
TypeLocaKey: "UI_MENU_PAGE_MASTERY_UNLOCKABLE_NAME_weapon",
})),
Unlockable: sniperLoadout.MainUnlockable,
},
})
},

View File

@ -273,7 +273,11 @@ export async function planningView(
userCentric.Contract.Metadata.Type = "mission"
}
const sniperLoadouts = createSniperLoadouts(contractData)
const sniperLoadouts = createSniperLoadouts(
req.jwt.unique_name,
req.gameVersion,
contractData,
)
if (req.gameVersion === "scpc") {
sniperLoadouts.forEach((loadout) => {

View File

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { controller } from "../controller"
import { nilUuid } from "../utils"
import { getConfig } from "../configSwizzleManager"
import type {
CompletionData,
GameVersion,
MissionManifest,
SniperLoadout,
} from "../types/types"
@ -30,14 +32,21 @@ export type SniperLoadoutConfig = {
}
/**
* Creates the sniper loadouts data.
* Creates the sniper loadouts data for a contract. Returns loadouts for all three
* characters because multiplayer and singleplayer share the same request.
* (Official only returns one because multiplayer is not supported)
*
* @author Anthony Fuller
* @param userId The id of the user.
* @param gameVersion The game version.
* @param contractData The contract's data.
* @param loadoutData Should the output just contain loadout data in an array?
* @returns The sniper loadouts data.
* @returns An array containing the sniper loadouts data for all three characters
* if the contract is a sniper mission, or an empty array otherwise.
*/
export function createSniperLoadouts(
userId: string,
gameVersion: GameVersion,
contractData: MissionManifest,
loadoutData = false,
) {
@ -59,9 +68,9 @@ export function createSniperLoadouts(
{
Item: {
InstanceId: character.InstanceID,
ProfileId:
"00000000-0000-0000-0000-000000000000",
Unlockable: character.Unlockable,
ProfileId: nilUuid,
// TODO: All mastery upgrades are unlocked. Change this when adding sniper progression.
Unlockable: character.Unlockable[18],
Properties: {},
},
ItemDetails: {
@ -91,11 +100,10 @@ export function createSniperLoadouts(
Page: 0,
Recommended: {
item: {
InstanceId:
"00000000-0000-0000-0000-000000000000",
ProfileId:
"00000000-0000-0000-0000-000000000000",
Unlockable: character.Unlockable,
InstanceId: nilUuid,
ProfileId: nilUuid,
// TODO: All mastery upgrades are unlocked. Change this when adding sniper progression.
Unlockable: character.Unlockable[18],
Properties: {},
},
type: "carriedweapon",
@ -109,19 +117,12 @@ export function createSniperLoadouts(
],
LimitedLoadoutUnlockLevel: 0 as number | undefined,
},
// TODO(Anthony): Use the function to get these details.
CompletionData: {
Level: 20,
MaxLevel: 20,
XP: 0,
Completion: 1,
XpLeft: 0,
Id: index,
SubLocationId: "",
HideProgression: false,
IsLocationProgression: false,
Name: index,
} as CompletionData,
CompletionData: controller.masteryService.getFirearmCompletion(
index,
character.MainUnlockable.Properties.Name,
userId,
gameVersion,
),
}
if (loadoutData) {

View File

@ -18,7 +18,6 @@
import type { Response } from "express"
import {
clampValue,
DEFAULT_MASTERY_MAXLEVEL,
contractTypes,
difficultyToString,
@ -29,7 +28,6 @@ import {
levelForXp,
PEACOCKVERSTRING,
SNIPER_LEVEL_INFO,
xpRequiredForEvergreenLevel,
xpRequiredForLevel,
} from "./utils"
import { contractSessions, getCurrentState } from "./eventHandler"
@ -602,8 +600,6 @@ export async function missionEnd(
return
}
const locationParentIdLowerCase = locationParentId.toLocaleLowerCase()
//Resolve all opportunities for the location
const opportunities = contractData.Metadata.Opportunities
const opportunityCount = opportunities ? opportunities.length : 0
@ -647,16 +643,6 @@ export async function missionEnd(
opportunityCount,
)
//Get the location and playerprofile progression from the userdata
if (!userData.Extensions.progression.Locations[locationParentIdLowerCase]) {
userData.Extensions.progression.Locations[locationParentIdLowerCase] = {
Xp: 0,
Level: 1,
}
}
const locationProgressionData =
userData.Extensions.progression.Locations[locationParentIdLowerCase]
const playerProgressionData =
userData.Extensions.progression.PlayerProfileXP
@ -704,10 +690,17 @@ export async function missionEnd(
//NOTE: Official doesn't seem to make up it's mind whether or not XPGain is the same for both Mastery and Profile...
const totalXpGain = calculateXpResult.xp + masteryXpGain
const completionData = generateCompletionData(
contractData.Metadata.Location,
req.jwt.unique_name,
req.gameVersion,
contractData.Metadata.Type,
)
//Calculate the old location progression based on the current one and process it
const oldLocationXp = locationProgressionData.Xp - masteryXpGain
const oldLocationXp = completionData.XP - masteryXpGain
let oldLocationLevel = levelForXp(oldLocationXp)
const newLocationXp = locationProgressionData.Xp
const newLocationXp = completionData.XP
let newLocationLevel = levelForXp(newLocationXp)
const masteryData =
@ -724,12 +717,6 @@ export async function missionEnd(
})
}
const completionData = generateCompletionData(
contractData.Metadata.Location,
req.jwt.unique_name,
req.gameVersion,
)
//Calculate the old playerprofile progression based on the current one and process it
const oldPlayerProfileXp = playerProgressionData.Total - totalXpGain
const oldPlayerProfileLevel = levelForXp(oldPlayerProfileXp)
@ -847,27 +834,9 @@ export async function missionEnd(
locationLevelInfo = EVERGREEN_LEVEL_INFO
const currentLevelRequiredXp = xpRequiredForEvergreenLevel(
locationProgressionData.Level,
)
const nextLevelRequiredXp = clampValue(
xpRequiredForEvergreenLevel(locationProgressionData.Level + 1),
1,
100,
)
//Override completion data for proper animations
completionData.XP = locationProgressionData.Xp
completionData.Level = locationProgressionData.Level
completionData.Completion =
(currentLevelRequiredXp - locationProgressionData.Xp) /
(nextLevelRequiredXp - currentLevelRequiredXp)
//Override the location levels to trigger potential drops
oldLocationLevel = evergreenLevelForXp(
locationProgressionData.Xp - totalXpGain,
)
newLocationLevel = locationProgressionData.Level
oldLocationLevel = evergreenLevelForXp(completionData.XP - totalXpGain)
newLocationLevel = completionData.Level
//Override the silent assassin rank
if (calculateScoreResult.silentAssassin) {

View File

@ -851,7 +851,11 @@ export interface MissionManifestMetadata {
},
]
}[]
CharacterLoadoutData?: unknown
CharacterLoadoutData?: {
Id: string
Loadout: unknown
CompletionData: CompletionData
}[]
SpawnSelectionType?: "random" | string
Gamemodes?: ("versus" | string)[]
Enginemodes?: ("singleplayer" | "multiplayer" | string)[]
@ -1381,7 +1385,7 @@ export type SafehouseCategory = {
export type SniperLoadout = {
ID: string
InstanceID: string
Unlockable: Unlockable
Unlockable: Unlockable[]
MainUnlockable: Unlockable
}

View File

@ -189,6 +189,15 @@ export const SNIPER_LEVEL_INFO: number[] = [
38000000, 47000000, 58000000, 70000000,
]
/**
* Get the number of xp needed to reach a level in sniper missions.
* @param level The level in question.
* @returns The xp, as a number.
*/
export function xpRequiredForSniperLevel(level: number): number {
return SNIPER_LEVEL_INFO[level - 1]
}
/**
* Clamps the given value between a minimum and maximum value
*/
@ -196,6 +205,19 @@ export function clampValue(value: number, min: number, max: number) {
return Math.max(min, Math.min(value, max))
}
/**
* Returns whether a location is a sniper location. Works for both parent and child locations.
* @param location The location ID string.
* @returns A boolean denoting the result.
*/
export function isSniperLocation(location: string): boolean {
return (
location.includes("AUSTRIA") ||
location.includes("SALTY") ||
location.includes("CAGED")
)
}
export function castUserProfile(profile: UserProfile): UserProfile {
const j = fastClone(profile)

File diff suppressed because it is too large Load Diff