From 9731004de6f8a362c2523eec5138a3454df25968 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:51:47 -0700 Subject: [PATCH 01/10] feat: autogenerate railjack crew member vendor (#2170) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2170 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 44 +++- .../HubsRailjackCrewMemberVendorManifest.json | 244 ------------------ 2 files changed, 40 insertions(+), 248 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 701b85b4..295759b2 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -20,7 +20,6 @@ import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/Deim import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; -import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json"; import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; @@ -42,7 +41,6 @@ const rawVendorManifests: IVendorManifest[] = [ DuviriAcrithisVendorManifest, EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabVendorManifest, - HubsRailjackCrewMemberVendorManifest, MaskSalesmanManifest, Nova1999ConquestShopManifest, OstronPetVendorManifest, @@ -273,20 +271,39 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani const offersToAdd: IVendorOffer[] = []; if (!manifest.isOneBinPerCycle) { const remainingItemCapacity: Record = {}; + const missingItemsPerBin: Record = {}; + let numOffersThatNeedToMatchABin = 0; + if (manifest.numItemsPerBin) { + for (let bin = 0; bin != manifest.numItemsPerBin.length; ++bin) { + missingItemsPerBin[bin] = manifest.numItemsPerBin[bin]; + numOffersThatNeedToMatchABin += manifest.numItemsPerBin[bin]; + } + } for (const item of manifest.items) { remainingItemCapacity[item.storeItem] = 1 + item.duplicates; } for (const offer of info.ItemManifest) { remainingItemCapacity[offer.StoreItem] -= 1; + const bin = parseInt(offer.Bin.substring(4)); + if (missingItemsPerBin[bin]) { + missingItemsPerBin[bin] -= 1; + numOffersThatNeedToMatchABin -= 1; + } } if (manifest.numItems && manifest.items.length != manifest.numItems.minValue) { const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue); while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) { - // TODO: Consider per-bin item limits // TODO: Consider item probability weightings const item = rng.randomElement(manifest.items)!; - if (remainingItemCapacity[item.storeItem] != 0) { + if ( + remainingItemCapacity[item.storeItem] != 0 && + (numOffersThatNeedToMatchABin == 0 || missingItemsPerBin[item.bin]) + ) { remainingItemCapacity[item.storeItem] -= 1; + if (missingItemsPerBin[item.bin]) { + missingItemsPerBin[item.bin] -= 1; + numOffersThatNeedToMatchABin -= 1; + } offersToAdd.push(item); } } @@ -383,6 +400,12 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani info.ItemManifest.push(item); } + info.ItemManifest.sort((a, b) => { + const aBin = parseInt(a.Bin.substring(4)); + const bBin = parseInt(b.Bin.substring(4)); + return aBin == bBin ? 0 : aBin < bBin ? +1 : -1; + }); + // Update vendor expiry let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; for (const offer of info.ItemManifest) { @@ -424,4 +447,17 @@ if (isDev) { ) { logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest`); } + + const cms = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest")! + .VendorInfo.ItemManifest; + if ( + cms.length != 9 || + cms[0].Bin != "BIN_2" || + cms[8].Bin != "BIN_0" || + cms.reduce((a, x) => a + (x.Bin == "BIN_2" ? 1 : 0), 0) < 2 || + cms.reduce((a, x) => a + (x.Bin == "BIN_1" ? 1 : 0), 0) < 2 || + cms.reduce((a, x) => a + (x.Bin == "BIN_0" ? 1 : 0), 0) < 4 + ) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest`); + } } diff --git a/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json b/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json deleted file mode 100644 index 16506360..00000000 --- a/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json +++ /dev/null @@ -1,244 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "5fb70313c96976e97d6be787" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/SteelMeridianCrewMemberGeneratorStrong", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem", - "ItemCount": 2220, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [2180000, 2180000], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "SteelMeridianSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 4185144421, - "Id": { - "$oid": "670daf92d21f34757a5e73da" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorStrong", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem", - "ItemCount": 2130, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [1890000, 1890000], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "NewLokaSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 496053258, - "Id": { - "$oid": "670daf92d21f34757a5e73db" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/SteelMeridianCrewMemberGeneratorMediumVersionTwo", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem", - "ItemCount": 440, - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "SteelMeridianSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 2078883475, - "Id": { - "$oid": "670daf92d21f34757a5e73dc" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorMediumVersionTwo", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/RailjackMiscItems/AsteriteRailjackItem", - "ItemCount": 730, - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "NewLokaSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 3890380934, - "Id": { - "$oid": "670daf92d21f34757a5e73dd" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/CephalonSudaCrewMemberGeneratorMediumVersionTwo", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/RailjackMiscItems/AsteriteRailjackItem", - "ItemCount": 720, - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "CephalonSudaSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 3425148044, - "Id": { - "$oid": "670daf92d21f34757a5e73de" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/ArbitersCrewMemberGeneratorMediumVersionTwo", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/RailjackMiscItems/CubicsRailjackItem", - "ItemCount": 6500, - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "ArbitersSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 2472754512, - "Id": { - "$oid": "670daf92d21f34757a5e73df" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/PerrinCrewMemberGeneratorVersionTwo", - "RegularPrice": [105000, 105000], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "PerrinSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 966238763, - "Id": { - "$oid": "670daf92d21f34757a5e73e0" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorVersionTwo", - "RegularPrice": [120000, 120000], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "NewLokaSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 356717213, - "Id": { - "$oid": "670daf92d21f34757a5e73e1" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/ArbitersCrewMemberGeneratorVersionTwo", - "RegularPrice": [120000, 120000], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "Affiliation": "ArbitersSyndicate", - "MinAffiliationRank": 0, - "ReductionPerPositiveRank": 0.1, - "IncreasePerNegativeRank": 0.5, - "AllowMultipurchase": false, - "LocTagRandSeed": 1969797050, - "Id": { - "$oid": "670daf92d21f34757a5e73e2" - } - } - ], - "PropertyTextHash": "BE543CCC0A4F50A1D80CD2B523796EAE", - "RandomSeedType": "VRST_FLAVOUR_TEXT", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} From 3e99e069be857932bd4ff483ac16fa2b8e460f91 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:55:35 -0700 Subject: [PATCH 02/10] feat: void storm rotation (#2171) Re #1512 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2171 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 69 +++++++++++++++++++ src/types/worldStateTypes.ts | 9 +++ .../worldState/worldState.json | 44 ------------ 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index c5e30868..253bdc21 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -16,6 +16,7 @@ import { ISortie, ISortieMission, ISyndicateMissionInfo, + IVoidStorm, IWorldState } from "../types/worldStateTypes"; import { version_compare } from "../helpers/inventoryHelpers"; @@ -963,6 +964,61 @@ const getCalendarSeason = (week: number): ICalendarSeason => { }; }; +// Not very faithful, but to avoid the same node coming up back-to-back (which is not valid), I've split these into 2 arrays which we're alternating between. + +const voidStormMissionsA = { + VoidT1: ["CrewBattleNode519", "CrewBattleNode518", "CrewBattleNode515", "CrewBattleNode503"], + VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530"], + VoidT3: ["CrewBattleNode521", "CrewBattleNode516"], + VoidT4: [ + "CrewBattleNode555", + "CrewBattleNode553", + "CrewBattleNode554", + "CrewBattleNode539", + "CrewBattleNode531", + "CrewBattleNode527" + ] +}; + +const voidStormMissionsB = { + VoidT1: ["CrewBattleNode509", "CrewBattleNode522", "CrewBattleNode511", "CrewBattleNode512"], + VoidT2: ["CrewBattleNode535", "CrewBattleNode533"], + VoidT3: ["CrewBattleNode524", "CrewBattleNode525"], + VoidT4: [ + "CrewBattleNode542", + "CrewBattleNode538", + "CrewBattleNode543", + "CrewBattleNode536", + "CrewBattleNode550", + "CrewBattleNode529" + ] +}; + +const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => { + const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute; + const expiry = activation + 90 * unixTimesInMs.minute; + let accum = 0; + const rng = new SRng(new SRng(hour).randomInt(0, 100_000)); + const voidStormMissions = structuredClone(hour & 1 ? voidStormMissionsA : voidStormMissionsB); + for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) { + const idx = rng.randomInt(0, voidStormMissions[tier].length - 1); + const node = voidStormMissions[tier][idx]; + voidStormMissions[tier].splice(idx, 1); + arr.push({ + _id: { + $oid: + ((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") + + "0321e89b" + + (accum++).toString().padStart(8, "0") + }, + Node: node, + Activation: { $date: { $numberLong: activation.toString() } }, + Expiry: { $date: { $numberLong: expiry.toString() } }, + ActiveMissionTier: tier + }); + } +}; + const doesTimeSatsifyConstraints = (timeSecs: number): boolean => { if (config.worldState?.eidolonOverride) { const eidolonEpoch = 1391992660; @@ -1032,6 +1088,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Sorties: [], LiteSorties: [], GlobalUpgrades: [], + VoidStorms: [], EndlessXpChoices: [], KnownCalendarSeasons: [], ...staticWorldState, @@ -1228,6 +1285,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1)); } + // Void Storms + const hour = Math.trunc(timeMs / unixTimesInMs.hour); + const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute; + const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute; + if (overLastHourStormExpiry > timeMs) { + pushVoidStorms(worldState.VoidStorms, hour - 2); + } + pushVoidStorms(worldState.VoidStorms, hour - 1); + if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) { + pushVoidStorms(worldState.VoidStorms, hour); + } + // Sentient Anomaly cycling every 30 minutes const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2)); const tmp = { diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 78c1f330..880c5800 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -12,6 +12,7 @@ export interface IWorldState { GlobalUpgrades: IGlobalUpgrade[]; ActiveMissions: IFissure[]; NodeOverrides: INodeOverride[]; + VoidStorms: IVoidStorm[]; PVPChallengeInstances: IPVPChallengeInstance[]; EndlessXpChoices: IEndlessXpChoice[]; SeasonInfo?: { @@ -131,6 +132,14 @@ export interface ILiteSortie { }[]; } +export interface IVoidStorm { + _id: IOid; + Node: string; + Activation: IMongoDate; + Expiry: IMongoDate; + ActiveMissionTier: string; +} + export interface IPVPChallengeInstance { _id: IOid; challengeTypeRefID: string; diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 95b5fde2..73d48ce1 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -2562,50 +2562,6 @@ ] } ], - "VoidStorms": [ - { - "_id": { "$oid": "663a7581ced28e18f694b550" }, - "Node": "CrewBattleNode519", - "Activation": { "$date": { "$numberLong": "1715109601821" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "ActiveMissionTier": "VoidT1" - }, - { - "_id": { "$oid": "663a7581ced28e18f694b551" }, - "Node": "CrewBattleNode515", - "Activation": { "$date": { "$numberLong": "1715109601825" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "ActiveMissionTier": "VoidT1" - }, - { - "_id": { "$oid": "663a7581ced28e18f694b554" }, - "Node": "CrewBattleNode536", - "Activation": { "$date": { "$numberLong": "1715109601832" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "ActiveMissionTier": "VoidT4" - }, - { - "_id": { "$oid": "663a7581ced28e18f694b555" }, - "Node": "CrewBattleNode539", - "Activation": { "$date": { "$numberLong": "1715109601834" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "ActiveMissionTier": "VoidT4" - }, - { - "_id": { "$oid": "663a7581ced28e18f694b553" }, - "Node": "CrewBattleNode521", - "Activation": { "$date": { "$numberLong": "1715109601829" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "ActiveMissionTier": "VoidT3" - }, - { - "_id": { "$oid": "663a7581ced28e18f694b552" }, - "Node": "CrewBattleNode535", - "Activation": { "$date": { "$numberLong": "1715109601827" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "ActiveMissionTier": "VoidT2" - } - ], "PrimeAccessAvailability": { "State": "PRIME1" }, "PrimeVaultAvailabilities": [false, false, false, false, false], "PrimeTokenAvailability": true, From 8c1147998d064ae7c71ac581dcd9862ee917dd13 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:55:56 -0700 Subject: [PATCH 03/10] fix(webui): respond with 200 for successful shard operations (#2175) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2175 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/popArchonCrystalUpgradeController.ts | 1 + src/controllers/custom/pushArchonCrystalUpgradeController.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/controllers/custom/popArchonCrystalUpgradeController.ts b/src/controllers/custom/popArchonCrystalUpgradeController.ts index 34e87ec6..d6ef86e0 100644 --- a/src/controllers/custom/popArchonCrystalUpgradeController.ts +++ b/src/controllers/custom/popArchonCrystalUpgradeController.ts @@ -12,6 +12,7 @@ export const popArchonCrystalUpgradeController: RequestHandler = async (req, res ); await inventory.save(); res.end(); + return; } res.status(400).end(); }; diff --git a/src/controllers/custom/pushArchonCrystalUpgradeController.ts b/src/controllers/custom/pushArchonCrystalUpgradeController.ts index 3a9286ee..0db365fb 100644 --- a/src/controllers/custom/pushArchonCrystalUpgradeController.ts +++ b/src/controllers/custom/pushArchonCrystalUpgradeController.ts @@ -15,6 +15,7 @@ export const pushArchonCrystalUpgradeController: RequestHandler = async (req, re } await inventory.save(); res.end(); + return; } } res.status(400).end(); From 4e832d3b2c9b8743cef25f8d3b9027c07143925d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:05:26 -0700 Subject: [PATCH 04/10] chore(webui): add login/register error messages to translation system (#2179) Closes #2178 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2179 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 2 +- static/webui/translations/de.js | 2 ++ static/webui/translations/en.js | 2 ++ static/webui/translations/es.js | 2 ++ static/webui/translations/fr.js | 2 ++ static/webui/translations/ru.js | 2 ++ static/webui/translations/zh.js | 2 ++ 7 files changed, 13 insertions(+), 1 deletion(-) diff --git a/static/webui/script.js b/static/webui/script.js index 5cc9feaf..b51648e1 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -29,7 +29,7 @@ function loginFromLocalStorage() { }, () => { logout(); - alert(isRegister ? "Registration failed. Account already exists?" : "Login failed"); + alert(loc(isRegister ? "code_regFail" : "code_loginFail")); } ); } diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 4a3cd271..8b88151b 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -3,6 +3,8 @@ dict = { general_inventoryUpdateNote: `Hinweis: Änderungen, die hier vorgenommen werden, werden erst im Spiel angewendet, sobald das Inventar synchronisiert wird. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, general_addButton: `Hinzufügen`, general_bulkActions: `Massenaktionen`, + code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, + code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, code_nonValidAuthz: `Deine Anmeldedaten sind nicht mehr gültig.`, code_changeNameConfirm: `In welchen Namen möchtest du deinen Account umbenennen?`, code_deleteAccountConfirm: `Bist du sicher, dass du deinen Account |DISPLAYNAME| (|EMAIL|) löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 070dfa4c..437d5057 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -2,6 +2,8 @@ dict = { general_inventoryUpdateNote: `Note: Changes made here will only be applied in-game when the game syncs the inventory. Visiting the navigation should be the easiest way to trigger that.`, general_addButton: `Add`, general_bulkActions: `Bulk Actions`, + code_loginFail: `Login failed. Double-check the email and password.`, + code_regFail: `Registration failed. Account already exists?`, code_nonValidAuthz: `Your credentials are no longer valid.`, code_changeNameConfirm: `What would you like to change your account name to?`, code_deleteAccountConfirm: `Are you sure you want to delete your account |DISPLAYNAME| (|EMAIL|)? This action cannot be undone.`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 907b9298..0b6b15ec 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -3,6 +3,8 @@ dict = { general_inventoryUpdateNote: `Nota: Los cambios realizados aquí se reflejarán en el juego cuando este sincronice el inventario. Usar la navegación debería ser la forma más sencilla de activar esto.`, general_addButton: `Agregar`, general_bulkActions: `Acciones masivas`, + code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, + code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, code_nonValidAuthz: `Tus credenciales no son válidas.`, code_changeNameConfirm: `¿Qué nombre te gustaría ponerle a tu cuenta?`, code_deleteAccountConfirm: `¿Estás seguro de que deseas eliminar tu cuenta |DISPLAYNAME| (|EMAIL|)? Esta acción es permanente.`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 22f2dfc8..8d4eceb2 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -3,6 +3,8 @@ dict = { general_inventoryUpdateNote: `Note : Les changements effectués ici seront appliqués lors de la syncrhonisation. Visiter la navigation appliquera les changements apportés à l'inventaire.`, general_addButton: `Ajouter`, general_bulkActions: `Action groupée`, + code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, + code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, code_nonValidAuthz: `Informations de connexion invalides`, code_changeNameConfirm: `Nouveau nom du compte :`, code_deleteAccountConfirm: `Supprimer |DISPLAYNAME| (|EMAIL|) ? Cette action est irreversible.`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 41dc15c7..7493099d 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -3,6 +3,8 @@ dict = { general_inventoryUpdateNote: `Примечание: изменения, внесенные здесь, отобразятся в игре только после повторной загрузки вашего инвентаря. Посещение навигации — самый простой способ этого добиться.`, general_addButton: `Добавить`, general_bulkActions: `Массовые действия`, + code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, + code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, code_nonValidAuthz: `Ваши данные больше не действительны.`, code_changeNameConfirm: `Какое имя вы хотите установить для своей учетной записи?`, code_deleteAccountConfirm: `Вы уверены, что хотите удалить аккаунт |DISPLAYNAME| (|EMAIL|)? Это действие нельзя отменить.`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 9bd48150..08cb30d5 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -3,6 +3,8 @@ dict = { general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`, general_addButton: `添加`, general_bulkActions: `批量操作`, + code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, + code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, code_nonValidAuthz: `您的登录凭证已失效。`, code_changeNameConfirm: `您想将账户名称更改为什么?`, code_deleteAccountConfirm: `确定要删除账户 |DISPLAYNAME| (|EMAIL|) 吗?此操作不可撤销。`, From 53976378bbed3523986db127ee2a76465f458ffb Mon Sep 17 00:00:00 2001 From: qianlishun Date: Tue, 17 Jun 2025 03:28:49 -0700 Subject: [PATCH 05/10] chore(webui): update Chinese translation (#2184) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2184 Co-authored-by: qianlishun Co-committed-by: qianlishun --- static/webui/translations/zh.js | 110 ++++++++++++++++---------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 08cb30d5..722b709a 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -3,8 +3,8 @@ dict = { general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`, general_addButton: `添加`, general_bulkActions: `批量操作`, - code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, - code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, + code_loginFail: `登录失败。请检查邮箱和密码。`, + code_regFail: `注册失败。账号已存在。`, code_nonValidAuthz: `您的登录凭证已失效。`, code_changeNameConfirm: `您想将账户名称更改为什么?`, code_deleteAccountConfirm: `确定要删除账户 |DISPLAYNAME| (|EMAIL|) 吗?此操作不可撤销。`, @@ -27,7 +27,7 @@ dict = { code_renamePrompt: `输入新的自定义名称:`, code_remove: `移除`, code_addItemsConfirm: `确定要向账户添加 |COUNT| 件物品吗?`, - code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, + code_succRankUp: `等级已提升`, code_noEquipmentToRankUp: `没有可升级的装备。`, code_succAdded: `已成功添加。`, code_succRemoved: `已成功移除。`, @@ -36,8 +36,8 @@ dict = { code_rerollsNumber: `洗卡次数`, code_viewStats: `查看属性`, code_rank: `等级`, - code_rankUp: `[UNTRANSLATED] Rank up`, - code_rankDown: `[UNTRANSLATED] Rank down`, + code_rankUp: `等级提升`, + code_rankDown: `等级下降`, code_count: `数量`, code_focusAllUnlocked: `所有专精学派均已解锁。`, code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`, @@ -178,56 +178,56 @@ dict = { import_importNote: `您可以在此处提供完整或部分库存响应(客户端表示)。支持的所有字段将被覆盖到您的账户中。`, import_submit: `提交`, - upgrade_Equilibrium: `[UNTRANSLATED] +|VAL|% Energy from Health pickups, +|VAL|% Health from Energy pickups`, - upgrade_MeleeCritDamage: `[UNTRANSLATED] +|VAL|% Melee Critical Damage`, - upgrade_PrimaryStatusChance: `[UNTRANSLATED] +|VAL|% Primary Status Chance`, - upgrade_SecondaryCritChance: `[UNTRANSLATED] +|VAL|% Secondary Critical Chance`, - upgrade_WarframeAbilityDuration: `[UNTRANSLATED] +|VAL|% Ability Duration`, - upgrade_WarframeAbilityStrength: `[UNTRANSLATED] +|VAL|% Ability Strength`, - upgrade_WarframeArmourMax: `[UNTRANSLATED] +|VAL| Armor`, - upgrade_WarframeBlastProc: `[UNTRANSLATED] +|VAL| Shields on inflicting Blast Status`, - upgrade_WarframeCastingSpeed: `[UNTRANSLATED] +|VAL|% Casting Speed`, - upgrade_WarframeCorrosiveDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Corrosion Status`, - upgrade_WarframeCorrosiveStack: `[UNTRANSLATED] Increase max stacks of Corrosion Status by +|VAL|`, - upgrade_WarframeCritDamageBoost: `[UNTRANSLATED] +|VAL|% Melee Critical Damage (Doubles over 500 Energy)`, - upgrade_WarframeElectricDamage: `[UNTRANSLATED] +|VAL1|% Primary Electricity Damage (+|VAL2|% per additional Shard)`, - upgrade_WarframeElectricDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Electricity Status`, - upgrade_WarframeEnergyMax: `[UNTRANSLATED] +|VAL| Energy Max`, - upgrade_WarframeGlobeEffectEnergy: `[UNTRANSLATED] +|VAL|% Energy Orb Effectiveness`, - upgrade_WarframeGlobeEffectHealth: `[UNTRANSLATED] +|VAL|% Health Orb Effectiveness`, - upgrade_WarframeHealthMax: `[UNTRANSLATED] +|VAL| Health`, - upgrade_WarframeHPBoostFromImpact: `[UNTRANSLATED] +|VAL1| Health per enemy killed with Blast Damage (Max |VAL2| Health)`, - upgrade_WarframeParkourVelocity: `[UNTRANSLATED] +|VAL|% Parkour Velocity`, - upgrade_WarframeRadiationDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Radiation Status`, - upgrade_WarframeRegen: `[UNTRANSLATED] +|VAL| Health Regen/s`, - upgrade_WarframeShieldMax: `[UNTRANSLATED] +|VAL| Shield`, - upgrade_WarframeStartingEnergy: `[UNTRANSLATED] +|VAL|% Energy on Spawn`, - upgrade_WarframeToxinDamage: `[UNTRANSLATED] +|VAL|% Toxin Status Effect Damage`, - upgrade_WarframeToxinHeal: `[UNTRANSLATED] +|VAL| Health on damaging enemies with Toxin Status`, - upgrade_WeaponCritBoostFromHeat: `[UNTRANSLATED] +|VAL1|% Secondary Critical Chance per Heat-affected enemy killed (Max |VAL2|%)`, - upgrade_AvatarAbilityRange: `[UNTRANSLATED] +7.5% Ability Range`, - upgrade_AvatarAbilityEfficiency: `[UNTRANSLATED] +5% Ability Efficiency`, - upgrade_AvatarEnergyRegen: `[UNTRANSLATED] +0.5 Energy Regen/s`, - upgrade_AvatarEnemyRadar: `[UNTRANSLATED] +5m Enemy Radar`, - upgrade_AvatarLootRadar: `[UNTRANSLATED] +7m Loot Radar`, - upgrade_WeaponAmmoMax: `[UNTRANSLATED] +15% Ammo Max`, - upgrade_EnemyArmorReductionAura: `[UNTRANSLATED] -3% Enemy Armor`, - upgrade_OnExecutionAmmo: `[UNTRANSLATED] 100% Primary and Secondary Magazine Refill on Mercy`, - upgrade_OnExecutionHealthDrop: `[UNTRANSLATED] 100% chance to drop a Health Orb on Mercy`, - upgrade_OnExecutionEnergyDrop: `[UNTRANSLATED] 50% chance to drop an Energy Orb on Mercy`, - upgrade_OnFailHackReset: `[UNTRANSLATED] +50% to retry on Hacking failure`, - upgrade_DamageReductionOnHack: `[UNTRANSLATED] 75% Damage Reduction while Hacking`, - upgrade_OnExecutionReviveCompanion: `[UNTRANSLATED] Mercy Kills reduce Companion Recovery by 15s`, - upgrade_OnExecutionParkourSpeed: `[UNTRANSLATED] +60% Parkour Speed after a Mercy for 15s`, - upgrade_AvatarTimeLimitIncrease: `[UNTRANSLATED] s to Hacking`, - upgrade_ElectrifyOnHack: `[UNTRANSLATED] Shock enemies within 20m while Hacking`, - upgrade_OnExecutionTerrify: `[UNTRANSLATED] 50% chance for enemies within 15m to cower in fear for 8 seconds on Mercy`, - upgrade_OnHackLockers: `[UNTRANSLATED] Unlock 5 lockers within 20m after Hacking`, - upgrade_OnExecutionBlind: `[UNTRANSLATED] Blind enemies within 18m on Mercy`, - upgrade_OnExecutionDrainPower: `[UNTRANSLATED] 100% chance for next ability cast to gain +50% Ability Strength on Mercy`, - upgrade_OnHackSprintSpeed: `[UNTRANSLATED] +75% Sprint Speed for 15s after Hacking`, - upgrade_SwiftExecute: `[UNTRANSLATED] Speed of Mercy Kills increased by 50%`, - upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after hacking`, + upgrade_Equilibrium: `+|VAL|% 能量 来自生命球, +|VAL|% 生命 来自能量球`, + upgrade_MeleeCritDamage: `+|VAL|% 近战暴击伤害`, + upgrade_PrimaryStatusChance: `+|VAL|% 主武器触发几率`, + upgrade_SecondaryCritChance: `+|VAL|% 次要武器暴击几率`, + upgrade_WarframeAbilityDuration: `+|VAL|% 技能持续时间`, + upgrade_WarframeAbilityStrength: `+|VAL|% 技能强度`, + upgrade_WarframeArmourMax: `+|VAL| 护甲`, + upgrade_WarframeBlastProc: `施加爆炸状态时,护盾 +|VAL|`, + upgrade_WarframeCastingSpeed: `+|VAL|% 施放速度`, + upgrade_WarframeCorrosiveDamageBoost: `对受腐蚀状态影响的敌人 +|VAL|% 技能伤害`, + upgrade_WarframeCorrosiveStack: `腐蚀状态最大堆叠数 +|VAL|`, + upgrade_WarframeCritDamageBoost: `+|VAL|% 近战暴击伤害 (500能量以上翻倍)`, + upgrade_WarframeElectricDamage: `+|VAL1|% 主武器伤害效果 (+|VAL2|% 每附加一个碎片)`, + upgrade_WarframeElectricDamageBoost: `对受电能状态影响的敌人 +|VAL|% 技能伤害`, + upgrade_WarframeEnergyMax: `+|VAL| 最大能量`, + upgrade_WarframeGlobeEffectEnergy: `+|VAL|% 能量球效果`, + upgrade_WarframeGlobeEffectHealth: `+|VAL|% 生命球效果`, + upgrade_WarframeHealthMax: `+|VAL| 生命`, + upgrade_WarframeHPBoostFromImpact: `每个被爆炸伤害击杀的敌人,补充 |VAL1|生命 (最大 |VAL2| 生命)`, + upgrade_WarframeParkourVelocity: `+|VAL|% 跑酷速度`, + upgrade_WarframeRadiationDamageBoost: `对受辐射状态影响的敌人 +|VAL|% 技能伤害`, + upgrade_WarframeRegen: `+|VAL| 生命再生/s`, + upgrade_WarframeShieldMax: `+|VAL| 护盾`, + upgrade_WarframeStartingEnergy: `+|VAL|% 能量出生时`, + upgrade_WarframeToxinDamage: `+|VAL|% 毒素伤害效果`, + upgrade_WarframeToxinHeal: `+|VAL| 生命 对毒素状态的敌人造成伤害时`, + upgrade_WeaponCritBoostFromHeat: `每个被火焰伤害杀死的敌人, 增加|VAL1|% 次要武器暴击几率 (最大 |VAL2|%)`, + upgrade_AvatarAbilityRange: `+7.5% 技能范围`, + upgrade_AvatarAbilityEfficiency: `+5% 技能效率`, + upgrade_AvatarEnergyRegen: `+0.5 能量再生/秒`, + upgrade_AvatarEnemyRadar: `+5米 敌方雷达`, + upgrade_AvatarLootRadar: `+7米 战利品雷达`, + upgrade_WeaponAmmoMax: `+15% 弹药最大容量`, + upgrade_EnemyArmorReductionAura: `-3% 敌方护甲`, + upgrade_OnExecutionAmmo: `怜悯之击 100% 补充主次要武器弹匣`, + upgrade_OnExecutionHealthDrop: `怜悯之击 100% 几率 掉落生命球`, + upgrade_OnExecutionEnergyDrop: `怜悯之击 50% 几率 掉落生命球`, + upgrade_OnFailHackReset: `+50% 在入侵失败时重试`, + upgrade_DamageReductionOnHack: `入侵时,+75% 伤害减免`, + upgrade_OnExecutionReviveCompanion: `怜悯之击 减少同伴复苏时间 15秒`, + upgrade_OnExecutionParkourSpeed: `怜悯之击 15秒内 +60% 跑酷速度`, + upgrade_AvatarTimeLimitIncrease: `增加入侵限制时间`, + upgrade_ElectrifyOnHack: `入侵时震慑20米之内的敌人`, + upgrade_OnExecutionTerrify: `怜悯之击 50% 几率让 15米 以内的敌人恐慌`, + upgrade_OnHackLockers: `入侵后解锁20米内的5个储物柜`, + upgrade_OnExecutionBlind: `怜悯之击 致盲18米之内的敌人`, + upgrade_OnExecutionDrainPower: `怜悯之击会使下一个技能有100%的机会获得+50%的技能强度`, + upgrade_OnHackSprintSpeed: `入侵后+75%冲刺速度,持续15秒`, + upgrade_SwiftExecute: `怜悯之击速度提升50%`, + upgrade_OnHackInvis: `入侵后隐身15秒`, prettier_sucks_ass: `` }; From 2e8fe799d74cfd0ac037cbe797845a8b636b5e0c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 17 Jun 2025 05:01:21 -0700 Subject: [PATCH 06/10] feat: setSuitInfection (#2174) Closes #2172 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2174 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/setSuitInfectionController.ts | 22 +++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 src/controllers/api/setSuitInfectionController.ts diff --git a/src/controllers/api/setSuitInfectionController.ts b/src/controllers/api/setSuitInfectionController.ts new file mode 100644 index 00000000..b4a169d9 --- /dev/null +++ b/src/controllers/api/setSuitInfectionController.ts @@ -0,0 +1,22 @@ +import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { RequestHandler } from "express"; + +export const setSuitInfectionController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Suits"); + const payload = getJSONfromString(String(req.body)); + for (const clientSuit of payload.Suits) { + const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!; + dbSuit.InfestationDate = fromMongoDate(clientSuit.InfestationDate!); + } + await inventory.save(); + res.end(); +}; + +interface ISetSuitInfectionRequest { + Suits: IEquipmentClient[]; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index a12efbd0..55c2fd7b 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -132,6 +132,7 @@ import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDeco import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController"; import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController"; import { setShipVignetteController } from "@/src/controllers/api/setShipVignetteController"; +import { setSuitInfectionController } from "@/src/controllers/api/setSuitInfectionController"; import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController"; import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController"; import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController"; @@ -317,6 +318,7 @@ apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController); apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController); apiRouter.post("/setShipVignette.php", setShipVignetteController); +apiRouter.post("/setSuitInfection.php", setSuitInfectionController); apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController); apiRouter.post("/shipDecorations.php", shipDecorationsController); apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController); From 01e490768c883184c34d10631ca779fba4cf1f7b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 17 Jun 2025 05:01:55 -0700 Subject: [PATCH 07/10] fix: ensure helminth shard operations don't produce a null shard (#2176) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2176 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/infestedFoundryController.ts | 8 +++++--- src/models/inventoryModels/inventoryModel.ts | 11 +++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index 4cc21061..495b4d8b 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -30,8 +30,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const request = getJSONfromString(String(req.body)); const inventory = await getInventory(account._id.toString()); const suit = inventory.Suits.id(request.SuitId.$oid)!; - if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { - suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; + suit.ArchonCrystalUpgrades ??= []; + while (suit.ArchonCrystalUpgrades.length < request.Slot) { + suit.ArchonCrystalUpgrades.push({}); } suit.ArchonCrystalUpgrades[request.Slot] = { UpgradeType: request.UpgradeType, @@ -92,7 +93,8 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { } // remove from suit - suit.ArchonCrystalUpgrades![request.Slot] = {}; + suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined; + suit.ArchonCrystalUpgrades![request.Slot].Color = undefined; await inventory.save(); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 2e4008dd..a9335fa7 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -251,12 +251,6 @@ const ArchonCrystalUpgradeSchema = new Schema( { _id: false } ); -ArchonCrystalUpgradeSchema.set("toJSON", { - transform(_document, returnedObject) { - delete returnedObject.__v; - } -}); - const boosterSchema = new Schema( { ExpiryDate: Number, @@ -1079,6 +1073,11 @@ EquipmentSchema.set("toJSON", { if (db.UmbraDate) { client.UmbraDate = toMongoDate(db.UmbraDate); } + + if (client.ArchonCrystalUpgrades) { + // For some reason, mongoose turns empty objects here into nulls, so we have to fix it. + client.ArchonCrystalUpgrades = client.ArchonCrystalUpgrades.map(x => (x as unknown) ?? {}); + } } }); From 6c2055a2465f8f84b7c1ca79e1e684ed1c080acd Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 17 Jun 2025 05:02:11 -0700 Subject: [PATCH 08/10] feat: echoes of umbra (#2177) Having this item in the inventory unlocks the helminth option which is helpfully called "remove cyst" to install and uninstall it on a frame. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2177 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/umbraController.ts | 27 ++++++++++++++++++++++++++ src/routes/api.ts | 2 ++ src/services/inventoryService.ts | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/umbraController.ts diff --git a/src/controllers/api/umbraController.ts b/src/controllers/api/umbraController.ts new file mode 100644 index 00000000..e89ca74d --- /dev/null +++ b/src/controllers/api/umbraController.ts @@ -0,0 +1,27 @@ +import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addMiscItem, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { RequestHandler } from "express"; + +export const umbraController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Suits MiscItems"); + const payload = getJSONfromString(String(req.body)); + for (const clientSuit of payload.Suits) { + const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!; + if (clientSuit.UmbraDate) { + addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/UmbraEchoes", -1); + dbSuit.UmbraDate = fromMongoDate(clientSuit.UmbraDate); + } else { + dbSuit.UmbraDate = undefined; + } + } + await inventory.save(); + res.end(); +}; + +interface IUmbraRequest { + Suits: IEquipmentClient[]; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 55c2fd7b..8b0a12bc 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -148,6 +148,7 @@ import { syndicateStandingBonusController } from "@/src/controllers/api/syndicat import { tauntHistoryController } from "@/src/controllers/api/tauntHistoryController"; import { tradingController } from "@/src/controllers/api/tradingController"; import { trainingResultController } from "@/src/controllers/api/trainingResultController"; +import { umbraController } from "@/src/controllers/api/umbraController"; import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController"; import { updateAlignmentController } from "@/src/controllers/api/updateAlignmentController"; import { updateChallengeProgressController } from "@/src/controllers/api/updateChallengeProgressController"; @@ -329,6 +330,7 @@ apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController); apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController); apiRouter.post("/tauntHistory.php", tauntHistoryController); apiRouter.post("/trainingResult.php", trainingResultController); +apiRouter.post("/umbra.php", umbraController); apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController); apiRouter.post("/updateAlignment.php", updateAlignmentController); apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ad53cad5..d598cbb1 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1580,7 +1580,7 @@ export const addMiscItem = ( inventory: TInventoryDatabaseDocument, type: string, count: number, - inventoryChanges: IInventoryChanges + inventoryChanges: IInventoryChanges = {} ): void => { const miscItemChanges: IMiscItem[] = [ { From 145d21e30e00c1a80ec8a58d8c5bf685db3b75f6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 17 Jun 2025 05:02:34 -0700 Subject: [PATCH 09/10] fix: weaken infested lich (#2181) Closes #2180 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2181 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/nemesisController.ts | 34 +++++++++++++++--------- src/helpers/nemesisHelpers.ts | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 8da8fe4d..fe536267 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -1,5 +1,6 @@ import { version_compare } from "@/src/helpers/inventoryHelpers"; import { + antivirusMods, consumeModCharge, decodeNemesisGuess, encodeNemesisGuess, @@ -134,34 +135,37 @@ export const nemesisController: RequestHandler = async (req, res) => { for (const upgrade of body.knife!.AttachedUpgrades) { switch (upgrade.ItemType) { case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod": - antivirusGain += 10; - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - break; case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod": antivirusGain += 10; consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); break; case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure - antivirusGain += 15; - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - break; case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield antivirusGain += 15; consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); break; - case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod": - antivirusGain += 10; - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - break; } } inventory.Nemesis!.HenchmenKilled += antivirusGain; + if (inventory.Nemesis!.HenchmenKilled >= 100) { + inventory.Nemesis!.HenchmenKilled = 100; + // Client doesn't seem to request mode=w for infested liches, so weakening it here. + inventory.Nemesis!.InfNodes = [ + { + Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode, + Influence: 1 + } + ]; + inventory.Nemesis!.Weakened = true; + const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, antivirusMods[passcode]); + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + } } - if (inventory.Nemesis!.HenchmenKilled >= 100) { - inventory.Nemesis!.HenchmenKilled = 100; + if (inventory.Nemesis!.HenchmenKilled < 100) { + inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0); } - inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0); await inventory.save(); res.json(response); @@ -283,6 +287,10 @@ export const nemesisController: RequestHandler = async (req, res) => { ); //const body = getJSONfromString(String(req.body)); + if (inventory.Nemesis!.Weakened) { + logger.warn(`client is weakening an already-weakened nemesis?!`); + } + inventory.Nemesis!.InfNodes = [ { Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode, diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 8d0d8527..0c7e5bdd 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -248,7 +248,7 @@ const requiemMods: readonly string[] = [ "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod" ]; -const antivirusMods: readonly string[] = [ +export const antivirusMods: readonly string[] = [ "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod", "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod", "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod", From 1ead5817803df0714202faaec4afe2783bfa3745 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 17 Jun 2025 05:02:58 -0700 Subject: [PATCH 10/10] chore: improve typing of IFocusXp (#2182) Any given focus school can be undefined in this object due to importing. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2182 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/focusController.ts | 14 ++++++----- src/services/inventoryService.ts | 27 +++++++++++++++++----- src/types/inventoryTypes/inventoryTypes.ts | 10 ++++---- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts index 90b55a2e..c7f96a6d 100644 --- a/src/controllers/api/focusController.ts +++ b/src/controllers/api/focusController.ts @@ -43,7 +43,7 @@ export const focusController: RequestHandler = async (req, res) => { inventory.FocusAbility ??= focusType; inventory.FocusUpgrades.push({ ItemType: focusType }); if (inventory.FocusXP) { - inventory.FocusXP[focusPolarity] -= cost; + inventory.FocusXP[focusPolarity]! -= cost; } await inventory.save(); res.json({ @@ -78,7 +78,7 @@ export const focusController: RequestHandler = async (req, res) => { cost += ExportFocusUpgrades[focusType].baseFocusPointCost; inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 }); } - inventory.FocusXP![focusPolarity] -= cost; + inventory.FocusXP![focusPolarity]! -= cost; await inventory.save(); res.json({ FocusTypes: request.FocusTypes, @@ -96,7 +96,7 @@ export const focusController: RequestHandler = async (req, res) => { const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!; focusUpgradeDb.Level = focusUpgrade.Level; } - inventory.FocusXP![focusPolarity] -= cost; + inventory.FocusXP![focusPolarity]! -= cost; await inventory.save(); res.json({ FocusInfos: request.FocusInfos, @@ -123,7 +123,7 @@ export const focusController: RequestHandler = async (req, res) => { const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest; const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]); const inventory = await getInventory(accountId); - inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length; + inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length; addMiscItems(inventory, [ { ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem", @@ -168,8 +168,10 @@ export const focusController: RequestHandler = async (req, res) => { shard.ItemCount *= -1; } const inventory = await getInventory(accountId); - inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 }; - inventory.FocusXP[request.Polarity] += xp; + const polarity = request.Polarity; + inventory.FocusXP ??= {}; + inventory.FocusXP[polarity] ??= 0; + inventory.FocusXP[polarity] += xp; addMiscItems(inventory, request.Shards); await inventory.save(); break; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index d598cbb1..38c197f8 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1731,12 +1731,27 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus AP_ANY } - inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 }; - inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK]; - inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE]; - inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC]; - inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; - inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; + inventory.FocusXP ??= {}; + if (focusXpPlus[FocusType.AP_ATTACK]) { + inventory.FocusXP.AP_ATTACK ??= 0; + inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK]; + } + if (focusXpPlus[FocusType.AP_DEFENSE]) { + inventory.FocusXP.AP_DEFENSE ??= 0; + inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE]; + } + if (focusXpPlus[FocusType.AP_TACTIC]) { + inventory.FocusXP.AP_TACTIC ??= 0; + inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC]; + } + if (focusXpPlus[FocusType.AP_POWER]) { + inventory.FocusXP.AP_POWER ??= 0; + inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; + } + if (focusXpPlus[FocusType.AP_WARD]) { + inventory.FocusXP.AP_WARD ??= 0; + inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; + } if (!config.noDailyFocusLimit) { inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 997b50f4..94149f7c 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -641,11 +641,11 @@ export interface IFocusUpgrade { } export interface IFocusXP { - AP_POWER: number; - AP_TACTIC: number; - AP_DEFENSE: number; - AP_ATTACK: number; - AP_WARD: number; + AP_POWER?: number; + AP_TACTIC?: number; + AP_DEFENSE?: number; + AP_ATTACK?: number; + AP_WARD?: number; } export type TFocusPolarity = keyof IFocusXP;