From 8ebb7497327370d6f40bfa25c1858cde11c2c809 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Mon, 14 Apr 2025 11:45:00 -0700 Subject: [PATCH 01/32] chore(webui): update to Spanish translation (#1636) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1636 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index b918d6c0..2c303d80 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -139,7 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, - cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, + cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`, cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`, cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`, cheats_fastDojoRoomDestruction: `Destrucción rápida de salas del dojo`, From 43f3917b0948a4c5e714734887a07299f367b56e Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:10:25 -0700 Subject: [PATCH 02/32] fix: additional checks in bounty rewards (#1626) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1626 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b506b37b..dd751926 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -922,7 +922,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (syndicateEntry.Tag === "EntratiSyndicate") { const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); - if (vault) job = vault; + if (vault && locationTag) job = vault; // if ( // [ // "DeimosRuinsExterminateBounty", @@ -997,8 +997,10 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) && !isEndlessJob ) { - rewardManifests.push(job.rewards); - rotations.push(ExportRewards[job.rewards].length - 1); + if (ExportRewards[job.rewards]) { + rewardManifests.push(job.rewards); + rotations.push(ExportRewards[job.rewards].length - 1); + } } } } @@ -1053,17 +1055,20 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (rewardManifests.length != 0) { logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); } - rewardManifests - .map(name => ExportRewards[name]) - .forEach(table => { - for (const rotation of rotations) { - const rotationRewards = table[rotation]; - const drop = getRandomRewardByChance(rotationRewards); - if (drop) { - drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); - } + rewardManifests.forEach(name => { + const table = ExportRewards[name]; + if (!table) { + logger.error(`unknown droptable: ${name}`); + return; + } + for (const rotation of rotations) { + const rotationRewards = table[rotation]; + const drop = getRandomRewardByChance(rotationRewards); + if (drop) { + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } - }); + } + }); if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) { const deck = ExportRewards[region.cacheRewardManifest]; From fa68a1357dc94e11e59ac767c57dfbc17cd291f3 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Tue, 15 Apr 2025 06:15:33 -0700 Subject: [PATCH 03/32] chore(webui): update to German translation (#1642) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1642 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 047b3b7d..7cca988a 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -139,7 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, - cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, + cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, From bd837381689cc6e87b30053c9bb2727b8d9672b3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:15:49 -0700 Subject: [PATCH 04/32] fix: provide a response to setPlacedDecoInfo (#1632) This seems to be needed for the client when refreshing the ship after loading into a mission as it does not resync the ship otherwise. Closes #1629 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1632 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/setPlacedDecoInfoController.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/setPlacedDecoInfoController.ts b/src/controllers/api/setPlacedDecoInfoController.ts index 56b9afe7..19f76061 100644 --- a/src/controllers/api/setPlacedDecoInfoController.ts +++ b/src/controllers/api/setPlacedDecoInfoController.ts @@ -1,5 +1,5 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; +import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; import { RequestHandler } from "express"; import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService"; @@ -7,5 +7,17 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest; await handleSetPlacedDecoInfo(accountId, payload); - res.end(); + res.json({ + DecoId: payload.DecoId, + IsPicture: true, + PictureFrameInfo: payload.PictureFrameInfo, + BootLocation: payload.BootLocation + } satisfies ISetPlacedDecoInfoResponse); }; + +interface ISetPlacedDecoInfoResponse { + DecoId: string; + IsPicture: boolean; + PictureFrameInfo?: IPictureFrameInfo; + BootLocation?: string; +} From 380f0662a407f33b38f42c57d1949d8eb2202851 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:16:07 -0700 Subject: [PATCH 05/32] fix: don't try to subtract MiscItems for polarity swap (#1633) Closes #1620 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1633 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/upgradesController.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts index 855d2aa7..2a376b4a 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -25,7 +25,13 @@ export const upgradesController: RequestHandler = async (req, res) => { operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker" ) { updateCurrency(inventory, 10, true); - } else if (operation.OperationType != "UOT_ABILITY_OVERRIDE") { + } else if ( + operation.OperationType != "UOT_SWAP_POLARITY" && + operation.OperationType != "UOT_ABILITY_OVERRIDE" + ) { + if (!operation.UpgradeRequirement) { + throw new Error(`${operation.OperationType} operation should be free?`); + } addMiscItems(inventory, [ { ItemType: operation.UpgradeRequirement, From 0c884576bd7cc2bdf5a37bff57484c88ce06d6dd Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:16:19 -0700 Subject: [PATCH 06/32] feat: picking up prex cards (#1634) Closes #1621 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1634 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../giveShipDecoAndLoreFragmentController.ts | 20 +++++++++++++++++++ src/routes/api.ts | 2 ++ src/services/inventoryService.ts | 14 ++++++++++++- src/services/missionInventoryUpdateService.ts | 10 ++-------- 4 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 src/controllers/api/giveShipDecoAndLoreFragmentController.ts diff --git a/src/controllers/api/giveShipDecoAndLoreFragmentController.ts b/src/controllers/api/giveShipDecoAndLoreFragmentController.ts new file mode 100644 index 00000000..08385cbf --- /dev/null +++ b/src/controllers/api/giveShipDecoAndLoreFragmentController.ts @@ -0,0 +1,20 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations"); + const data = getJSONfromString(String(req.body)); + addLoreFragmentScans(inventory, data.LoreFragmentScans); + addShipDecorations(inventory, data.ShipDecorations); + await inventory.save(); + res.end(); +}; + +interface IGiveShipDecoAndLoreFragmentRequest { + LoreFragmentScans: ILoreFragmentScan[]; + ShipDecorations: ITypeCount[]; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index bd3ffd4b..1c69da55 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -62,6 +62,7 @@ import { gildWeaponController } from "@/src/controllers/api/gildWeaponController import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey"; +import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController"; import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; import { guildTechController } from "@/src/controllers/api/guildTechController"; import { hostSessionController } from "@/src/controllers/api/hostSessionController"; @@ -239,6 +240,7 @@ apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController); apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController); +apiRouter.post("/giveShipDecoAndLoreFragment.php", giveShipDecoAndLoreFragmentController); apiRouter.post("/giveStartingGear.php", giveStartingGearController); apiRouter.post("/guildTech.php", guildTechController); apiRouter.post("/hostSession.php", hostSessionController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 6357d404..48657e88 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -21,7 +21,8 @@ import { ICalendarProgress, IDroneClient, IUpgradeClient, - TPartialStartingGear + TPartialStartingGear, + ILoreFragmentScan } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -1350,6 +1351,17 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); }; +export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: ILoreFragmentScan[]): void => { + arr.forEach(clientFragment => { + const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); + if (fragment) { + fragment.Progress += clientFragment.Progress; + } else { + inventory.LoreFragmentScans.push(clientFragment); + } + }); +}; + export const addChallenges = ( inventory: TInventoryDatabaseDocument, ChallengeProgress: IChallengeProgress[], diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index dd751926..44923250 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -23,6 +23,7 @@ import { addGearExpByCategory, addItem, addLevelKeys, + addLoreFragmentScans, addMiscItems, addMissionComplete, addMods, @@ -291,14 +292,7 @@ export const addMissionInventoryUpdates = async ( break; } case "LoreFragmentScans": - value.forEach(clientFragment => { - const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); - if (fragment) { - fragment.Progress += clientFragment.Progress; - } else { - inventory.LoreFragmentScans.push(clientFragment); - } - }); + addLoreFragmentScans(inventory, value); break; case "LibraryScans": value.forEach(scan => { From a6d2c8b18afeb81947545f256f20c802faea35bc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:16:31 -0700 Subject: [PATCH 07/32] fix: don't give credits for junctions, the index, and free flight (#1635) Closes #1625 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1635 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 44923250..7fab571b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -586,7 +586,14 @@ export const addMissionRewards = async ( const node = ExportRegions[missions.Tag]; //node based credit rewards for mission completion - if (node.missionIndex !== 28) { + if ( + node.missionIndex != 23 && // junction + node.missionIndex != 28 && // open world + missions.Tag != "SolNode761" && // the index + missions.Tag != "SolNode762" && // the index + missions.Tag != "SolNode763" && // the index + missions.Tag != "CrewBattleNode556" // free flight + ) { const levelCreditReward = getLevelCreditRewards(node); missionCompletionCredits += levelCreditReward; inventory.RegularCredits += levelCreditReward; From d28437b6588184f8bdcd59175d888268561fd502 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:16:40 -0700 Subject: [PATCH 08/32] feat: give 5 steel essence when completing an SP incursion (#1637) Closes #1631 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1637 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 7fab571b..b74c1ddf 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -899,6 +899,12 @@ function getLevelCreditRewards(node: IRegion): number { function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] { const drops: IMissionReward[] = []; + if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) { + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence", + ItemCount: 5 + }); + } if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; let rewardManifests: string[] = From 3f0a2bec48c4a3e8790983f45d7ad457396c7f96 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:46:08 -0700 Subject: [PATCH 09/32] fix: generate rewards based on RewardSeed to match what's show in client (#1628) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1628 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/getNewRewardSeedController.ts | 2 -- src/models/inventoryModels/inventoryModel.ts | 2 +- src/services/missionInventoryUpdateService.ts | 18 +++++++++++++++--- src/services/rngService.ts | 6 +++++- src/types/inventoryTypes/inventoryTypes.ts | 2 +- src/types/requestTypes.ts | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/getNewRewardSeedController.ts b/src/controllers/api/getNewRewardSeedController.ts index 4ff82405..cb9e1f82 100644 --- a/src/controllers/api/getNewRewardSeedController.ts +++ b/src/controllers/api/getNewRewardSeedController.ts @@ -1,14 +1,12 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { generateRewardSeed } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; export const getNewRewardSeedController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const rewardSeed = generateRewardSeed(); - logger.debug(`generated new reward seed: ${rewardSeed}`); await Inventory.updateOne( { accountOwnerId: accountId diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a0eddb10..9c737d24 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1213,7 +1213,7 @@ const inventorySchema = new Schema( accountOwnerId: Schema.Types.ObjectId, SubscribedToEmails: { type: Number, default: 0 }, SubscribedToEmailsPersonalized: { type: Number, default: 0 }, - RewardSeed: Number, + RewardSeed: BigInt, //Credit RegularCredits: { type: Number, default: 0 }, diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b74c1ddf..813e7966 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -9,7 +9,7 @@ import { } from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService"; +import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService"; import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { addBooster, @@ -31,6 +31,7 @@ import { addShipDecorations, addStanding, combineInventoryChanges, + generateRewardSeed, updateCurrency, updateSyndicate } from "@/src/services/inventoryService"; @@ -70,7 +71,12 @@ const getRotations = (rotationCount: number, tierOverride: number | undefined): return rotatedValues; }; -const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { +const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | undefined => { + if (rng) { + const res = rng.randomReward(pool as IRngResult[]); + rng.randomFloat(); // something related to rewards multiplier + return res; + } return getRandomReward(pool as IRngResult[]); }; @@ -548,6 +554,11 @@ export const addMissionRewards = async ( return { MissionRewards: [] }; } + if (rewardInfo.rewardSeed) { + // We're using a reward seed, so give the client a new one in the response. On live, missionInventoryUpdate seems to always provide a fresh one in the response. + inventory.RewardSeed = generateRewardSeed(); + } + //TODO: check double reward merging const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier); logger.debug("random mission drops:", MissionRewards); @@ -1062,6 +1073,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (rewardManifests.length != 0) { logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); } + const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn); rewardManifests.forEach(name => { const table = ExportRewards[name]; if (!table) { @@ -1070,7 +1082,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u } for (const rotation of rotations) { const rotationRewards = table[rotation]; - const drop = getRandomRewardByChance(rotationRewards); + const drop = getRandomRewardByChance(rotationRewards, rng); if (drop) { drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 983977ce..f88b23c0 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -31,7 +31,7 @@ const getRewardAtPercentage = (pool: T[], per return item; } } - throw new Error("What the fuck?"); + return pool[pool.length - 1]; }; export const getRandomReward = (pool: T[]): T | undefined => { @@ -142,4 +142,8 @@ export class SRng { this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645; } + + randomReward(pool: T[]): T | undefined { + return getRewardAtPercentage(pool, this.randomFloat()); + } } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 1146bef6..63207ef0 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -194,7 +194,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Mailbox?: IMailboxClient; SubscribedToEmails: number; Created: IMongoDate; - RewardSeed: number; + RewardSeed: number | bigint; RegularCredits: number; PremiumCredits: number; PremiumCreditsFree: number; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 494e23f4..fff164a8 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -141,7 +141,7 @@ export interface IRewardInfo { EOM_AFK?: number; rewardQualifications?: string; // did a Survival for 5 minutes and this was "1" PurgatoryRewardQualifications?: string; - rewardSeed?: number; + rewardSeed?: number | bigint; periodicMissionTag?: string; // for bounties, only EOM_AFK and node are given from above, plus: From 28d7ca8ca0f24f7ebf05f403f806ecd9fd3dd0e7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:48:17 +0200 Subject: [PATCH 10/32] chore: address eslint warnings --- src/services/missionInventoryUpdateService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 813e7966..2a9dda9a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1015,6 +1015,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) && !isEndlessJob ) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ExportRewards[job.rewards]) { rewardManifests.push(job.rewards); rotations.push(ExportRewards[job.rewards].length - 1); @@ -1076,6 +1077,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn); rewardManifests.forEach(name => { const table = ExportRewards[name]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!table) { logger.error(`unknown droptable: ${name}`); return; From 3165d9f459f586dcb23a778bc897e03d71b06e50 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 10:47:38 -0700 Subject: [PATCH 11/32] fix: respect rewardTier for rescue missions (#1650) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1650 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 25 +++++++++++++------ src/types/requestTypes.ts | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2a9dda9a..87a17190 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -55,7 +55,22 @@ import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { getWorldState } from "./worldStateService"; -const getRotations = (rotationCount: number, tierOverride: number | undefined): number[] => { +const getRotations = (rewardInfo: IRewardInfo, tierOverride: number | undefined): number[] => { + // For Spy missions, e.g. 3 vaults cracked = A, B, C + if (rewardInfo.VaultsCracked) { + const rotations: number[] = []; + for (let i = 0; i != rewardInfo.VaultsCracked; ++i) { + rotations.push(i); + } + return rotations; + } + + // For Rescue missions + if (rewardInfo.rewardTier) { + return [rewardInfo.rewardTier]; + } + + const rotationCount = rewardInfo.rewardQualifications?.length || 0; if (rotationCount === 0) return [0]; const rotationPattern = @@ -1062,14 +1077,8 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u } else { logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`); } - } else if (RewardInfo.VaultsCracked) { - // For Spy missions, e.g. 3 vaults cracked = A, B, C - for (let i = 0; i != RewardInfo.VaultsCracked; ++i) { - rotations.push(i); - } } else { - const rotationCount = RewardInfo.rewardQualifications?.length || 0; - rotations = getRotations(rotationCount, tierOverride); + rotations = getRotations(RewardInfo, tierOverride); } if (rewardManifests.length != 0) { logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index fff164a8..77e7f322 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -130,7 +130,7 @@ export type IMissionInventoryUpdateRequest = { export interface IRewardInfo { node: string; VaultsCracked?: number; // for Spy missions - rewardTier?: number; + rewardTier?: number; // for Rescue missions nightmareMode?: boolean; useVaultManifest?: boolean; EnemyCachesFound?: number; From a10c3b061a47da2cbcfe68da7ac7b3043f442554 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:58:15 -0700 Subject: [PATCH 12/32] fix: respect VaultsCracked when rolling droptable for level key rewards (#1639) Fixes #1638 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1639 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 87a17190..2f78f65a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -55,7 +55,7 @@ import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { getWorldState } from "./worldStateService"; -const getRotations = (rewardInfo: IRewardInfo, tierOverride: number | undefined): number[] => { +const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { // For Spy missions, e.g. 3 vaults cracked = A, B, C if (rewardInfo.VaultsCracked) { const rotations: number[] = []; @@ -587,7 +587,7 @@ export const addMissionRewards = async ( const fixedLevelRewards = getLevelKeyRewards(levelKeyName); //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); if (fixedLevelRewards.levelKeyRewards) { - addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards); + addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards, rewardInfo); } if (fixedLevelRewards.levelKeyRewards2) { for (const reward of fixedLevelRewards.levelKeyRewards2) { @@ -627,7 +627,7 @@ export const addMissionRewards = async ( } if (node.missionReward) { - missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards); + missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo); } } @@ -870,7 +870,8 @@ export const addCredits = ( export const addFixedLevelRewards = ( rewards: IMissionRewardExternal, inventory: TInventoryDatabaseDocument, - MissionRewards: IMissionReward[] + MissionRewards: IMissionReward[], + rewardInfo?: IRewardInfo ): number => { let missionBonusCredits = 0; if (rewards.credits) { @@ -900,13 +901,16 @@ export const addFixedLevelRewards = ( } if (rewards.droptable) { if (rewards.droptable in ExportRewards) { - logger.debug(`rolling ${rewards.droptable} for level key rewards`); - const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][0]); - if (reward) { - MissionRewards.push({ - StoreItem: reward.type, - ItemCount: reward.itemCount - }); + const rotations: number[] = rewardInfo ? getRotations(rewardInfo) : [0]; + logger.debug(`rolling ${rewards.droptable} for level key rewards`, { rotations }); + for (const tier of rotations) { + const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][tier]); + if (reward) { + MissionRewards.push({ + StoreItem: reward.type, + ItemCount: reward.itemCount + }); + } } } else { logger.error(`unknown droptable ${rewards.droptable}`); From ea0ca8c88bc852e808402dbd6702d1b60c1c0e16 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 01:35:28 +0200 Subject: [PATCH 13/32] chore: fix file name for giveQuestKeyRewardController --- .../api/{giveQuestKey.ts => giveQuestKeyRewardController.ts} | 0 src/routes/api.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/controllers/api/{giveQuestKey.ts => giveQuestKeyRewardController.ts} (100%) diff --git a/src/controllers/api/giveQuestKey.ts b/src/controllers/api/giveQuestKeyRewardController.ts similarity index 100% rename from src/controllers/api/giveQuestKey.ts rename to src/controllers/api/giveQuestKeyRewardController.ts diff --git a/src/routes/api.ts b/src/routes/api.ts index 1c69da55..39b3d751 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -61,7 +61,7 @@ import { giftingController } from "@/src/controllers/api/giftingController"; import { gildWeaponController } from "@/src/controllers/api/gildWeaponController"; import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; -import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey"; +import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKeyRewardController"; import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController"; import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; import { guildTechController } from "@/src/controllers/api/guildTechController"; From 7a53363b1b3a458a715dde4452dae53c738835b2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 01:36:46 +0200 Subject: [PATCH 14/32] fix response of giveQuestKeyReward --- src/controllers/api/giveQuestKeyRewardController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/giveQuestKeyRewardController.ts b/src/controllers/api/giveQuestKeyRewardController.ts index 80846234..d74d56bf 100644 --- a/src/controllers/api/giveQuestKeyRewardController.ts +++ b/src/controllers/api/giveQuestKeyRewardController.ts @@ -16,7 +16,7 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) => const inventory = await getInventory(accountId); const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount); await inventory.save(); - res.json(inventoryChanges.InventoryChanges); + res.json(inventoryChanges); //TODO: consider whishlist changes }; From 47551e93b3fb9e74c4671e4915e2346acee21d7f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 03:11:21 +0200 Subject: [PATCH 15/32] feat(webui): add everything in ExportCustoms as an "add items" option --- src/controllers/custom/getItemListsController.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 63bf949f..38e42bdb 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService" import { ExportArcanes, ExportAvionics, + ExportCustoms, ExportDrones, ExportGear, ExportKeys, @@ -171,6 +172,12 @@ const getItemListsController: RequestHandler = (req, response) => { name: getString(item.name, lang) }); } + for (const [uniqueName, item] of Object.entries(ExportCustoms)) { + res.miscitems.push({ + uniqueName: uniqueName, + name: getString(item.name, lang) + }); + } res.mods = []; for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { From 64fbdf60643ac2306b6ffd78d904668eb2cdd0b3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:11:13 -0700 Subject: [PATCH 16/32] fix: put house version railjack components into the salvage array (#1654) Fixes #1645 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1654 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/inventoryService.ts | 26 +++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a858a5d0..e84d1c57 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.54", + "warframe-public-export-plus": "^0.5.55", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.54", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.54.tgz", - "integrity": "sha512-27r6qLErr3P8UVDiEzhDAs/BjdAS3vI2CQ58jSI+LClDlj6QL+y1jQe8va/npl3Ft2K8PywLkZ8Yso0j9YzvOA==" + "version": "0.5.55", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.55.tgz", + "integrity": "sha512-Gnd4FCBVuxm2xWGfu8xxxqPIPSnnTqiEWlpP3rsFpVlQs09RNFnW2PEX9rCZt0f3SvHBv5ssDDrFlzgBHS1yrA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 008d5a18..58b231d3 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.54", + "warframe-public-export-plus": "^0.5.55", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 48657e88..fe35cff6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -408,8 +408,32 @@ export const addItem = async ( const meta = ExportCustoms[typeName]; let inventoryChanges: IInventoryChanges; if (meta.productCategory == "CrewShipWeaponSkins") { - inventoryChanges = addCrewShipWeaponSkin(inventory, typeName); + if (meta.subroutines || meta.randomisedUpgrades) { + // House versions need to be identified to get stats so put them into raw salvage first. + const rawSalvageChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addCrewShipRawSalvage(inventory, rawSalvageChanges); + inventoryChanges = { CrewShipRawSalvage: rawSalvageChanges }; + } else { + // Sigma versions can be added directly. + if (quantity != 1) { + throw new Error( + `unexpected acquisition quantity of CrewShipWeaponSkin: got ${quantity}, expected 1` + ); + } + inventoryChanges = { + ...addCrewShipWeaponSkin(inventory, typeName), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) + }; + } } else { + if (quantity != 1) { + throw new Error(`unexpected acquisition quantity of WeaponSkins: got ${quantity}, expected 1`); + } inventoryChanges = addSkin(inventory, typeName); } if (meta.additionalItems) { From eb6b1c1f57815939771425d50249c78767c8bac0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 03:40:21 +0200 Subject: [PATCH 17/32] chore: fix typo --- src/models/inventoryModels/inventoryModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 9c737d24..6b4b4959 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1613,7 +1613,7 @@ export type InventoryDocumentProps = { QuestKeys: Types.DocumentArray; Drones: Types.DocumentArray; CrewShipWeaponSkins: Types.DocumentArray; - CrewShipSalvagedWeaponsSkins: Types.DocumentArray; + CrewShipSalvagedWeaponSkins: Types.DocumentArray; PersonalTechProjects: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; From c13615c4df0fb13346b0a183463bbbf96b2617fe Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:41:46 -0700 Subject: [PATCH 18/32] fix: provide upcoming bounties in worldState when new cycle is imminent (#1657) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1657 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 471 +++++++++++++----- src/types/worldStateTypes.ts | 5 +- .../worldState/worldState.json | 242 --------- 3 files changed, 344 insertions(+), 374 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index dbe61652..15576cef 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -9,7 +9,6 @@ import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; import { ISeasonChallenge, IWorldState } from "../types/worldStateTypes"; -import { logger } from "../utils/logger"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -80,73 +79,76 @@ const sortieBossNode: Record = { SORTIE_BOSS_INFALAD: "SolNode705" }; -const jobSets: string[][] = [ - [ - "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt", - "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap", - "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft", - "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", - "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne", - "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", - "/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab", - "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc" - ], - [ - "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", - "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt", - "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft", - "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib" - ], - [ - "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy" - ], - [ - "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate", - "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", - "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense", - "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation" - ], - [ - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty" - ], - [ - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" - ] +const eidolonJobs = [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", + "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne", + "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", + "/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab", + "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc" +]; + +const eidolonNarmerJobs = [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib" +]; + +const venusJobs = [ + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy" +]; + +const venusNarmerJobs = [ + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation" +]; + +const microplanetJobs = [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty" +]; + +const microplanetEndlessJobs = [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" ]; const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 @@ -218,6 +220,10 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng }; }; +const expiresBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { + return Date.now() + 300_000 > date; +}; + export const getWorldState = (buildLabel?: string): IWorldState => { const day = Math.trunc((Date.now() - EPOCH) / 86400000); const week = Math.trunc(day / 7); @@ -228,9 +234,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => { BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel, Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), Goals: [], - GlobalUpgrades: [], + Alerts: [], Sorties: [], LiteSorties: [], + GlobalUpgrades: [], EndlessXpChoices: [], SeasonInfo: { Activation: { $date: { $numberLong: "1715796000000" } }, @@ -268,7 +275,8 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } ] }, - ...staticWorldState + ...staticWorldState, + SyndicateMissions: [...staticWorldState.SyndicateMissions] }; if (config.worldState?.starDays) { @@ -292,69 +300,272 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation - const bountyCycle = Math.trunc(Date.now() / 9000000); - const bountyCycleStart = bountyCycle * 9000000; - const bountyCycleEnd = bountyCycleStart + 9000000; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, - Tag: "ZarimanSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, - Tag: "EntratiLabSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, - Tag: "HexSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - for (const syndicateInfo of worldState.SyndicateMissions) { - if (syndicateInfo.Jobs && syndicateInfo.Seed != bountyCycle) { - syndicateInfo.Activation.$date.$numberLong = bountyCycleStart.toString(10); - syndicateInfo.Expiry.$date.$numberLong = bountyCycleEnd.toString(10); - syndicateInfo.Seed = bountyCycle; - logger.debug(`refreshing jobs for ${syndicateInfo.Tag}`); + let bountyCycle = Math.trunc(Date.now() / 9000000); + let bountyCycleEnd: number | undefined; + do { + const bountyCycleStart = bountyCycle * 9000000; + bountyCycleEnd = bountyCycleStart + 9000000; + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, + Tag: "ZarimanSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, + Tag: "EntratiLabSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "HexSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + + const table = String.fromCharCode(65 + (bountyCycle % 3)); + const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3)); + const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2)); + + // TODO: xpAmounts need to be calculated based on the jobType somehow? + + { const rng = new CRng(bountyCycle); - const table = String.fromCharCode(65 + (bountyCycle % 3)); - const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3)); - const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2)); - //console.log({ bountyCycleStart, bountyCycleEnd, table, vaultTable, deimosDTable }); - for (const jobInfo of syndicateInfo.Jobs) { - if (jobInfo.jobType) { - let found = false; - for (const jobSet of jobSets) { - if (jobSet.indexOf(jobInfo.jobType) != -1) { - jobInfo.jobType = rng.randomElement(jobSet); - // TODO: xpAmounts seems like it might need to differ depending on the job? - found = true; - break; - } + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000008" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "CetusSyndicate", + Seed: bountyCycle, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [430, 430, 430] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: [620, 620, 620] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: [670, 670, 670, 990] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: [570, 570, 570, 570, 1110] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [740, 740, 740, 740, 1450] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [840, 840, 840, 840, 1660] + }, + { + jobType: rng.randomElement(eidolonNarmerJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 50, + maxEnemyLevel: 70, + xpAmounts: [840, 840, 840, 840, 1650] } - if (!found) { - logger.warn(`no job set found for type ${jobInfo.jobType}`); - } - } - if (jobInfo.endless || jobInfo.isVault) { - jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${vaultTable}Rewards`); - } else if (jobInfo.rewards.startsWith("/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierD")) { - jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${deimosDTable}Rewards`); - } else if (!jobInfo.rewards.startsWith("/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierE")) { - jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${table}Rewards`); - } - } + ] + }); } - } + + { + const rng = new CRng(bountyCycle); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000025" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "SolarisSyndicate", + Seed: bountyCycle, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [340, 340, 340] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: [660, 660, 660] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: [610, 610, 610, 900] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: [600, 600, 600, 600, 1170] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [690, 690, 690, 690, 1350] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [840, 840, 840, 840, 1660] + }, + { + jobType: rng.randomElement(venusNarmerJobs), + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", + masteryReq: 0, + minEnemyLevel: 50, + maxEnemyLevel: 70, + xpAmounts: [780, 780, 780, 780, 1540] + } + ] + }); + } + + { + const rng = new CRng(bountyCycle); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000002" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "EntratiSyndicate", + Seed: bountyCycle, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [5, 5, 5] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [12, 12, 12] + }, + { + jobType: rng.randomElement(microplanetEndlessJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 25, + maxEnemyLevel: 30, + endless: true, + xpAmounts: [14, 14, 14] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`, + masteryReq: 2, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: [17, 17, 17, 25] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [22, 22, 22, 22, 43] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [25, 25, 25, 25, 50] + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: [2, 2, 2, 4], + locationTag: "ChamberB", + isVault: true + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 50, + xpAmounts: [4, 4, 4, 5], + locationTag: "ChamberA", + isVault: true + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 50, + maxEnemyLevel: 60, + xpAmounts: [5, 5, 5, 7], + locationTag: "ChamberC", + isVault: true + } + ] + }); + } + } while (expiresBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index a3b6efac..aaa292e4 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -5,10 +5,11 @@ export interface IWorldState { BuildLabel: string; Time: number; Goals: IGoal[]; - SyndicateMissions: ISyndicateMissionInfo[]; - GlobalUpgrades: IGlobalUpgrade[]; + Alerts: []; Sorties: ISortie[]; LiteSorties: ILiteSortie[]; + SyndicateMissions: ISyndicateMissionInfo[]; + GlobalUpgrades: IGlobalUpgrade[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; SeasonInfo: { diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index d9e228ae..3b4143e5 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -278,248 +278,6 @@ "Tag": "SteelMeridianSyndicate", "Seed": 42366, "Nodes": ["SolNode27", "SolNode107", "SolNode214", "SettlementNode1", "SolNode177", "SolNode141", "SolNode408"] - }, - { - "_id": { "$oid": "663a71c80000000000000002" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "EntratiSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [5, 5, 5] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 15, - "maxEnemyLevel": 25, - "xpAmounts": [12, 12, 12] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards", - "masteryReq": 5, - "minEnemyLevel": 25, - "maxEnemyLevel": 30, - "endless": true, - "xpAmounts": [14, 14, 14] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 30, - "maxEnemyLevel": 40, - "xpAmounts": [17, 17, 17, 25] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards", - "masteryReq": 3, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [22, 22, 22, 22, 43] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [25, 25, 25, 25, 50] - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATableCRewards", - "masteryReq": 5, - "minEnemyLevel": 30, - "maxEnemyLevel": 40, - "xpAmounts": [2, 2, 2, 4], - "locationTag": "ChamberB", - "isVault": true - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTableCRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 50, - "xpAmounts": [4, 4, 4, 5], - "locationTag": "ChamberA", - "isVault": true - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTableCRewards", - "masteryReq": 5, - "minEnemyLevel": 50, - "maxEnemyLevel": 60, - "xpAmounts": [5, 5, 5, 7], - "locationTag": "ChamberC", - "isVault": true - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000004" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "EntratiLabSyndicate", - "Seed": 99562, - "Nodes": [] - }, - { - "_id": { "$oid": "663a71c80000000000000008" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "CetusSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [430, 430, 430] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 10, - "maxEnemyLevel": 30, - "xpAmounts": [620, 620, 620] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 20, - "maxEnemyLevel": 40, - "xpAmounts": [670, 670, 670, 990] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTableBRewards", - "masteryReq": 3, - "minEnemyLevel": 30, - "maxEnemyLevel": 50, - "xpAmounts": [570, 570, 570, 570, 1110] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [740, 740, 740, 740, 1450] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [840, 840, 840, 840, 1660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", - "masteryReq": 0, - "minEnemyLevel": 50, - "maxEnemyLevel": 70, - "xpAmounts": [840, 840, 840, 840, 1650] - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000025" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "SolarisSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [340, 340, 340] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 10, - "maxEnemyLevel": 30, - "xpAmounts": [660, 660, 660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 20, - "maxEnemyLevel": 40, - "xpAmounts": [610, 610, 610, 900] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTableBRewards", - "masteryReq": 3, - "minEnemyLevel": 30, - "maxEnemyLevel": 50, - "xpAmounts": [600, 600, 600, 600, 1170] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [690, 690, 690, 690, 1350] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [840, 840, 840, 840, 1660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", - "masteryReq": 0, - "minEnemyLevel": 50, - "maxEnemyLevel": 70, - "xpAmounts": [780, 780, 780, 780, 1540] - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000029" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "ZarimanSyndicate", - "Seed": 99562, - "Nodes": [] - }, - { - "_id": { "$oid": "676b8d340000000000000006" }, - "Activation": { "$date": { "$numberLong": "1735101748215" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "HexSyndicate", - "Seed": 33872, - "Nodes": [] } ], "ActiveMissions": [ From 95562a97ad617505b2df50b608438e898445b087 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:53:27 -0700 Subject: [PATCH 19/32] fix: provide current & upcoming sortie if rollover is imminent (#1666) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1666 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 276 +++++++++++++++--------------- 1 file changed, 136 insertions(+), 140 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 15576cef..5137b5ed 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -8,7 +8,7 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; -import { ISeasonChallenge, IWorldState } from "../types/worldStateTypes"; +import { ISeasonChallenge, ISortie, IWorldState } from "../types/worldStateTypes"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -153,6 +153,10 @@ const microplanetEndlessJobs = [ const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 +const isBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { + return Date.now() + 300_000 > date; +}; + const getSortieTime = (day: number): number => { const dayStart = EPOCH + day * 86400000; const date = new Date(dayStart); @@ -167,6 +171,134 @@ const getSortieTime = (day: number): number => { return dayStart + (isDst ? 16 : 17) * 3600000; }; +const pushSortieIfRelevant = (out: ISortie[], day: number): void => { + const dayStart = getSortieTime(day); + if (!isBeforeNextExpectedWorldStateRefresh(dayStart)) { + return; + } + const dayEnd = getSortieTime(day + 1); + if (Date.now() >= dayEnd) { + return; + } + + const rng = new CRng(day); + + const boss = rng.randomElement(sortieBosses); + + const modifiers = [ + "SORTIE_MODIFIER_LOW_ENERGY", + "SORTIE_MODIFIER_IMPACT", + "SORTIE_MODIFIER_SLASH", + "SORTIE_MODIFIER_PUNCTURE", + "SORTIE_MODIFIER_EXIMUS", + "SORTIE_MODIFIER_MAGNETIC", + "SORTIE_MODIFIER_CORROSIVE", + "SORTIE_MODIFIER_VIRAL", + "SORTIE_MODIFIER_ELECTRICITY", + "SORTIE_MODIFIER_RADIATION", + "SORTIE_MODIFIER_GAS", + "SORTIE_MODIFIER_FIRE", + "SORTIE_MODIFIER_EXPLOSION", + "SORTIE_MODIFIER_FREEZE", + "SORTIE_MODIFIER_TOXIN", + "SORTIE_MODIFIER_POISON", + "SORTIE_MODIFIER_HAZARD_RADIATION", + "SORTIE_MODIFIER_HAZARD_MAGNETIC", + "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest + "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon + "SORTIE_MODIFIER_HAZARD_ICE", + "SORTIE_MODIFIER_HAZARD_COLD", + "SORTIE_MODIFIER_SECONDARY_ONLY", + "SORTIE_MODIFIER_SHOTGUN_ONLY", + "SORTIE_MODIFIER_SNIPER_ONLY", + "SORTIE_MODIFIER_RIFLE_ONLY", + "SORTIE_MODIFIER_MELEE_ONLY", + "SORTIE_MODIFIER_BOW_ONLY" + ]; + + if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); + if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); + + const nodes: string[] = []; + const availableMissionIndexes: number[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && + sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 && // Exclude MT_ASSASSINATION + value.missionIndex != 5 && // Exclude MT_CAPTURE + value.missionIndex != 21 && // Exclude MT_PURIFY + value.missionIndex != 23 && // Exclude MT_JUNCTION + value.missionIndex <= 28 + ) { + if (!availableMissionIndexes.includes(value.missionIndex)) { + availableMissionIndexes.push(value.missionIndex); + } + nodes.push(key); + } + } + + const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; + const missionTypes = new Set(); + + for (let i = 0; i < 3; i++) { + const randomIndex = rng.randomInt(0, nodes.length - 1); + const node = nodes[randomIndex]; + let missionIndex = ExportRegions[node].missionIndex; + + if ( + !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions + missionIndex != 28 && + rng.randomInt(0, 2) == 2 + ) { + missionIndex = rng.randomElement(availableMissionIndexes); + } + + if (i == 2 && rng.randomInt(0, 2) == 2) { + const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); + const modifierType = rng.randomElement(filteredModifiers); + + if (boss == "SORTIE_BOSS_PHORID") { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); + nodes.splice(randomIndex, 1); + continue; + } else if (sortieBossNode[boss]) { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); + continue; + } + } + + const missionType = eMissionType[missionIndex].tag; + + if (missionTypes.has(missionType)) { + i--; + continue; + } + + const filteredModifiers = + missionType === "MT_TERRITORY" + ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") + : modifiers; + + const modifierType = rng.randomElement(filteredModifiers); + + selectedNodes.push({ missionType, modifierType, node }); + nodes.splice(randomIndex, 1); + missionTypes.add(missionType); + } + + out.push({ + _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", + Seed: day, + Boss: boss, + Variants: selectedNodes + }); +}; + const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/") ); @@ -220,10 +352,6 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng }; }; -const expiresBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { - return Date.now() + 300_000 > date; -}; - export const getWorldState = (buildLabel?: string): IWorldState => { const day = Math.trunc((Date.now() - EPOCH) / 86400000); const week = Math.trunc(day / 7); @@ -565,7 +693,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { ] }); } - } while (expiresBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); + } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ @@ -605,140 +733,8 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // Sortie cycling every day - { - let genDay; - let dayStart; - let dayEnd; - const sortieRolloverToday = getSortieTime(day); - if (Date.now() < sortieRolloverToday) { - // Early in the day, generate sortie for `day - 1`, expiring at `sortieRolloverToday`. - genDay = day - 1; - dayStart = getSortieTime(genDay); - dayEnd = sortieRolloverToday; - } else { - // Late in the day, generate sortie for `day`, expiring at `getSortieTime(day + 1)`. - genDay = day; - dayStart = sortieRolloverToday; - dayEnd = getSortieTime(day + 1); - } - - const rng = new CRng(genDay); - - const boss = rng.randomElement(sortieBosses); - - const modifiers = [ - "SORTIE_MODIFIER_LOW_ENERGY", - "SORTIE_MODIFIER_IMPACT", - "SORTIE_MODIFIER_SLASH", - "SORTIE_MODIFIER_PUNCTURE", - "SORTIE_MODIFIER_EXIMUS", - "SORTIE_MODIFIER_MAGNETIC", - "SORTIE_MODIFIER_CORROSIVE", - "SORTIE_MODIFIER_VIRAL", - "SORTIE_MODIFIER_ELECTRICITY", - "SORTIE_MODIFIER_RADIATION", - "SORTIE_MODIFIER_GAS", - "SORTIE_MODIFIER_FIRE", - "SORTIE_MODIFIER_EXPLOSION", - "SORTIE_MODIFIER_FREEZE", - "SORTIE_MODIFIER_TOXIN", - "SORTIE_MODIFIER_POISON", - "SORTIE_MODIFIER_HAZARD_RADIATION", - "SORTIE_MODIFIER_HAZARD_MAGNETIC", - "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest - "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon - "SORTIE_MODIFIER_HAZARD_ICE", - "SORTIE_MODIFIER_HAZARD_COLD", - "SORTIE_MODIFIER_SECONDARY_ONLY", - "SORTIE_MODIFIER_SHOTGUN_ONLY", - "SORTIE_MODIFIER_SNIPER_ONLY", - "SORTIE_MODIFIER_RIFLE_ONLY", - "SORTIE_MODIFIER_MELEE_ONLY", - "SORTIE_MODIFIER_BOW_ONLY" - ]; - - if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); - if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); - - const nodes: string[] = []; - const availableMissionIndexes: number[] = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && - sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && - value.name.indexOf("Archwing") == -1 && - value.missionIndex != 0 && // Exclude MT_ASSASSINATION - value.missionIndex != 5 && // Exclude MT_CAPTURE - value.missionIndex != 21 && // Exclude MT_PURIFY - value.missionIndex != 23 && // Exclude MT_JUNCTION - value.missionIndex <= 28 - ) { - if (!availableMissionIndexes.includes(value.missionIndex)) { - availableMissionIndexes.push(value.missionIndex); - } - nodes.push(key); - } - } - - const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; - const missionTypes = new Set(); - - for (let i = 0; i < 3; i++) { - const randomIndex = rng.randomInt(0, nodes.length - 1); - const node = nodes[randomIndex]; - let missionIndex = ExportRegions[node].missionIndex; - - if ( - !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions - missionIndex != 28 && - rng.randomInt(0, 2) == 2 - ) { - missionIndex = rng.randomElement(availableMissionIndexes); - } - - if (i == 2 && rng.randomInt(0, 2) == 2) { - const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); - const modifierType = rng.randomElement(filteredModifiers); - - if (boss == "SORTIE_BOSS_PHORID") { - selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); - nodes.splice(randomIndex, 1); - continue; - } else if (sortieBossNode[boss]) { - selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); - continue; - } - } - - const missionType = eMissionType[missionIndex].tag; - - if (missionTypes.has(missionType)) { - i--; - continue; - } - - const filteredModifiers = - missionType === "MT_TERRITORY" - ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") - : modifiers; - - const modifierType = rng.randomElement(filteredModifiers); - - selectedNodes.push({ missionType, modifierType, node }); - nodes.splice(randomIndex, 1); - missionTypes.add(missionType); - } - - worldState.Sorties.push({ - _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, - Activation: { $date: { $numberLong: dayStart.toString() } }, - Expiry: { $date: { $numberLong: dayEnd.toString() } }, - Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", - Seed: genDay, - Boss: boss, - Variants: selectedNodes - }); - } + pushSortieIfRelevant(worldState.Sorties, day - 1); + pushSortieIfRelevant(worldState.Sorties, day); // Archon Hunt cycling every week { From a738dbfa9a64726b6928287ac167c33e845d25bd Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:28:34 -0700 Subject: [PATCH 20/32] fix: use JobTier instead of parsing the jobId for it (#1649) Should fix #1647 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1649 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2f78f65a..c932ac0b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -738,14 +738,12 @@ export const addMissionRewards = async ( if (rewardInfo.JobStage != undefined && rewardInfo.jobId) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [jobType, tierStr, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_"); - const tier = Number(tierStr); - + const [jobType, unkIndex, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_"); const worldState = getWorldState(); let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId); if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); // Sometimes syndicateId can be tag if (syndicateEntry && syndicateEntry.Jobs) { - let currentJob = syndicateEntry.Jobs[tier]; + let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; if (syndicateEntry.Tag === "EntratiSyndicate") { const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); if (vault) currentJob = vault; @@ -770,7 +768,7 @@ export const addMissionRewards = async ( }); SyndicateXPItemReward = medallionAmount; } else { - if (tier >= 0) { + if (rewardInfo.JobTier! >= 0) { AffiliationMods.push( addStanding(inventory, syndicateEntry.Tag, currentJob.xpAmounts[rewardInfo.JobStage]) ); @@ -946,8 +944,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (RewardInfo.jobId) { if (RewardInfo.JobStage! >= 0) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [jobType, tierStr, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_"); - const tier = Number(tierStr); + const [jobType, unkIndex, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_"); let isEndlessJob = false; if (syndicateId) { const worldState = getWorldState(); @@ -955,7 +952,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); if (syndicateEntry && syndicateEntry.Jobs) { - let job = syndicateEntry.Jobs[tier]; + let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; if (syndicateEntry.Tag === "EntratiSyndicate") { const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); From 38502f10bf30c2ee1b04af4d86f115809c6ff83d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:28:52 -0700 Subject: [PATCH 21/32] fix: give ample duplicates of ship decos with unlockAllShipDecorations (#1651) Closes #1644 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1651 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/inventoryController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index bf71ae42..910420cf 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -149,7 +149,7 @@ export const getInventoryResponse = async ( inventoryResponse.ShipDecorations = []; for (const [uniqueName, item] of Object.entries(ExportResources)) { if (item.productCategory == "ShipDecorations") { - inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 }); + inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 }); } } } From 729061951fb1e54632e0c457b32db412f7596b86 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:29:09 -0700 Subject: [PATCH 22/32] fix: allow manageQuests' deleteKey op to be used with any ItemType (#1653) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1653 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/manageQuestsController.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index 49ae004c..04650a06 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -56,15 +56,12 @@ export const manageQuestsController: RequestHandler = async (req, res) => { break; } case "deleteKey": { - if (allQuestKeys.includes(questItemType)) { - const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); - if (!questKey) { - logger.error(`Quest key not found in inventory: ${questItemType}`); - break; - } - - inventory.QuestKeys.pull({ ItemType: questItemType }); + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; } + inventory.QuestKeys.pull({ ItemType: questItemType }); break; } case "completeKey": { From 4cb1ea94e5c2e30151dbd6cc5458f53f82a8dd7e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:29:27 -0700 Subject: [PATCH 23/32] feat: sell/scrap CrewShipWeapons (#1655) Closes #1646 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1655 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/sellController.ts | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index ad31c259..f6840589 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -6,9 +6,12 @@ import { addRecipes, addMiscItems, addConsumables, - freeUpSlot + freeUpSlot, + combineInventoryChanges } from "@/src/services/inventoryService"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; export const sellController: RequestHandler = async (req, res) => { const payload = JSON.parse(String(req.body)) as ISellRequest; @@ -48,6 +51,9 @@ export const sellController: RequestHandler = async (req, res) => { if (payload.Items.Hoverboards) { requiredFields.add(InventorySlot.SPACESUITS); } + if (payload.Items.CrewShipWeapons) { + requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + } const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); // Give currency @@ -69,10 +75,14 @@ export const sellController: RequestHandler = async (req, res) => { ItemCount: payload.SellPrice } ]); + } else if (payload.SellCurrency == "SC_Resources") { + // Will add appropriate MiscItems from CrewShipWeapons } else { throw new Error("Unknown SellCurrency: " + payload.SellCurrency); } + const inventoryChanges: IInventoryChanges = {}; + // Remove item(s) if (payload.Items.Suits) { payload.Items.Suits.forEach(sellItem => { @@ -145,6 +155,24 @@ export const sellController: RequestHandler = async (req, res) => { inventory.Drones.pull({ _id: sellItem.String }); }); } + if (payload.Items.CrewShipWeapons) { + payload.Items.CrewShipWeapons.forEach(sellItem => { + const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String)); + if (index != -1) { + const itemType = inventory.CrewShipWeapons[index].ItemType; + const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType)!; + const miscItemChanges = recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: Math.trunc(x.ItemCount * 0.8) + })); + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); + + inventory.CrewShipWeapons.splice(index, 1); + freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + } + }); + } if (payload.Items.Consumables) { const consumablesChanges = []; for (const sellItem of payload.Items.Consumables) { @@ -191,7 +219,9 @@ export const sellController: RequestHandler = async (req, res) => { } await inventory.save(); - res.json({}); + res.json({ + inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges" + }); }; interface ISellRequest { @@ -212,6 +242,7 @@ interface ISellRequest { OperatorAmps?: ISellItem[]; Hoverboards?: ISellItem[]; Drones?: ISellItem[]; + CrewShipWeapons?: ISellItem[]; }; SellPrice: number; SellCurrency: From 7d607b7348216ea75133eace136779d13a4b73b2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:29:50 -0700 Subject: [PATCH 24/32] fix: check ascension ceremony contributors when changing clan tier (#1656) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1656 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/contributeGuildClassController.ts | 43 ++---------------- src/services/guildService.ts | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts index 1a20f4ef..c4fa7280 100644 --- a/src/controllers/api/contributeGuildClassController.ts +++ b/src/controllers/api/contributeGuildClassController.ts @@ -1,8 +1,7 @@ import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { Guild, GuildMember } from "@/src/models/guildModel"; -import { config } from "@/src/services/configService"; -import { createMessage } from "@/src/services/inboxService"; +import { Guild } from "@/src/models/guildModel"; +import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; @@ -31,43 +30,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) = guild.CeremonyContributors.push(new Types.ObjectId(accountId)); - // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo. - if (guild.CeremonyContributors.length == payload.RequiredContributors) { - guild.Class = guild.CeremonyClass!; - guild.CeremonyClass = undefined; - guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000)); - if (!config.fastClanAscension) { - // Send message to all active guild members - const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId"); - for (const member of members) { - // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg". - await createMessage(member.accountId, [ - { - sndr: guild.Name, - msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails", - arg: [ - { - Key: "RESETDATE", - Tag: - guild.CeremonyResetDate.getUTCMonth() + - "/" + - guild.CeremonyResetDate.getUTCDate() + - "/" + - (guild.CeremonyResetDate.getUTCFullYear() % 100) + - " " + - guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") + - ":" + - guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0") - } - ], - sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress", - icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png", - highPriority: true - } - ]); - } - } - } + await checkClanAscensionHasRequiredContributors(guild); await guild.save(); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 15b973f8..e490c304 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -31,6 +31,7 @@ import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTy import { IInventoryChanges } from "../types/purchaseTypes"; import { parallelForeach } from "../utils/async-utils"; import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json"; +import { createMessage } from "./inboxService"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -601,6 +602,50 @@ const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Pro await processGuildTechProjectContributionsUpdate(guild, project); } } + if (guild.CeremonyContributors) { + await checkClanAscensionHasRequiredContributors(guild); + } +}; + +export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDatabaseDocument): Promise => { + const requiredContributors = [1, 5, 15, 30, 50][guild.Tier - 1]; + // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo. + if (guild.CeremonyContributors!.length >= requiredContributors) { + guild.Class = guild.CeremonyClass!; + guild.CeremonyClass = undefined; + guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000)); + if (!config.fastClanAscension) { + // Send message to all active guild members + const members = await GuildMember.find({ guildId: guild._id, status: 0 }, "accountId"); + await parallelForeach(members, async member => { + // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg". + await createMessage(member.accountId, [ + { + sndr: guild.Name, + msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails", + arg: [ + { + Key: "RESETDATE", + Tag: + guild.CeremonyResetDate!.getUTCMonth() + + "/" + + guild.CeremonyResetDate!.getUTCDate() + + "/" + + (guild.CeremonyResetDate!.getUTCFullYear() % 100) + + " " + + guild.CeremonyResetDate!.getUTCHours().toString().padStart(2, "0") + + ":" + + guild.CeremonyResetDate!.getUTCMinutes().toString().padStart(2, "0") + } + ], + sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress", + icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png", + highPriority: true + } + ]); + }); + } + } }; export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => { From 46aef2c00e668b71e1a54d09db803e9d56ca0577 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:30:06 -0700 Subject: [PATCH 25/32] feat: send jordas precept email when completing pluto to eris junction (#1660) Closes #1659 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1660 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c932ac0b..d7872fc8 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -629,6 +629,19 @@ export const addMissionRewards = async ( if (node.missionReward) { missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo); } + + if (missions.Tag == "PlutoToErisJunction") { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/G1Quests/GolemQuestJordasName", + msg: "/Lotus/Language/G1Quests/GolemQuestIntroBody", + att: ["/Lotus/Types/Keys/GolemQuest/GolemQuestKeyChainItem"], + sub: "/Lotus/Language/G1Quests/GolemQuestIntroTitle", + icon: "/Lotus/Interface/Icons/Npcs/JordasPortrait.png", + highPriority: true + } + ]); + } } if (rewardInfo.useVaultManifest) { From 3d1b009bdb8dab1f34cbdefb6d6d55c13bd7ecec Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:30:22 -0700 Subject: [PATCH 26/32] feat: noDailyFocusLimit cheat (#1661) Closes #1641 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1661 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- config.json.example | 1 + src/controllers/api/inventoryController.ts | 4 ++++ src/services/configService.ts | 1 + src/services/inventoryService.ts | 4 +++- static/webui/index.html | 4 ++++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/es.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 11 files changed, 19 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index 907f8af7..9d072329 100644 --- a/config.json.example +++ b/config.json.example @@ -29,6 +29,7 @@ "unlockExilusEverywhere": false, "unlockArcanesEverywhere": false, "noDailyStandingLimits": false, + "noDailyFocusLimit": false, "noArgonCrystalDecay": false, "noMasteryRankUpCooldown": false, "noVendorPurchaseLimits": true, diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 910420cf..bede097f 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -258,6 +258,10 @@ export const getInventoryResponse = async ( } } + if (config.noDailyFocusLimit) { + inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000); + } + if (inventoryResponse.InfestedFoundry) { applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); } diff --git a/src/services/configService.ts b/src/services/configService.ts index 88a6634a..cb1a2c38 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -35,6 +35,7 @@ interface IConfig { unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + noDailyFocusLimit?: boolean; noArgonCrystalDecay?: boolean; noMasteryRankUpCooldown?: boolean; noVendorPurchaseLimits?: boolean; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index fe35cff6..2d17f7ed 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1372,7 +1372,9 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; - inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); + if (!config.noDailyFocusLimit) { + inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); + } }; export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: ILoreFragmentScan[]): void => { diff --git a/static/webui/index.html b/static/webui/index.html index d4a900c3..ec786b4e 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -595,6 +595,10 @@ +
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 7cca988a..0e69b371 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 01a97404..bbf2561a 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -133,6 +133,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, + cheats_noDailyFocusLimit: `No Daily Focus Limit`, cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 2c303d80..e2ea40ec 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Adaptadores Exilus en todas partes`, cheats_unlockArcanesEverywhere: `Adaptadores de Arcanos en todas partes`, cheats_noDailyStandingLimits: `Sin límite diario de reputación`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index e2ebd5e7..54f041c6 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`, cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`, cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 1def5df7..62c82f70 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 1ee2ea4c..3d7af8ec 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `全物品自带适配器`, cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`, cheats_noDailyStandingLimits: `无每日声望限制`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, From 51b82df5fd2e7f324b7cfb3f3531545daf7ba2ad Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:30:36 -0700 Subject: [PATCH 27/32] feat: granum void/purgatory rewards (#1663) Closes #1627 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1663 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index d7872fc8..21298951 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1141,6 +1141,32 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } } + + if (RewardInfo.PurgatoryRewardQualifications) { + for (const encodedQualification of RewardInfo.PurgatoryRewardQualifications) { + const qualification = parseInt(encodedQualification) - 1; + if (qualification < 0 || qualification > 8) { + logger.error(`unexpected purgatory reward qualification: ${qualification}`); + } else { + const drop = getRandomRewardByChance( + ExportRewards[ + [ + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards", + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryGoldTokenRewards", + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards" + ][Math.trunc(qualification / 3)] + ][qualification % 3] + ); + if (drop) { + drops.push({ + StoreItem: drop.type, + ItemCount: drop.itemCount, + FromEnemyCache: true // to show "identified" + }); + } + } + } + } } return drops; } From 0ea67ea89ade9f48baf499cce2f824c356bc5dad Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:31:00 -0700 Subject: [PATCH 28/32] feat: identify & repair railjack components (#1664) Closes #911 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1664 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/crewShipIdentifySalvageController.ts | 41 +++++++ src/controllers/api/guildTechController.ts | 114 +++++++++++++----- src/models/inventoryModels/inventoryModel.ts | 5 + src/routes/api.ts | 2 + src/services/inventoryService.ts | 24 +++- src/types/inventoryTypes/inventoryTypes.ts | 8 +- src/types/purchaseTypes.ts | 1 + 7 files changed, 158 insertions(+), 37 deletions(-) create mode 100644 src/controllers/api/crewShipIdentifySalvageController.ts diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts new file mode 100644 index 00000000..c40f78c5 --- /dev/null +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -0,0 +1,41 @@ +import { addCrewShipSalvagedWeaponSkin, addCrewShipRawSalvage, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportCustoms } from "warframe-public-export-plus"; +import { IFingerprintStat } from "@/src/helpers/rivenHelper"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; + +export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "CrewShipSalvagedWeaponSkins CrewShipRawSalvage"); + const payload = getJSONfromString(String(req.body)); + + const buffs: IFingerprintStat[] = []; + for (const upgrade of ExportCustoms[payload.ItemType].randomisedUpgrades!) { + buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + } + const inventoryChanges: IInventoryChanges = addCrewShipSalvagedWeaponSkin( + inventory, + payload.ItemType, + JSON.stringify({ compat: payload.ItemType, buffs } satisfies IInnateDamageFingerprint) + ); + + inventoryChanges.CrewShipRawSalvage = [ + { + ItemType: payload.ItemType, + ItemCount: -1 + } + ]; + addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage); + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); +}; + +interface ICrewShipIdentifySalvageRequest { + ItemType: string; +} diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 55083d5a..257434fb 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -14,20 +14,23 @@ import { import { ExportDojoRecipes } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { + addCrewShipWeaponSkin, addItem, addMiscItems, addRecipes, combineInventoryChanges, getInventory, + occupySlot, updateCurrency } from "@/src/services/inventoryService"; -import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes"; import { GuildMember } from "@/src/models/guildModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -99,6 +102,8 @@ export const guildTechController: RequestHandler = async (req, res) => { State: 0, ReqCredits: recipe.price, ItemType: data.RecipeType, + ProductCategory: data.TechProductCategory, + CategoryItemId: data.CategoryItemId, ReqItems: recipe.ingredients }) - 1 ]; @@ -222,33 +227,44 @@ export const guildTechController: RequestHandler = async (req, res) => { }); } } else if (data.Action.split(",")[0] == "Buy") { - const guild = await getGuildForRequestEx(req, inventory); - if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { - res.status(400).send("-1").end(); - return; - } const purchase = data as IGuildTechBuyRequest; - const quantity = parseInt(data.Action.split(",")[1]); - const recipeChanges = [ - { - ItemType: purchase.RecipeType, - ItemCount: quantity + if (purchase.Mode == "Guild") { + const guild = await getGuildForRequestEx(req, inventory); + if ( + !hasAccessToDojo(inventory) || + !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator)) + ) { + res.status(400).send("-1").end(); + return; } - ]; - addRecipes(inventory, recipeChanges); - const currencyChanges = updateCurrency( - inventory, - ExportDojoRecipes.research[purchase.RecipeType].replicatePrice, - false - ); - await inventory.save(); - // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. - res.json({ - inventoryChanges: { - ...currencyChanges, - Recipes: recipeChanges - } - }); + const quantity = parseInt(data.Action.split(",")[1]); + const recipeChanges = [ + { + ItemType: purchase.RecipeType, + ItemCount: quantity + } + ]; + addRecipes(inventory, recipeChanges); + const currencyChanges = updateCurrency( + inventory, + ExportDojoRecipes.research[purchase.RecipeType].replicatePrice, + false + ); + await inventory.save(); + // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. + res.json({ + inventoryChanges: { + ...currencyChanges, + Recipes: recipeChanges + } + }); + } else { + const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!); + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); + } } else if (data.Action == "Fabricate") { const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { @@ -289,9 +305,18 @@ export const guildTechController: RequestHandler = async (req, res) => { guild.ActiveDojoColorResearch = data.RecipeType; await guild.save(); res.end(); + } else if (data.Action == "Rush" && data.CategoryItemId) { + const inventoryChanges: IInventoryChanges = { + ...updateCurrency(inventory, 20, true), + ...claimSalvagedComponent(inventory, data.CategoryItemId) + }; + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); } else { logger.debug(`data provided to ${req.path}: ${String(req.body)}`); - throw new Error(`unknown guildTech action: ${data.Action}`); + throw new Error(`unhandled guildTech request`); } }; @@ -301,15 +326,15 @@ type TGuildTechRequest = | IGuildTechContributeRequest; interface IGuildTechBasicRequest { - Action: "Start" | "Fabricate" | "Pause" | "Unpause"; + Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush"; Mode: "Guild" | "Personal"; RecipeType: string; + TechProductCategory?: string; + CategoryItemId?: string; } -interface IGuildTechBuyRequest { +interface IGuildTechBuyRequest extends Omit { Action: string; - Mode: "Guild"; - RecipeType: string; } interface IGuildTechContributeRequest { @@ -321,3 +346,30 @@ interface IGuildTechContributeRequest { VaultCredits: number; VaultMiscItems: IMiscItem[]; } + +const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => { + // delete personal tech project + const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId)); + if (personalTechProjectIndex != -1) { + inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); + } + + // find salved part & delete it + const crewShipSalvagedWeaponSkinsIndex = inventory.CrewShipSalvagedWeaponSkins.findIndex(x => x._id.equals(itemId)); + const crewShipWeaponSkin = inventory.CrewShipSalvagedWeaponSkins[crewShipSalvagedWeaponSkinsIndex]; + inventory.CrewShipSalvagedWeaponSkins.splice(crewShipSalvagedWeaponSkinsIndex, 1); + + // add final item + const inventoryChanges = { + ...addCrewShipWeaponSkin(inventory, crewShipWeaponSkin.ItemType, crewShipWeaponSkin.UpgradeFingerprint), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) + }; + + inventoryChanges.RemovedIdItems = [ + { + ItemId: { $oid: itemId } + } + ]; + + return inventoryChanges; +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 6b4b4959..97a0022e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -504,6 +504,8 @@ const personalTechProjectSchema = new Schema({ State: Number, ReqCredits: Number, ItemType: String, + ProductCategory: String, + CategoryItemId: Schema.Types.ObjectId, ReqItems: { type: [typeCountSchema], default: undefined }, HasContributions: Boolean, CompletionDate: Date @@ -522,6 +524,9 @@ personalTechProjectSchema.set("toJSON", { const db = ret as IPersonalTechProjectDatabase; const client = ret as IPersonalTechProjectClient; + if (db.CategoryItemId) { + client.CategoryItemId = toOid(db.CategoryItemId); + } if (db.CompletionDate) { client.CompletionDate = toMongoDate(db.CompletionDate); } diff --git a/src/routes/api.ts b/src/routes/api.ts index 39b3d751..d16ea321 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -27,6 +27,7 @@ import { contributeToVaultController } from "@/src/controllers/api/contributeToV import { createAllianceController } from "@/src/controllers/api/createAllianceController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; +import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController"; @@ -218,6 +219,7 @@ apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentContro apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createAlliance.php", createAllianceController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController); apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 2d17f7ed..d3e86bfd 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -426,7 +426,7 @@ export const addItem = async ( ); } inventoryChanges = { - ...addCrewShipWeaponSkin(inventory, typeName), + ...addCrewShipWeaponSkin(inventory, typeName, undefined), ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) }; } @@ -1107,12 +1107,14 @@ export const addSkin = ( return inventoryChanges; }; -const addCrewShipWeaponSkin = ( +export const addCrewShipWeaponSkin = ( inventory: TInventoryDatabaseDocument, typeName: string, + upgradeFingerprint: string | undefined, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = inventory.CrewShipWeaponSkins.push({ ItemType: typeName }) - 1; + const index = + inventory.CrewShipWeaponSkins.push({ ItemType: typeName, UpgradeFingerprint: upgradeFingerprint }) - 1; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.CrewShipWeaponSkins ??= []; (inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push( @@ -1121,6 +1123,22 @@ const addCrewShipWeaponSkin = ( return inventoryChanges; }; +export const addCrewShipSalvagedWeaponSkin = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + upgradeFingerprint: string | undefined, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = + inventory.CrewShipSalvagedWeaponSkins.push({ ItemType: typeName, UpgradeFingerprint: upgradeFingerprint }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + inventoryChanges.CrewShipSalvagedWeaponSkins ??= []; + (inventoryChanges.CrewShipSalvagedWeaponSkins as IUpgradeClient[]).push( + inventory.CrewShipSalvagedWeaponSkins[index].toJSON() + ); + return inventoryChanges; +}; + const addCrewShip = ( inventory: TInventoryDatabaseDocument, typeName: string, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 63207ef0..73156437 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -947,15 +947,17 @@ export interface IPersonalTechProjectDatabase { State: number; ReqCredits: number; ItemType: string; + ProductCategory?: string; + CategoryItemId?: Types.ObjectId; ReqItems: ITypeCount[]; HasContributions?: boolean; CompletionDate?: Date; } -export interface IPersonalTechProjectClient extends Omit { - CompletionDate?: IMongoDate; - ProductCategory?: string; +export interface IPersonalTechProjectClient + extends Omit { CategoryItemId?: IOid; + CompletionDate?: IMongoDate; ItemId: IOid; } diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 51459454..1b4c9aca 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -43,6 +43,7 @@ export type IInventoryChanges = { Drones?: IDroneClient[]; MiscItems?: IMiscItem[]; EmailItems?: ITypeCount[]; + CrewShipRawSalvage?: ITypeCount[]; Nemesis?: Partial; NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 From deb652ab37a164e6f992fda3d6b1175d54fe79f1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:31:16 -0700 Subject: [PATCH 29/32] fix: provide upcoming nightwave daily challenge if rollover is imminent (#1667) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1667 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 62 +++++++++++++++++-------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 5137b5ed..55f6afd9 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -374,34 +374,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Season: 14, Phase: 0, Params: "", - ActiveChallenges: [ - getSeasonDailyChallenge(day - 2), - getSeasonDailyChallenge(day - 1), - getSeasonDailyChallenge(day - 0), - getSeasonWeeklyChallenge(week, 0), - getSeasonWeeklyChallenge(week, 1), - getSeasonWeeklyHardChallenge(week, 2), - getSeasonWeeklyHardChallenge(week, 3), - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: - "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) - }, - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) - }, - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) - } - ] + ActiveChallenges: [] }, ...staticWorldState, SyndicateMissions: [...staticWorldState.SyndicateMissions] @@ -424,6 +397,37 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); } + // Nightwave Challenges + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0)); + if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1)); + } + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3)); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) + }); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) + }); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) + }); + // TODO: Provide upcoming weekly acts if rollover is imminent + // Elite Sanctuary Onslaught cycling every week worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful @@ -737,6 +741,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { pushSortieIfRelevant(worldState.Sorties, day); // Archon Hunt cycling every week + // TODO: Handle imminent rollover { const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; @@ -829,6 +834,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); // 1999 Calendar Season cycling every week + YearIteration every 4 weeks + // TODO: Handle imminent rollover worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } }; worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } }; worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4]; From 44da0eb50a40e2d05b67123aafaddc1841c7f460 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:35:39 +0200 Subject: [PATCH 30/32] docs: some basic explanation of config.json & config.json.example --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ef091058..e1ef957a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ To get an idea of what functionality you can expect to be missing [have a look t ## config.json +SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled. + - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. - `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE. From 379e83a764988ece565f11c677d831e95dcdd9fc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 08:36:00 -0700 Subject: [PATCH 31/32] fix: use rewardTier only for rescue missions (#1674) Fixes #1672 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1674 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 2 +- src/types/requestTypes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 21298951..0b452ccc 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -66,7 +66,7 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] } // For Rescue missions - if (rewardInfo.rewardTier) { + if (rewardInfo.node in ExportRegions && ExportRegions[rewardInfo.node].missionIndex == 3 && rewardInfo.rewardTier) { return [rewardInfo.rewardTier]; } diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 77e7f322..fff164a8 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -130,7 +130,7 @@ export type IMissionInventoryUpdateRequest = { export interface IRewardInfo { node: string; VaultsCracked?: number; // for Spy missions - rewardTier?: number; // for Rescue missions + rewardTier?: number; nightmareMode?: boolean; useVaultManifest?: boolean; EnemyCachesFound?: number; From 9a50c05205bb49f2227332286c054bb9c232a11e Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Wed, 16 Apr 2025 09:01:47 -0700 Subject: [PATCH 32/32] chore(webui): update to German translation (#1680) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1680 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 0e69b371..20be9fbd 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -133,8 +133,8 @@ dict = { cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`, cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, - cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, - cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, + cheats_noDailyStandingLimits: `Kein tägliches Ansehen Limit`, + cheats_noDailyFocusLimit: `Kein tägliches Fokus-Limit`, cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`,