diff --git a/package-lock.json b/package-lock.json index db379b43..9ab7b4bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.60", + "warframe-public-export-plus": "^0.5.62", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3703,9 +3703,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.60", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.60.tgz", - "integrity": "sha512-vMfytUc4xRi+b7RTSq+TJEl91vwEegpQKxLtXwRPfs9ZHhntxc4rmDYSNWJTvgf/aWXsFUxQlqL/GV5OLPGM7g==" + "version": "0.5.62", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.62.tgz", + "integrity": "sha512-D8ZzjkU9rrK/59VqCfpMoV31HVmwHZV1dNZxPO85AOlcjg/G81Fu3kgITQTaw9sdNagLPLQnFaiXY58pxxRwgA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 010e5dcb..5b995cbd 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.60", + "warframe-public-export-plus": "^0.5.62", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/addToAllianceController.ts b/src/controllers/api/addToAllianceController.ts index e7b24dec..7509e95a 100644 --- a/src/controllers/api/addToAllianceController.ts +++ b/src/controllers/api/addToAllianceController.ts @@ -1,7 +1,7 @@ import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers"; import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; import { createMessage } from "@/src/services/inboxService"; -import { getInventory } from "@/src/services/inventoryService"; +import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { GuildPermission } from "@/src/types/guildTypes"; import { logger } from "@/src/utils/logger"; @@ -95,7 +95,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => { } ], sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title", - icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, contextInfo: alliance._id.toString(), highPriority: true, acceptAction: "ALLIANCE_INVITE", diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index aeda4214..53f0b445 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -4,7 +4,7 @@ import { Account } from "@/src/models/loginModel"; import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService"; import { hasGuildPermission } from "@/src/services/guildService"; import { createMessage } from "@/src/services/inboxService"; -import { getInventory } from "@/src/services/inventoryService"; +import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; import { IOid } from "@/src/types/commonTypes"; import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes"; @@ -64,7 +64,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { } ], sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title", - icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, contextInfo: payload.GuildId.$oid, highPriority: true, acceptAction: "GUILD_INVITE", diff --git a/src/controllers/api/crewMembersController.ts b/src/controllers/api/crewMembersController.ts index 15ef0fbf..a4f2ea2a 100644 --- a/src/controllers/api/crewMembersController.ts +++ b/src/controllers/api/crewMembersController.ts @@ -1,4 +1,5 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -7,15 +8,23 @@ import { Types } from "mongoose"; export const crewMembersController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId, "CrewMembers"); + const inventory = await getInventory(accountId, "CrewMembers NemesisHistory"); const data = getJSONfromString(String(req.body)); - const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!; - dbCrewMember.AssignedRole = data.crewMember.AssignedRole; - dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency; - dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx; - dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid); - dbCrewMember.Configs = data.crewMember.Configs; - dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand; + if (data.crewMember.SecondInCommand) { + clearOnCall(inventory); + } + if (data.crewMember.ItemId.$oid == "000000000000000000000000") { + const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!; + convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand; + } else { + const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!; + dbCrewMember.AssignedRole = data.crewMember.AssignedRole; + dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency; + dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx; + dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid); + dbCrewMember.Configs = data.crewMember.Configs; + dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand; + } await inventory.save(); res.json({ crewMemberId: data.crewMember.ItemId.$oid, @@ -26,3 +35,20 @@ export const crewMembersController: RequestHandler = async (req, res) => { interface ICrewMembersRequest { crewMember: ICrewMemberClient; } + +const clearOnCall = (inventory: TInventoryDatabaseDocument): void => { + for (const cm of inventory.CrewMembers) { + if (cm.SecondInCommand) { + cm.SecondInCommand = false; + return; + } + } + if (inventory.NemesisHistory) { + for (const cm of inventory.NemesisHistory) { + if (cm.SecondInCommand) { + cm.SecondInCommand = false; + return; + } + } + } +}; diff --git a/src/controllers/api/getVendorInfoController.ts b/src/controllers/api/getVendorInfoController.ts index b161176e..5f9d3292 100644 --- a/src/controllers/api/getVendorInfoController.ts +++ b/src/controllers/api/getVendorInfoController.ts @@ -1,14 +1,20 @@ import { RequestHandler } from "express"; -import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; -export const getVendorInfoController: RequestHandler = (req, res) => { - if (typeof req.query.vendor == "string") { - const manifest = getVendorManifestByTypeName(req.query.vendor); - if (!manifest) { - throw new Error(`Unknown vendor: ${req.query.vendor}`); - } - res.json(manifest); - } else { - res.status(400).end(); +export const getVendorInfoController: RequestHandler = async (req, res) => { + let manifest = getVendorManifestByTypeName(req.query.vendor as string); + if (!manifest) { + throw new Error(`Unknown vendor: ${req.query.vendor as string}`); } + + // For testing purposes, authenticating with this endpoint is optional here, but would be required on live. + if (req.query.accountId) { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + manifest = applyStandingToVendorManifest(inventory, manifest); + } + + res.json(manifest); }; diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts index 09b48f4e..55865cee 100644 --- a/src/controllers/api/giftingController.ts +++ b/src/controllers/api/giftingController.ts @@ -2,7 +2,12 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Account } from "@/src/models/loginModel"; import { areFriends } from "@/src/services/friendService"; import { createMessage } from "@/src/services/inboxService"; -import { combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { + combineInventoryChanges, + getEffectiveAvatarImageType, + getInventory, + updateCurrency +} from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { IOid } from "@/src/types/commonTypes"; @@ -85,7 +90,7 @@ export const giftingController: RequestHandler = async (req, res) => { } ], sub: "/Lotus/Language/Menu/GiftReceivedSubject", - icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, gifts: [ { GiftType: data.PurchaseParams.StoreItem diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 94d253ad..8e775c10 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -9,7 +9,12 @@ import { getMessage } from "@/src/services/inboxService"; import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; -import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; +import { + addItems, + combineInventoryChanges, + getEffectiveAvatarImageType, + getInventory +} from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; import { ExportFlavour } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; @@ -88,7 +93,7 @@ export const inboxController: RequestHandler = async (req, res) => { } ], sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject", - icon: ExportFlavour[inventory.ActiveAvatarImageType].icon, + icon: ExportFlavour[getEffectiveAvatarImageType(inventory)].icon, highPriority: true } ]); diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index a26a73c5..97cc050b 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -340,7 +340,7 @@ export const getInventoryResponse = async ( }; const addString = (arr: string[], str: string): void => { - if (!arr.find(x => x == str)) { + if (arr.indexOf(str) == -1) { arr.push(str); } }; diff --git a/src/controllers/api/loginRewardsSelectionController.ts b/src/controllers/api/loginRewardsSelectionController.ts index 4b6fc210..290a13f8 100644 --- a/src/controllers/api/loginRewardsSelectionController.ts +++ b/src/controllers/api/loginRewardsSelectionController.ts @@ -26,7 +26,7 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res) StoreItemType: body.ChosenReward }; inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges; - if (!evergreenRewards.find(x => x == body.ChosenReward)) { + if (evergreenRewards.indexOf(body.ChosenReward) == -1) { inventory.LoginMilestoneRewards.push(body.ChosenReward); } } else { diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 41f49143..fedff108 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -61,7 +61,11 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) if ( missionReport.MissionStatus !== "GS_SUCCESS" && - !(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId) + !( + missionReport.RewardInfo?.jobId || + missionReport.RewardInfo?.challengeMissionId || + missionReport.RewardInfo?.T + ) ) { if (missionReport.EndOfMatchUpload) { inventory.RewardSeed = generateRewardSeed(); diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 60b02a17..df8e1652 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -7,7 +7,7 @@ import { getNemesisPasscodeModTypes, getWeaponsForManifest, IKnifeResponse, - showdownNodes + nemesisFactionInfos } from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; @@ -24,7 +24,8 @@ import { IUpgradeClient, IWeaponSkinClient, LoadoutIndex, - TEquipmentKey + TEquipmentKey, + TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; @@ -176,7 +177,7 @@ export const nemesisController: RequestHandler = async (req, res) => { weaponIdx = initialWeaponIdx; do { const weapon = weapons[weaponIdx]; - if (!body.target.DisallowedWeapons.find(x => x == weapon)) { + if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { break; } weaponIdx = (weaponIdx + 1) % weapons.length; @@ -222,7 +223,7 @@ export const nemesisController: RequestHandler = async (req, res) => { inventory.Nemesis!.InfNodes = [ { - Node: showdownNodes[inventory.Nemesis!.Faction], + Node: nemesisFactionInfos[inventory.Nemesis!.Faction].showdownNode, Influence: 1 } ]; @@ -269,7 +270,7 @@ interface INemesisStartRequest { WeaponIdx: number; AgentIdx: number; BirthNode: string; - Faction: string; + Faction: TNemesisFaction; Rank: number; k: boolean; Traded: boolean; diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 14dc9aa2..171538a7 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -1,8 +1,7 @@ -import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; -import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { addEmailItem, getDialogue, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; @@ -107,26 +106,3 @@ interface IOtherDialogueInfo { Tag: string; Value: number; } - -const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => { - let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName); - if (!dialogue) { - dialogue = - inventory.DialogueHistory!.Dialogues![ - inventory.DialogueHistory!.Dialogues!.push({ - Rank: 0, - Chemistry: 0, - AvailableDate: new Date(0), - AvailableGiftDate: new Date(0), - RankUpExpiry: new Date(0), - BountyChemExpiry: new Date(0), - QueuedDialogues: [], - Gifts: [], - Booleans: [], - Completed: [], - DialogueName: dialogueName - }) - 1 - ]; - } - return dialogue; -}; diff --git a/src/controllers/api/saveLoadoutController.ts b/src/controllers/api/saveLoadoutController.ts index 6fd3de61..c5383e1c 100644 --- a/src/controllers/api/saveLoadoutController.ts +++ b/src/controllers/api/saveLoadoutController.ts @@ -2,11 +2,12 @@ import { RequestHandler } from "express"; import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes"; import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService"; import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; export const saveLoadoutController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest; + const body: ISaveLoadoutRequest = getJSONfromString(String(req.body)); // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true })); // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 6e9db804..015aec25 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -1,5 +1,5 @@ import { ExportRegions, ExportWarframes } from "warframe-public-export-plus"; -import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInfNode, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes"; import { getRewardAtPercentage, SRng } from "@/src/services/rngService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { logger } from "../utils/logger"; @@ -7,13 +7,51 @@ import { IOid } from "../types/commonTypes"; import { Types } from "mongoose"; import { addMods, generateRewardSeed } from "../services/inventoryService"; import { isArchwingMission } from "../services/worldStateService"; -import { fromStoreItem, toStoreItem } from "../services/itemDataService"; -import { createMessage } from "../services/inboxService"; import { version_compare } from "./inventoryHelpers"; -export const getInfNodes = (faction: string, rank: number): IInfNode[] => { +export interface INemesisFactionInfo { + systemIndexes: number[]; + showdownNode: string; + ephemeraChance: number; + firstKillReward: string; + firstConvertReward: string; + messageTitle: string; + messageBody: string; +} + +export const nemesisFactionInfos: Record = { + FC_GRINEER: { + systemIndexes: [2, 3, 9, 11, 18], + showdownNode: "CrewBattleNode557", + ephemeraChance: 0.05, + firstKillReward: "/Lotus/StoreItems/Upgrades/Skins/Clan/LichKillerBadgeItem", + firstConvertReward: "/Lotus/StoreItems/Upgrades/Skins/Sigils/KuvaLichSigil", + messageTitle: "/Lotus/Language/Inbox/VanquishKuvaMsgTitle", + messageBody: "/Lotus/Language/Inbox/VanquishLichMsgBody" + }, + FC_CORPUS: { + systemIndexes: [1, 15, 4, 7, 8], + showdownNode: "CrewBattleNode558", + ephemeraChance: 0.2, + firstKillReward: "/Lotus/StoreItems/Upgrades/Skins/Clan/CorpusLichBadgeItem", + firstConvertReward: "/Lotus/StoreItems/Upgrades/Skins/Sigils/CorpusLichSigil", + messageTitle: "/Lotus/Language/Inbox/VanquishLawyerMsgTitle", + messageBody: "/Lotus/Language/Inbox/VanquishLichMsgBody" + }, + FC_INFESTATION: { + systemIndexes: [23], + showdownNode: "CrewBattleNode559", + ephemeraChance: 0, + firstKillReward: "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichVanquishedSigil", + firstConvertReward: "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichConvertedSigil", + messageTitle: "/Lotus/Language/Inbox/VanquishBandMsgTitle", + messageBody: "/Lotus/Language/Inbox/VanquishBandMsgBody" + } +}; + +export const getInfNodes = (faction: TNemesisFaction, rank: number): IInfNode[] => { const infNodes = []; - const systemIndex = systemIndexes[faction][rank]; + const systemIndex = nemesisFactionInfos[faction].systemIndexes[rank]; for (const [key, value] of Object.entries(ExportRegions)) { if ( value.systemIndex === systemIndex && @@ -35,20 +73,38 @@ export const getInfNodes = (faction: string, rank: number): IInfNode[] => { return infNodes; }; -const systemIndexes: Record = { - FC_GRINEER: [2, 3, 9, 11, 18], - FC_CORPUS: [1, 15, 4, 7, 8], - FC_INFESTATION: [23] -}; +type TInnateDamageTag = + | "InnateElectricityDamage" + | "InnateHeatDamage" + | "InnateFreezeDamage" + | "InnateToxinDamage" + | "InnateMagDamage" + | "InnateRadDamage" + | "InnateImpactDamage"; -export const showdownNodes: Record = { - FC_GRINEER: "CrewBattleNode557", - FC_CORPUS: "CrewBattleNode558", - FC_INFESTATION: "CrewBattleNode559" +const ephmeraTypes: Record<"FC_GRINEER" | "FC_CORPUS", Record> = { + FC_GRINEER: { + InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaLightningEphemera", + InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaFireEphemera", + InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaIceEphemera", + InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaToxinEphemera", + InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaMagneticEphemera", + InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaTricksterEphemera", + InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaImpactEphemera" + }, + FC_CORPUS: { + InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraA", + InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraB", + InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraC", + InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraD", + InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraE", + InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraF", + InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraG" + } }; // Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. -export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => { +export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFaction }): number[] => { const rng = new SRng(nemesis.fp); const choices = [0, 1, 2, 3, 5, 6, 7]; let choiceIndex = rng.randomInt(0, choices.length - 1); @@ -87,7 +143,7 @@ const antivirusMods: readonly string[] = [ "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod" ]; -export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: string }): string[] => { +export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => { const passcode = getNemesisPasscode(nemesis); return nemesis.Faction == "FC_INFESTATION" ? passcode.map(i => antivirusMods[i]) @@ -248,7 +304,7 @@ export const getWeaponsForManifest = (manifest: string): readonly string[] => { }; export const isNemesisCompatibleWithVersion = ( - nemesis: { manifest: string; Faction: string }, + nemesis: { manifest: string; Faction: TNemesisFaction }, buildLabel: string ): boolean => { // Anything below 35.6.0 is not going to be okay given our set of supported manifests. @@ -271,21 +327,31 @@ export const isNemesisCompatibleWithVersion = ( return true; }; -export const getInnateDamageTag = ( - KillingSuit: string -): - | "InnateElectricityDamage" - | "InnateFreezeDamage" - | "InnateHeatDamage" - | "InnateImpactDamage" - | "InnateMagDamage" - | "InnateRadDamage" - | "InnateToxinDamage" => { +export const getInnateDamageTag = (KillingSuit: string): TInnateDamageTag => { return ExportWarframes[KillingSuit].nemesisUpgradeTag!; }; -// TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003. -export const getInnateDamageValue = (fp: bigint): number => { +const petHeads = [ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC" +] as const; + +export interface INemesisProfile { + innateDamageTag: TInnateDamageTag; + innateDamageValue: number; + ephemera?: string; + petHead?: (typeof petHeads)[number]; + petBody?: string; + petLegs?: string; + petTail?: string; +} + +export const generateNemesisProfile = ( + fp: bigint = generateRewardSeed(), + Faction: TNemesisFaction = "FC_CORPUS", + killingSuit: string = "/Lotus/Powersuits/Ember/Ember" +): INemesisProfile => { const rng = new SRng(fp); rng.randomFloat(); // used for the weapon index const WeaponUpgradeValueAttenuationExponent = 2.25; @@ -293,7 +359,33 @@ export const getInnateDamageValue = (fp: bigint): number => { if (value >= 0.941428) { value = 1; } - return Math.trunc(value * 0x40000000); + const profile: INemesisProfile = { + innateDamageTag: getInnateDamageTag(killingSuit), + innateDamageValue: Math.trunc(value * 0x40000000) // TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003. + }; + if (rng.randomFloat() <= nemesisFactionInfos[Faction].ephemeraChance && Faction != "FC_INFESTATION") { + profile.ephemera = ephmeraTypes[Faction][profile.innateDamageTag]; + } + rng.randomFloat(); // something related to sentinel agent maybe + if (Faction == "FC_CORPUS") { + profile.petHead = rng.randomElement(petHeads)!; + profile.petBody = rng.randomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC" + ])!; + profile.petLegs = rng.randomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC" + ])!; + profile.petTail = rng.randomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC" + ])!; + } + return profile; }; export const getKillTokenRewardCount = (fp: bigint): number => { @@ -352,52 +444,3 @@ export const getInfestedLichItemRewards = (fp: bigint): string[] => { const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type; return [rotAReward, rotBReward]; }; - -export const sendCodaFinishedMessage = async ( - inventory: TInventoryDatabaseDocument, - fp: bigint = generateRewardSeed(), - name: string = "ZEKE_BEATWOMAN_TM.1999", - killed: boolean = true -): Promise => { - const att: string[] = []; - - // First vanquish/convert gives a sigil - const sigil = killed - ? "/Lotus/Upgrades/Skins/Sigils/InfLichVanquishedSigil" - : "/Lotus/Upgrades/Skins/Sigils/InfLichConvertedSigil"; - if (!inventory.WeaponSkins.find(x => x.ItemType == sigil)) { - att.push(toStoreItem(sigil)); - } - - const [rotAReward, rotBReward] = getInfestedLichItemRewards(fp); - att.push(fromStoreItem(rotAReward)); - att.push(fromStoreItem(rotBReward)); - - let countedAtt: ITypeCount[] | undefined; - if (killed) { - countedAtt = [ - { - ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", - ItemCount: getKillTokenRewardCount(fp) - } - ]; - } - - await createMessage(inventory.accountOwnerId, [ - { - sndr: "/Lotus/Language/Bosses/Ordis", - msg: "/Lotus/Language/Inbox/VanquishBandMsgBody", - arg: [ - { - Key: "LICH_NAME", - Tag: name - } - ], - att: att, - countedAtt: countedAtt, - sub: "/Lotus/Language/Inbox/VanquishBandMsgTitle", - icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", - highPriority: true - } - ]); -}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index ba9a1010..27caf691 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1318,7 +1318,7 @@ const nemesisSchema = new Schema( InfNodes: { type: [infNodeSchema], default: undefined }, HenchmenKilled: Number, HintProgress: Number, - Hints: { type: [Number], default: undefined }, + Hints: { type: [Number], default: [] }, GuessHistory: { type: [Number], default: undefined }, MissionCount: Number, LastEnc: Number @@ -1621,7 +1621,7 @@ const inventorySchema = new Schema( Drones: [droneSchema], //Active profile ico - ActiveAvatarImageType: { type: String, default: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault" }, + ActiveAvatarImageType: String, // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable DiscoveredMarkers: [discoveredMarkerSchema], diff --git a/src/services/guildService.ts b/src/services/guildService.ts index f1bb5ff9..0d8999aa 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -445,7 +445,7 @@ export const addGuildMemberShipDecoContribution = (guildMember: IGuildMemberData export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { if (build.guildXpValue) { guild.ClaimedXP ??= []; - if (!guild.ClaimedXP.find(x => x == build.resultType)) { + if (guild.ClaimedXP.indexOf(build.resultType) == -1) { guild.ClaimedXP.push(build.resultType); guild.XP += build.guildXpValue; } diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ae5ded50..09d23306 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -28,7 +28,8 @@ import { ITraits, ICalendarProgress, INemesisWeaponTargetFingerprint, - INemesisPetTargetFingerprint + INemesisPetTargetFingerprint, + IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -83,7 +84,7 @@ import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from ". import { createMessage } from "./inboxService"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { getWorldState } from "./worldStateService"; -import { getInnateDamageTag, getInnateDamageValue } from "../helpers/nemesisHelpers"; +import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -1906,6 +1907,29 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void => } }; +export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => { + let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName); + if (!dialogue) { + dialogue = + inventory.DialogueHistory!.Dialogues![ + inventory.DialogueHistory!.Dialogues!.push({ + Rank: 0, + Chemistry: 0, + AvailableDate: new Date(0), + AvailableGiftDate: new Date(0), + RankUpExpiry: new Date(0), + BountyChemExpiry: new Date(0), + QueuedDialogues: [], + Gifts: [], + Booleans: [], + Completed: [], + DialogueName: dialogueName + }) - 1 + ]; + } + return dialogue; +}; + export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => { const currentSeason = getWorldState().KnownCalendarSeasons[0]; @@ -1945,8 +1969,7 @@ export const giveNemesisWeaponRecipe = ( weaponType: string, nemesisName: string = "AGOR ROK", weaponLoc?: string, - KillingSuit: string = "/Lotus/Powersuits/Ember/Ember", - fp: bigint = generateRewardSeed() + profile: INemesisProfile = generateNemesisProfile() ): void => { if (!weaponLoc) { weaponLoc = ExportWeapons[weaponType].name; @@ -1967,8 +1990,8 @@ export const giveNemesisWeaponRecipe = ( compat: weaponType, buffs: [ { - Tag: getInnateDamageTag(KillingSuit), - Value: getInnateDamageValue(fp) + Tag: profile.innateDamageTag, + Value: profile.innateDamageValue } ] }, @@ -1977,27 +2000,15 @@ export const giveNemesisWeaponRecipe = ( }); }; -export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, nemesisName: string = "AGOR ROK"): void => { - const head = getRandomElement([ - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC" - ])!; - const body = getRandomElement([ - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC" - ])!; - const legs = getRandomElement([ - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC" - ])!; - const tail = getRandomElement([ - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC" - ])!; +export const giveNemesisPetRecipe = ( + inventory: TInventoryDatabaseDocument, + nemesisName: string = "AGOR ROK", + profile: INemesisProfile = generateNemesisProfile() +): void => { + const head = profile.petHead!; + const body = profile.petBody!; + const legs = profile.petLegs!; + const tail = profile.petTail!; const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0]; addRecipes(inventory, [ { @@ -2014,3 +2025,7 @@ export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, neme } satisfies INemesisPetTargetFingerprint) }); }; + +export const getEffectiveAvatarImageType = (inventory: TInventoryDatabaseDocument): string => { + return inventory.ActiveAvatarImageType ?? "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault"; +}; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 41e33ad3..f2ae70ec 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -90,7 +90,7 @@ export const getAccountIdForRequest = async (req: Request): Promise => { }; export const isAdministrator = (account: TAccountDocument): boolean => { - return !!config.administratorNames?.find(x => x == account.DisplayName); + return config.administratorNames?.indexOf(account.DisplayName) != -1; }; const platform_magics = [753, 639, 247, 37, 60]; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 4ec203bb..e577a486 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -10,7 +10,7 @@ import { import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService"; -import { equipmentKeys, IMission, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { equipmentKeys, IMission, ITypeCount, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { addBooster, addChallenges, @@ -35,6 +35,7 @@ import { combineInventoryChanges, generateRewardSeed, getCalendarProgress, + getDialogue, giveNemesisPetRecipe, giveNemesisWeaponRecipe, updateCurrency, @@ -43,7 +44,7 @@ import { import { updateQuestKey } from "@/src/services/questService"; import { Types } from "mongoose"; import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes"; -import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService"; +import { fromStoreItem, getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; @@ -55,10 +56,26 @@ import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent. import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json"; -import { getInfNodes, getWeaponsForManifest, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers"; +import { + generateNemesisProfile, + getInfestedLichItemRewards, + getInfNodes, + getKillTokenRewardCount, + getNemesisPasscode, + getWeaponsForManifest, + nemesisFactionInfos +} from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; -import { getLiteSortie, getSortie, idToBountyCycle, idToDay, idToWeek, pushClassicBounties } from "./worldStateService"; +import { + getLiteSortie, + getSortie, + getWorldState, + idToBountyCycle, + idToDay, + idToWeek, + pushClassicBounties +} from "./worldStateService"; import { config } from "./configService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import { ISyndicateMissionInfo } from "../types/worldStateTypes"; @@ -168,6 +185,14 @@ export const addMissionInventoryUpdates = async ( } if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) { inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress; + if (inventory.Nemesis.Faction != "FC_INFESTATION" && inventory.Nemesis.Hints.length != 3) { + const progressNeeded = [35, 60, 100][inventory.Nemesis.Hints.length]; + if (inventory.Nemesis.HintProgress >= progressNeeded) { + inventory.Nemesis.HintProgress -= progressNeeded; + const passcode = getNemesisPasscode(inventory.Nemesis); + inventory.Nemesis.Hints.push(passcode[inventory.Nemesis.Hints.length]); + } + } } if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) { // e.g. for Profit-Taker Phase 1: @@ -358,7 +383,7 @@ export const addMissionInventoryUpdates = async ( } if ( inventory.LibraryActiveDailyTaskInfo && - inventory.LibraryActiveDailyTaskInfo.EnemyTypes.find(x => x == scan.EnemyType) + inventory.LibraryActiveDailyTaskInfo.EnemyTypes.indexOf(scan.EnemyType) != -1 ) { inventory.LibraryActiveDailyTaskInfo.Scans ??= 0; inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count; @@ -631,12 +656,21 @@ export const addMissionInventoryUpdates = async ( Rank: inventory.Nemesis.Rank, Traded: inventory.Nemesis.Traded, PrevOwners: inventory.Nemesis.PrevOwners, - SecondInCommand: inventory.Nemesis.SecondInCommand, + SecondInCommand: false, Weakened: inventory.Nemesis.Weakened, // And set killed flag k: value.killed }); + const profile = generateNemesisProfile( + inventory.Nemesis.fp, + inventory.Nemesis.Faction, + inventory.Nemesis.KillingSuit + ); + const nemesisFactionInfo = nemesisFactionInfos[inventory.Nemesis.Faction]; + const att: string[] = []; + let countedAtt: ITypeCount[] | undefined; + if (value.killed) { if ( value.weaponLoc && @@ -645,23 +679,79 @@ export const addMissionInventoryUpdates = async ( const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[ inventory.Nemesis.WeaponIdx ]; - giveNemesisWeaponRecipe( - inventory, - weaponType, - value.nemesisName, - value.weaponLoc, - inventory.Nemesis.KillingSuit, - inventory.Nemesis.fp - ); + giveNemesisWeaponRecipe(inventory, weaponType, value.nemesisName, value.weaponLoc, profile); + att.push(weaponType); } - if (value.petLoc) { - giveNemesisPetRecipe(inventory); + //if (value.petLoc) { + if (profile.petHead) { + giveNemesisPetRecipe(inventory, value.nemesisName, profile); + att.push( + { + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": + "/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadABlueprint", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": + "/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadBBlueprint", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": + "/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadCBlueprint" + }[profile.petHead] + ); } } - // TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given? + // "Players will receive a Lich's Ephemera regardless of whether they Vanquish or Convert them." + if (profile.ephemera) { + addSkin(inventory, profile.ephemera); + att.push(profile.ephemera); + } + + const skinRewardStoreItem = value.killed + ? nemesisFactionInfo.firstKillReward + : nemesisFactionInfo.firstConvertReward; + if (Object.keys(addSkin(inventory, fromStoreItem(skinRewardStoreItem))).length != 0) { + att.push(skinRewardStoreItem); + } + if (inventory.Nemesis.Faction == "FC_INFESTATION") { - await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed); + const [rotARewardStoreItem, rotBRewardStoreItem] = getInfestedLichItemRewards( + inventory.Nemesis.fp + ); + const rotAReward = fromStoreItem(rotARewardStoreItem); + const rotBReward = fromStoreItem(rotBRewardStoreItem); + await addItem(inventory, rotAReward); + await addItem(inventory, rotBReward); + att.push(rotAReward); + att.push(rotBReward); + + if (value.killed) { + countedAtt = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + ItemCount: getKillTokenRewardCount(inventory.Nemesis.fp) + } + ]; + addMiscItems(inventory, countedAtt); + } + } + + if (value.killed) { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Ordis", + msg: nemesisFactionInfo.messageBody, + arg: [ + { + Key: "LICH_NAME", + Tag: value.nemesisName + } + ], + att: att, + countedAtt: countedAtt, + attVisualOnly: true, + sub: nemesisFactionInfo.messageTitle, + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + highPriority: true + } + ]); } inventory.Nemesis = undefined; @@ -1175,8 +1265,9 @@ export const addMissionRewards = async ( } if (rewardInfo.challengeMissionId) { - const [syndicateTag, tierStr] = rewardInfo.challengeMissionId.split("_"); // TODO: third part in HexSyndicate jobs - Chemistry points + const [syndicateTag, tierStr, chemistryStr] = rewardInfo.challengeMissionId.split("_"); const tier = Number(tierStr); + const chemistry = Number(chemistryStr); const isSteelPath = missions?.Tier; if (syndicateTag === "ZarimanSyndicate") { let medallionAmount = tier + 1; @@ -1193,6 +1284,23 @@ export const addMissionRewards = async ( if (isSteelPath) standingAmount *= 1.5; AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount)); } + if (syndicateTag == "HexSyndicate" && chemistry && tier < 6) { + const seed = getWorldState().SyndicateMissions.find(x => x.Tag == "HexSyndicate")!.Seed; + const { nodes, buddies } = getHexBounties(seed); + const buddy = buddies[tier]; + logger.debug(`Hex seed is ${seed}, giving chemistry for ${buddy}`); + if (missions?.Tag != nodes[tier]) { + logger.warn( + `Uh-oh, tier ${tier} bounty should've been on ${nodes[tier]} but you were just on ${missions?.Tag}` + ); + } + const tomorrowAt0Utc = config.noKimCooldowns + ? Date.now() + : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; + const dialogue = getDialogue(inventory, buddy); + dialogue.Chemistry += chemistry; + dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc); + } if (isSteelPath) { await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1); MissionRewards.push({ @@ -1403,6 +1511,37 @@ function getRandomMissionDrops( } else { rewardManifests = []; } + } else if (RewardInfo.T == 13) { + // Undercroft extra/side portal (normal mode), gives 1 Pathos Clamp + Duviri Arcane. + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem", + ItemCount: 1 + }); + rewardManifests = [ + "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriStaticUndercroftResourceRewards" + ]; + } else if (RewardInfo.T == 14) { + // Undercroft extra/side portal (steel path), gives 3 Pathos Clamps + Eidolon Arcane. + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem", + ItemCount: 3 + }); + rewardManifests = [ + "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriSteelPathStaticUndercroftResourceRewards" + ]; + } else if (RewardInfo.T == 15) { + rewardManifests = [ + mission?.Tier == 1 + ? "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoSteelPathRNGRewards" + : "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoNormalRNGRewards" + ]; + } else if (RewardInfo.T == 70) { + // Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path. + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem", + ItemCount: mission?.Tier == 1 ? 15 : 10 + }); + rewardManifests = []; } else { rewardManifests = region.rewardManifests; } @@ -1721,3 +1860,55 @@ const libraryPersonalTargetToAvatar: Record = { "/Lotus/Types/Game/Library/Targets/Research10Target": "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar" }; + +const node_excluded_buddies: Record = { + SolNode856: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", + SolNode852: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", + SolNode851: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", + SolNode850: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", + SolNode853: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", + SolNode854: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue" +}; + +const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } => { + // We're gonna shuffle these arrays, so they're not truly 'const'. + const nodes: string[] = [ + "SolNode850", + "SolNode851", + "SolNode852", + "SolNode853", + "SolNode854", + "SolNode856", + "SolNode858" + ]; + const excludable_nodes: string[] = ["SolNode851", "SolNode852", "SolNode853", "SolNode854"]; + const buddies: string[] = [ + "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue" + ]; + + const rng = new SRng(seed); + rng.shuffleArray(nodes); + rng.shuffleArray(excludable_nodes); + while (nodes.length > buddies.length) { + nodes.splice( + nodes.findIndex(x => x == excludable_nodes[0]), + 1 + ); + excludable_nodes.splice(0, 1); + } + rng.shuffleArray(buddies); + for (let i = 0; i != 6; ++i) { + if (buddies[i] == node_excluded_buddies[nodes[i]]) { + const swapIdx = (i + 1) % buddies.length; + const tmp = buddies[swapIdx]; + buddies[swapIdx] = buddies[i]; + buddies[i] = tmp; + } + } + return { nodes, buddies }; +}; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 38cfd3d1..59a431c3 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -9,7 +9,7 @@ import { updateSlots } from "@/src/services/inventoryService"; import { getRandomWeightedRewardUc } from "@/src/services/rngService"; -import { getVendorManifestByOid } from "@/src/services/serversideVendorsService"; +import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; @@ -53,8 +53,9 @@ export const handlePurchase = async ( const prePurchaseInventoryChanges: IInventoryChanges = {}; let seed: bigint | undefined; if (purchaseRequest.PurchaseParams.Source == 7) { - const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); + let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); if (manifest) { + manifest = applyStandingToVendorManifest(inventory, manifest); let ItemId: string | undefined; if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) @@ -92,7 +93,7 @@ export const handlePurchase = async ( if (!config.noVendorPurchaseLimits && ItemId) { inventory.RecentVendorPurchases ??= []; let vendorPurchases = inventory.RecentVendorPurchases.find( - x => x.VendorType == manifest.VendorInfo.TypeName + x => x.VendorType == manifest!.VendorInfo.TypeName ); if (!vendorPurchases) { vendorPurchases = diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 597ae01b..72379ab0 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -115,4 +115,19 @@ export class SRng { randomReward(pool: T[]): T | undefined { return getRewardAtPercentage(pool, this.randomFloat()); } + + churnSeed(its: number): void { + while (its--) { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + } + } + + shuffleArray(arr: T[]): void { + for (let lastIdx = arr.length - 1; lastIdx >= 1; --lastIdx) { + const swapIdx = this.randomInt(0, lastIdx); + const tmp = arr[swapIdx]; + arr[swapIdx] = arr[lastIdx]; + arr[lastIdx] = tmp; + } + } } diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index af7d0b91..aa6685a6 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,9 +1,10 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { catBreadHash } from "@/src/helpers/stringHelpers"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { mixSeeds, SRng } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; -import { ExportVendors, IRange } from "warframe-public-export-plus"; +import { ExportVendors, IRange, IVendor } from "warframe-public-export-plus"; import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; @@ -81,12 +82,6 @@ const generatableVendors: IGeneratableVendorInfo[] = [ WeaponUpgradeValueAttenuationExponent: 2.25, cycleOffset: 1744934400_000, cycleDuration: 4 * unixTimesInMs.day - }, - { - _id: { $oid: "61ba123467e5d37975aeeb03" }, - TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", - RandomSeedType: "VRST_FLAVOUR_TEXT", - cycleDuration: unixTimesInMs.week // TODO: Auto-detect this based on the items, so we don't need to specify it explicitly. } // { // _id: { $oid: "5dbb4c41e966f7886c3ce939" }, @@ -98,6 +93,25 @@ const getVendorOid = (typeName: string): string => { return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0"); }; +// https://stackoverflow.com/a/17445304 +const gcd = (a: number, b: number): number => { + return b ? gcd(b, a % b) : a; +}; + +const getCycleDuration = (manifest: IVendor): number => { + let dur = 0; + for (const item of manifest.items) { + if (typeof item.durationHours != "number") { + dur = 1; + break; + } + if (dur != item.durationHours) { + dur = gcd(dur, item.durationHours); + } + } + return dur * unixTimesInMs.hour; +}; + export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo.TypeName == typeName) { @@ -110,11 +124,12 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | } } if (typeName in ExportVendors) { + const manifest = ExportVendors[typeName]; return generateVendorManifest({ _id: { $oid: getVendorOid(typeName) }, TypeName: typeName, - RandomSeedType: ExportVendors[typeName].randomSeedType, - cycleDuration: unixTimesInMs.hour + RandomSeedType: manifest.randomSeedType, + cycleDuration: getCycleDuration(manifest) }); } return undefined; @@ -138,13 +153,50 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined _id: { $oid: typeNameOid }, TypeName: typeName, RandomSeedType: manifest.randomSeedType, - cycleDuration: unixTimesInMs.hour + cycleDuration: getCycleDuration(manifest) }); } } return undefined; }; +export const applyStandingToVendorManifest = ( + inventory: TInventoryDatabaseDocument, + vendorManifest: IVendorManifest +): IVendorManifest => { + return { + VendorInfo: { + ...vendorManifest.VendorInfo, + ItemManifest: [...vendorManifest.VendorInfo.ItemManifest].map(offer => { + if (offer.Affiliation && offer.ReductionPerPositiveRank && offer.IncreasePerNegativeRank) { + const title: number = inventory.Affiliations.find(x => x.Tag == offer.Affiliation)?.Title ?? 0; + const factor = + 1 + (title < 0 ? offer.IncreasePerNegativeRank : offer.ReductionPerPositiveRank) * title * -1; + //console.log(offer.Affiliation, title, factor); + if (factor) { + offer = { ...offer }; + if (offer.RegularPrice) { + offer.RegularPriceBeforeDiscount = offer.RegularPrice; + offer.RegularPrice = [ + Math.trunc(offer.RegularPriceBeforeDiscount[0] * factor), + Math.trunc(offer.RegularPriceBeforeDiscount[1] * factor) + ]; + } + if (offer.ItemPrices) { + offer.ItemPricesBeforeDiscount = offer.ItemPrices; + offer.ItemPrices = []; + for (const item of offer.ItemPricesBeforeDiscount) { + offer.ItemPrices.push({ ...item, ItemCount: Math.trunc(item.ItemCount * factor) }); + } + } + } + } + return offer; + }) + } + }; +}; + const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => { if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { const manifest = structuredClone(originalManifest); @@ -176,24 +228,27 @@ const toRange = (value: IRange | number): IRange => { return value; }; -const vendorInfoCache: Record = {}; +const vendorManifestCache: Record = {}; const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => { - if (!(vendorInfo.TypeName in vendorInfoCache)) { + if (!(vendorInfo.TypeName in vendorManifestCache)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; - vendorInfoCache[vendorInfo.TypeName] = { - ...clientVendorInfo, - ItemManifest: [], - Expiry: { $date: { $numberLong: "0" } } + vendorManifestCache[vendorInfo.TypeName] = { + VendorInfo: { + ...clientVendorInfo, + ItemManifest: [], + Expiry: { $date: { $numberLong: "0" } } + } }; } - const processed = vendorInfoCache[vendorInfo.TypeName]; - if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) { + const cacheEntry = vendorManifestCache[vendorInfo.TypeName]; + const info = cacheEntry.VendorInfo; + if (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) { // Remove expired offers - for (let i = 0; i != processed.ItemManifest.length; ) { - if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) { - processed.ItemManifest.splice(i, 1); + for (let i = 0; i != info.ItemManifest.length; ) { + if (Date.now() >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) { + info.ItemManifest.splice(i, 1); } else { ++i; } @@ -207,9 +262,14 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani const rng = new SRng(mixSeeds(vendorSeed, cycleIndex)); const manifest = ExportVendors[vendorInfo.TypeName]; const offersToAdd = []; - if (manifest.numItems && !manifest.isOneBinPerCycle) { + if ( + manifest.numItems && + (manifest.numItems.minValue != manifest.numItems.maxValue || + manifest.items.length != manifest.numItems.minValue) && + !manifest.isOneBinPerCycle + ) { const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue); - while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) { + while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) { // TODO: Consider per-bin item limits // TODO: Consider item probability weightings offersToAdd.push(rng.randomElement(manifest.items)!); @@ -288,20 +348,18 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn); } } - processed.ItemManifest.push(item); + info.ItemManifest.push(item); } // Update vendor expiry let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; - for (const offer of processed.ItemManifest) { + for (const offer of info.ItemManifest) { const offerExpiry = parseInt(offer.Expiry.$date.$numberLong); if (soonestOfferExpiry > offerExpiry) { soonestOfferExpiry = offerExpiry; } } - processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString(); + info.Expiry.$date.$numberLong = soonestOfferExpiry.toString(); } - return { - VendorInfo: processed - }; + return cacheEntry; }; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index b18696cc..32cac658 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -296,7 +296,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu SortieRewardAttenuation?: ISortieRewardAttenuation[]; Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; - ActiveAvatarImageType: string; + ActiveAvatarImageType?: string; ShipDecorations: ITypeCount[]; DiscoveredMarkers: IDiscoveredMarker[]; //CompletedJobs: ICompletedJob[]; @@ -863,6 +863,8 @@ export interface IMission extends IMissionDatabase { RewardsCooldownTime?: IMongoDate; } +export type TNemesisFaction = "FC_GRINEER" | "FC_CORPUS" | "FC_INFESTATION"; + export interface INemesisBaseClient { fp: bigint | number; manifest: string; @@ -872,7 +874,7 @@ export interface INemesisBaseClient { WeaponIdx: number; AgentIdx: number; BirthNode: string; - Faction: string; + Faction: TNemesisFaction; Rank: number; k: boolean; Traded: boolean; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index ab895bd1..6fdd0c09 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -177,6 +177,7 @@ export interface IRewardInfo { PurgatoryRewardQualifications?: string; rewardSeed?: number | bigint; periodicMissionTag?: string; + T?: number; // Duviri ConquestType?: string; ConquestCompleted?: number; ConquestEquipmentSuggestionsFulfilled?: number; diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index 2976ce07..9249a9a0 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -15,10 +15,16 @@ export interface IItemManifest { QuantityMultiplier: number; Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. PurchaseQuantityLimit?: number; + Affiliation?: string; + MinAffiliationRank?: number; + ReductionPerPositiveRank?: number; + IncreasePerNegativeRank?: number; RotatedWeekly?: boolean; AllowMultipurchase: boolean; LocTagRandSeed?: number | bigint; Id: IOid; + RegularPriceBeforeDiscount?: number[]; + ItemPricesBeforeDiscount?: IItemPrice[]; } export interface IVendorInfo {