1
mirror of https://github.com/thepeacockproject/Peacock synced 2024-11-29 09:15:11 +01:00
Peacock/components/candle/progressionService.ts
Anthony Fuller 46052c7b0e
Multi-Version Mastery and Sniper Scoring (#270)
* Add multi-version mastery files

* Add pro1 unlocks to legacy allunlockables

* Add 47's suit to scpc all unlockables

* Add and remove various configs

* Remove some useless promises

* Fix scpc hub

* Fix issue with user profile saving

* Fix scpc issues for hub

* Add singleplayer/multiplayer sniper

* A great many things

- Add multi-version mastery
- Improve sniper mastery support
- Improve general H2016 support

* Fix some warnings

* Fix pro1 mastery on destination screens

* Remove entP from createInventory, lock/unlock pro1 accordingly

* Remove JSDoc entP parameter from createInventory

* Remove difficultyunlocks from safehouse pages

* Add versioned user profiles

* Prettier run

* Remove false point from user profiles docs

* Add comment about profile versioning to types

* Fix default profile links

* Remove remaining lowercase

* Fix sniper showing XP as XP

* Add game versions to the unlockable map

* Update getMasteryForUnlockable call in planning

* Fix missing locations when updating profiles

* Update versions to v7

* Fix ICA Facility destination mastery

* Fix sniper challenge unlockables showing in inventory

* Sniper Scoring (#273)

* Initial sniper scoring

* Fix linting errors

* Update require table

* Calculate and display final sniper score on end screen

* Bump SMP version to v5.7.0

* Update since version for scoring

* Fix create inventory call for sniper scoring

* Support sniper unlockables in the inventory

* Update versions to v7

* Reflect changes to createInventory in scoreHandler

* Get unlockable name in completion data

* It was not okay.

* Thanks webstorm

* Add support for /profiles/page/GetMasteryCompletionDataForUnlockable

* Support sniper play next

* Remove sniper gamemodes template from overrides

* Remove debug prints from scoring event handler

* Fix challenge multiplier

* Exclude sniper unlockables from stashpoint

* Start fixing up the missionEnd response for sniper

* Update misleading comment

* Use existing global challenge to check for SA on sniper contracts

* Re-add removed global challenges

* Proper support for the mission end screen on sniper contracts

* Remove redundant label

---------

Signed-off-by: Anthony Fuller <24512050+AnthonyFuller@users.noreply.github.com>
Co-authored-by: Govert de Gans <grappigegovert@hotmail.com>

* Add co-op sniper scoring defs

* Update MasteryUnlockable template

* Bump SMP version to v5.9.3

* Re-add deepmerge

* Fix SMP checksum

* Fix linting errors caused by merge

* Fix score handler imports

* Move load flags

* Remove unnecessary game version arg

* Whoopsies

Co-authored-by: Reece Dunham <me@rdil.rocks>
Signed-off-by: Anthony Fuller <24512050+AnthonyFuller@users.noreply.github.com>

---------

Signed-off-by: Anthony Fuller <24512050+AnthonyFuller@users.noreply.github.com>
Co-authored-by: Govert de Gans <grappigegovert@hotmail.com>
Co-authored-by: Reece Dunham <me@rdil.rocks>
2023-07-24 23:47:28 +01:00

265 lines
9.2 KiB
TypeScript

/*
* The Peacock Project - a HITMAN server replacement.
* Copyright (C) 2021-2023 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/>.
*/
import { getSubLocationByName } from "../contracts/dataGen"
import { controller } from "../controller"
import { getUnlockablesById, grantDrops } from "../inventory"
import type { ContractSession, UserProfile, GameVersion } from "../types/types"
import {
clampValue,
DEFAULT_MASTERY_MAXLEVEL,
evergreenLevelForXp,
getMaxProfileLevel,
levelForXp,
sniperLevelForXp,
xpRequiredForEvergreenLevel,
xpRequiredForLevel,
xpRequiredForSniperLevel,
} from "../utils"
import { writeUserData } from "../databaseHandler"
import { MasteryPackageDrop } from "../types/mastery"
export class ProgressionService {
// NOTE: Official will always grant XP to both Location Mastery and the Player Profile
grantProfileProgression(
actionXp: number,
masteryXp: number,
dropIds: string[],
contractSession: ContractSession,
userProfile: UserProfile,
location: string,
sniperUnlockable?: string,
) {
// Total XP for profile XP is the total sum of the action and mastery XP
const xp = actionXp + masteryXp
// Grants profile XP, if this is at contract end where we're adding the final
// sniper score, don't grant it to the profile, otherwise you'll get 1,000+ levels.
if (!sniperUnlockable) {
this.grantUserXp(xp, contractSession, userProfile)
}
// Grants Mastery Progression and Drops
this.grantLocationMasteryXpAndRewards(
masteryXp,
actionXp,
contractSession,
userProfile,
location,
sniperUnlockable,
)
// Award provided drops. E.g. From challenges. Don't run this function
// if there aren't any drops being granted.
if (dropIds.length > 0) {
grantDrops(
userProfile.Id,
getUnlockablesById(dropIds, contractSession.gameVersion),
)
}
// Saves profile data
writeUserData(userProfile.Id, contractSession.gameVersion)
}
// Returns current mastery progression for given location
getMasteryProgressionForLocation(
userProfile: UserProfile,
location: string,
subPkgId?: string,
) {
return subPkgId
? userProfile.Extensions.progression.Locations[location][subPkgId]
: userProfile.Extensions.progression.Locations[location]
}
// Return mastery drops from location from a level range
private getLocationMasteryDrops(
gameVersion: GameVersion,
isEvergreenContract: boolean,
masteryLocationDrops: MasteryPackageDrop[],
minLevel: number,
maxLevel: number,
) {
const unlockableIds = masteryLocationDrops
.filter((drop) => drop.Level > minLevel && drop.Level <= maxLevel)
.map((drop) => drop.Id)
const unlockables = getUnlockablesById(unlockableIds, gameVersion)
/**
* If missions type is evergreen, checks if any of the unlockables has unlockable gear, and award those too
*
* This is required to unlock the item to the normal inventory too, as the freelancer and normal inventory item ID is not the same
*/
if (isEvergreenContract) {
const evergreenGearUnlockables = unlockables.reduce((acc, u) => {
if (u.Properties.Unlocks) acc.push(...u.Properties.Unlocks)
return acc
}, [])
if (evergreenGearUnlockables.length) {
unlockables.push(
...getUnlockablesById(
evergreenGearUnlockables,
gameVersion,
),
)
}
}
return unlockables
}
// Grants xp and rewards to mastery progression on contract location
private grantLocationMasteryXpAndRewards(
masteryXp: number,
actionXp: number,
contractSession: ContractSession,
userProfile: UserProfile,
location: string,
sniperUnlockable?: string,
): boolean {
const contract = controller.resolveContract(contractSession.contractId)
if (!contract) {
return false
}
const subLocation = getSubLocationByName(
location ?? contract.Metadata.Location,
contractSession.gameVersion,
)
const parentLocationId = subLocation
? subLocation.Properties?.ParentLocation
: location ?? contract.Metadata.Location
if (!parentLocationId) {
return false
}
// We can't grant sniper XP here as it's based on final score, so we skip updating mastery
// until we call this in mission end.
if (contract.Metadata.Type !== "sniper" || sniperUnlockable) {
const masteryData = controller.masteryService.getMasteryPackage(
parentLocationId,
contractSession.gameVersion,
)
const locationData = this.getMasteryProgressionForLocation(
userProfile,
parentLocationId,
contractSession.gameVersion === "h1"
? contract.Metadata.Difficulty ?? "normal"
: sniperUnlockable ?? undefined,
)
const maxLevel = masteryData?.MaxLevel || DEFAULT_MASTERY_MAXLEVEL
const isEvergreenContract = contract.Metadata.Type === "evergreen"
if (masteryData) {
const previousLevel = locationData.Level
locationData.Xp = clampValue(
locationData.Xp + masteryXp + actionXp,
0,
isEvergreenContract
? xpRequiredForEvergreenLevel(maxLevel)
: sniperUnlockable
? xpRequiredForSniperLevel(maxLevel)
: xpRequiredForLevel(maxLevel),
)
locationData.Level = clampValue(
isEvergreenContract
? evergreenLevelForXp(locationData.Xp)
: sniperUnlockable
? sniperLevelForXp(locationData.Xp)
: levelForXp(locationData.Xp),
1,
maxLevel,
)
// If mastery level has gone up, check if there are available drop rewards and award them
if (locationData.Level > previousLevel) {
const masteryLocationDrops = this.getLocationMasteryDrops(
contractSession.gameVersion,
isEvergreenContract,
sniperUnlockable
? masteryData.SubPackages.find(
(pkg) => pkg.Id === sniperUnlockable,
).Drops
: masteryData.Drops,
previousLevel,
locationData.Level,
)
grantDrops(userProfile.Id, masteryLocationDrops)
}
}
// Update the EvergreenLevel with the latest Mastery Level
if (isEvergreenContract) {
userProfile.Extensions.CPD[contract.Metadata.CpdId][
"EvergreenLevel"
] = locationData.Level
}
}
// Update the SubLocation data
const profileData = userProfile.Extensions.progression.PlayerProfileXP
let foundSubLocation = profileData.Sublocations.find(
(e) => e.Location === parentLocationId,
)
if (!foundSubLocation) {
foundSubLocation = {
Location: parentLocationId,
Xp: 0,
ActionXp: 0,
}
profileData.Sublocations.push(foundSubLocation)
}
foundSubLocation.Xp += masteryXp
foundSubLocation.ActionXp += actionXp
return true
}
// Grants xp to user profile
// TODO: Combine with grantLocationMasteryXp?
private grantUserXp(
xp: number,
contractSession: ContractSession,
userProfile: UserProfile,
): boolean {
const profileData = userProfile.Extensions.progression.PlayerProfileXP
profileData.Total += xp
profileData.ProfileLevel = clampValue(
levelForXp(profileData.Total),
1,
getMaxProfileLevel(contractSession.gameVersion),
)
return true
}
}