From bf67a4391d5f70dc016fd36ea3401afa0c653190 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:49:08 -0700 Subject: [PATCH] feat: eleanor weapon offerings (#1419) Need to do rotating offers for her some other time Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1419 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/helpers/pathHelper.ts | 4 + src/routes/webui.ts | 3 +- src/services/buildConfigService.ts | 3 +- src/services/configService.ts | 5 +- src/services/inventoryService.ts | 35 ++-- src/services/purchaseService.ts | 14 +- src/services/rngService.ts | 9 + src/services/serversideVendorsService.ts | 98 +++++------ src/types/vendorTypes.ts | 1 + .../InfestedLichWeaponVendorManifest.json | 157 ++++++++++++++++++ 10 files changed, 247 insertions(+), 82 deletions(-) create mode 100644 src/helpers/pathHelper.ts create mode 100644 static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json diff --git a/src/helpers/pathHelper.ts b/src/helpers/pathHelper.ts new file mode 100644 index 00000000..95621f6a --- /dev/null +++ b/src/helpers/pathHelper.ts @@ -0,0 +1,4 @@ +import path from "path"; + +export const rootDir = path.join(__dirname, "../.."); +export const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; diff --git a/src/routes/webui.ts b/src/routes/webui.ts index 48f9f2fd..2cfa14da 100644 --- a/src/routes/webui.ts +++ b/src/routes/webui.ts @@ -1,9 +1,8 @@ import express from "express"; import path from "path"; +import { repoDir, rootDir } from "@/src/helpers/pathHelper"; const webuiRouter = express.Router(); -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; // Redirect / to /webui/ webuiRouter.get("/", (_req, res) => { diff --git a/src/services/buildConfigService.ts b/src/services/buildConfigService.ts index dda9b909..007b1bed 100644 --- a/src/services/buildConfigService.ts +++ b/src/services/buildConfigService.ts @@ -1,5 +1,6 @@ import path from "path"; import fs from "fs"; +import { repoDir } from "@/src/helpers/pathHelper"; interface IBuildConfig { version: string; @@ -13,8 +14,6 @@ export const buildConfig: IBuildConfig = { matchmakingBuildId: "" }; -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; const buildConfigPath = path.join(repoDir, "static/data/buildConfig.json"); if (fs.existsSync(buildConfigPath)) { Object.assign(buildConfig, JSON.parse(fs.readFileSync(buildConfigPath, "utf-8")) as IBuildConfig); diff --git a/src/services/configService.ts b/src/services/configService.ts index 3aaa2796..6ceaf9d0 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -1,10 +1,9 @@ -import path from "path"; import fs from "fs"; import fsPromises from "fs/promises"; +import path from "path"; +import { repoDir } from "@/src/helpers/pathHelper"; import { logger } from "@/src/utils/logger"; -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; const configPath = path.join(repoDir, "config.json"); export const config = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 6eba1872..e62e51b9 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -69,7 +69,7 @@ import { addStartingGear } from "@/src/controllers/api/giveStartingGearControlle import { addQuestKey, completeQuest } from "@/src/services/questService"; import { handleBundleAcqusition } from "./purchaseService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; -import { getRandomElement, getRandomInt } from "./rngService"; +import { getRandomElement, getRandomInt, SRng } from "./rngService"; import { createMessage } from "./inboxService"; export const createInventory = async ( @@ -230,7 +230,8 @@ export const addItem = async ( inventory: TInventoryDatabaseDocument, typeName: string, quantity: number = 1, - premiumPurchase: boolean = false + premiumPurchase: boolean = false, + seed?: bigint ): Promise => { // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments. if (typeName in ExportBundles) { @@ -380,21 +381,31 @@ export const addItem = async ( defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY; } if (weapon.maxLevelCap == 40 && typeName.indexOf("BallasSword") == -1) { + if (!seed) { + seed = BigInt(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)); + } + const rng = new SRng(seed); + const tag = rng.randomElement([ + "InnateElectricityDamage", + "InnateFreezeDamage", + "InnateHeatDamage", + "InnateImpactDamage", + "InnateMagDamage", + "InnateRadDamage", + "InnateToxinDamage" + ]); + const WeaponUpgradeValueAttenuationExponent = 2.25; + let value = Math.pow(rng.randomFloat(), WeaponUpgradeValueAttenuationExponent); + if (value >= 0.941428) { + value = 1; + } defaultOverwrites.UpgradeType = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod"; defaultOverwrites.UpgradeFingerprint = JSON.stringify({ compat: typeName, buffs: [ { - Tag: getRandomElement([ - "InnateElectricityDamage", - "InnateFreezeDamage", - "InnateHeatDamage", - "InnateImpactDamage", - "InnateMagDamage", - "InnateRadDamage", - "InnateToxinDamage" - ]), - Value: Math.trunc(Math.random() * 0x40000000) + Tag: tag, + Value: Math.trunc(value * 0x40000000) } ] }); diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index f2b71427..04525704 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -51,6 +51,7 @@ export const handlePurchase = async ( logger.debug("purchase request", purchaseRequest); const prePurchaseInventoryChanges: IInventoryChanges = {}; + let seed: bigint | undefined; if (purchaseRequest.PurchaseParams.Source == 7) { const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); if (rawManifest) { @@ -74,6 +75,9 @@ export const handlePurchase = async ( prePurchaseInventoryChanges ); } + if (offer.LocTagRandSeed !== undefined) { + seed = BigInt(offer.LocTagRandSeed); + } if (!config.noVendorPurchaseLimits && ItemId) { inventory.RecentVendorPurchases ??= []; let vendorPurchases = inventory.RecentVendorPurchases.find( @@ -136,7 +140,10 @@ export const handlePurchase = async ( const purchaseResponse = await handleStoreItemAcquisition( purchaseRequest.PurchaseParams.StoreItem, inventory, - purchaseRequest.PurchaseParams.Quantity + purchaseRequest.PurchaseParams.Quantity, + undefined, + undefined, + seed ); combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); @@ -324,7 +331,8 @@ export const handleStoreItemAcquisition = async ( inventory: TInventoryDatabaseDocument, quantity: number = 1, durability: TRarity = "COMMON", - ignorePurchaseQuantity: boolean = false + ignorePurchaseQuantity: boolean = false, + seed?: bigint ): Promise => { let purchaseResponse = { InventoryChanges: {} @@ -345,7 +353,7 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true) }; + purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) }; break; } case "Types": diff --git a/src/services/rngService.ts b/src/services/rngService.ts index cb8f2cba..b98f7bd3 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -127,4 +127,13 @@ export class SRng { } return min; } + + randomElement(arr: T[]): T { + return arr[this.randomInt(0, arr.length - 1)]; + } + + randomFloat(): number { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645; + } } diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 37a3425c..9700e00e 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,69 +1,47 @@ +import fs from "fs"; +import path from "path"; +import { repoDir } from "@/src/helpers/pathHelper"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; import { IVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; +import { JSONParse } from "json-with-bigint"; -import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; -import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; -import DeimosFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json"; -import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json"; -import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json"; -import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json"; -import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json"; -import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json"; -import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json"; -import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json"; -import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json"; -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 GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json"; -import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json"; -import HubsPerrinSequenceWeaponVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.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 OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json"; -import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; -import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; -import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json"; -import SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json"; -import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; -import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json"; -import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; -import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; -import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; +const getVendorManifestJson = (name: string): IVendorManifest => { + return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); +}; const vendorManifests: IVendorManifest[] = [ - ArchimedeanVendorManifest, - DeimosEntratiFragmentVendorProductsManifest, - DeimosFishmongerVendorManifest, - DeimosHivemindCommisionsManifestFishmonger, - DeimosHivemindCommisionsManifestPetVendor, - DeimosHivemindCommisionsManifestProspector, - DeimosHivemindCommisionsManifestTokenVendor, - DeimosHivemindCommisionsManifestWeaponsmith, - DeimosHivemindTokenVendorManifest, - DeimosPetVendorManifest, - DeimosProspectorVendorManifest, - DuviriAcrithisVendorManifest, - EntratiLabsEntratiLabsCommisionsManifest, - EntratiLabsEntratiLabVendorManifest, - GuildAdvertisementVendorManifest, // uses preprocessing - HubsIronwakeDondaVendorManifest, // uses preprocessing - HubsPerrinSequenceWeaponVendorManifest, - HubsRailjackCrewMemberVendorManifest, - MaskSalesmanManifest, - Nova1999ConquestShopManifest, - OstronFishmongerVendorManifest, - OstronPetVendorManifest, - OstronProspectorVendorManifest, - RadioLegionIntermission12VendorManifest, - SolarisDebtTokenVendorManifest, - SolarisDebtTokenVendorRepossessionsManifest, - SolarisFishmongerVendorManifest, - SolarisProspectorVendorManifest, - TeshinHardModeVendorManifest, // uses preprocessing - ZarimanCommisionsManifestArchimedean + getVendorManifestJson("ArchimedeanVendorManifest"), + getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"), + getVendorManifestJson("DeimosFishmongerVendorManifest"), + getVendorManifestJson("DeimosHivemindCommisionsManifestFishmonger"), + getVendorManifestJson("DeimosHivemindCommisionsManifestPetVendor"), + getVendorManifestJson("DeimosHivemindCommisionsManifestProspector"), + getVendorManifestJson("DeimosHivemindCommisionsManifestTokenVendor"), + getVendorManifestJson("DeimosHivemindCommisionsManifestWeaponsmith"), + getVendorManifestJson("DeimosHivemindTokenVendorManifest"), + getVendorManifestJson("DeimosPetVendorManifest"), + getVendorManifestJson("DeimosProspectorVendorManifest"), + getVendorManifestJson("DuviriAcrithisVendorManifest"), + getVendorManifestJson("EntratiLabsEntratiLabsCommisionsManifest"), + getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"), + getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing + getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing + getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"), + getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"), + getVendorManifestJson("InfestedLichWeaponVendorManifest"), + getVendorManifestJson("MaskSalesmanManifest"), + getVendorManifestJson("Nova1999ConquestShopManifest"), + getVendorManifestJson("OstronFishmongerVendorManifest"), + getVendorManifestJson("OstronPetVendorManifest"), + getVendorManifestJson("OstronProspectorVendorManifest"), + getVendorManifestJson("RadioLegionIntermission12VendorManifest"), + getVendorManifestJson("SolarisDebtTokenVendorManifest"), + getVendorManifestJson("SolarisDebtTokenVendorRepossessionsManifest"), + getVendorManifestJson("SolarisFishmongerVendorManifest"), + getVendorManifestJson("SolarisProspectorVendorManifest"), + getVendorManifestJson("TeshinHardModeVendorManifest"), // uses preprocessing + getVendorManifestJson("ZarimanCommisionsManifestArchimedean") ]; export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index d7dbd749..0aa5b83e 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -19,6 +19,7 @@ interface IItemManifest { PurchaseQuantityLimit?: number; RotatedWeekly?: boolean; AllowMultipurchase: boolean; + LocTagRandSeed?: number | bigint; Id: IOid; } diff --git a/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json b/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json new file mode 100644 index 00000000..b3b419ba --- /dev/null +++ b/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json @@ -0,0 +1,157 @@ +{ + "VendorInfo": { + "_id": { + "$oid": "67dadc30e4b6e0e5979c8d84" + }, + "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/LongGuns/1999InfShotgun/1999InfShotgunWeapon", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 65079176837546984, + "Id": { + "$oid": "67e9da12793a120dbbc1c193" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaCaustacyst/CodaCaustacyst", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 5687904240491804000, + "Id": { + "$oid": "67e9da12793a120dbbc1c194" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaPathocyst/CodaPathocyst", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 6177144662234093000, + "Id": { + "$oid": "67e9da12793a120dbbc1c195" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Pistols/CodaTysis", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 1988275604378227700, + "Id": { + "$oid": "67e9da12793a120dbbc1c196" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/LongGuns/CodaSynapse", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 8607452585593957000, + "Id": { + "$oid": "67e9da12793a120dbbc1c197" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaHirudo", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 8385013066220909000, + "Id": { + "$oid": "67e9da12793a120dbbc1c198" + } + } + ], + "PropertyTextHash": "77093DD05A8561A022DEC9A4B9BB4A56", + "RandomSeedType": "VRST_WEAPON", + "RequiredGoalTag": "", + "WeaponUpgradeValueAttenuationExponent": 2.25, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + } + } +} \ No newline at end of file