feat: eleanor weapon offerings #1419

Merged
Sainan merged 5 commits from lich-weapons into main 2025-04-01 15:49:09 -07:00
10 changed files with 247 additions and 82 deletions

View File

@ -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;

View File

@ -1,9 +1,8 @@
import express from "express"; import express from "express";
import path from "path"; import path from "path";
import { repoDir, rootDir } from "@/src/helpers/pathHelper";
const webuiRouter = express.Router(); const webuiRouter = express.Router();
const rootDir = path.join(__dirname, "../..");
const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir;
// Redirect / to /webui/ // Redirect / to /webui/
webuiRouter.get("/", (_req, res) => { webuiRouter.get("/", (_req, res) => {

View File

@ -1,5 +1,6 @@
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import { repoDir } from "@/src/helpers/pathHelper";
interface IBuildConfig { interface IBuildConfig {
version: string; version: string;
@ -13,8 +14,6 @@ export const buildConfig: IBuildConfig = {
matchmakingBuildId: "" 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"); const buildConfigPath = path.join(repoDir, "static/data/buildConfig.json");
if (fs.existsSync(buildConfigPath)) { if (fs.existsSync(buildConfigPath)) {
Object.assign(buildConfig, JSON.parse(fs.readFileSync(buildConfigPath, "utf-8")) as IBuildConfig); Object.assign(buildConfig, JSON.parse(fs.readFileSync(buildConfigPath, "utf-8")) as IBuildConfig);

View File

@ -1,10 +1,9 @@
import path from "path";
import fs from "fs"; import fs from "fs";
import fsPromises from "fs/promises"; import fsPromises from "fs/promises";
import path from "path";
import { repoDir } from "@/src/helpers/pathHelper";
import { logger } from "@/src/utils/logger"; 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"); const configPath = path.join(repoDir, "config.json");
export const config = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig; export const config = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig;

View File

@ -69,7 +69,7 @@ import { addStartingGear } from "@/src/controllers/api/giveStartingGearControlle
import { addQuestKey, completeQuest } from "@/src/services/questService"; import { addQuestKey, completeQuest } from "@/src/services/questService";
import { handleBundleAcqusition } from "./purchaseService"; import { handleBundleAcqusition } from "./purchaseService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { getRandomElement, getRandomInt } from "./rngService"; import { getRandomElement, getRandomInt, SRng } from "./rngService";
import { createMessage } from "./inboxService"; import { createMessage } from "./inboxService";
export const createInventory = async ( export const createInventory = async (
@ -230,7 +230,8 @@ export const addItem = async (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
typeName: string, typeName: string,
quantity: number = 1, quantity: number = 1,
premiumPurchase: boolean = false premiumPurchase: boolean = false,
seed?: bigint
): Promise<IInventoryChanges> => { ): Promise<IInventoryChanges> => {
// 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. // 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) { if (typeName in ExportBundles) {
@ -380,21 +381,31 @@ export const addItem = async (
defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY; defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY;
} }
if (weapon.maxLevelCap == 40 && typeName.indexOf("BallasSword") == -1) { 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.UpgradeType = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod";
defaultOverwrites.UpgradeFingerprint = JSON.stringify({ defaultOverwrites.UpgradeFingerprint = JSON.stringify({
compat: typeName, compat: typeName,
buffs: [ buffs: [
{ {
Tag: getRandomElement([ Tag: tag,
"InnateElectricityDamage", Value: Math.trunc(value * 0x40000000)
"InnateFreezeDamage",
"InnateHeatDamage",
"InnateImpactDamage",
"InnateMagDamage",
"InnateRadDamage",
"InnateToxinDamage"
]),
Value: Math.trunc(Math.random() * 0x40000000)
} }
] ]
}); });

View File

@ -51,6 +51,7 @@ export const handlePurchase = async (
logger.debug("purchase request", purchaseRequest); logger.debug("purchase request", purchaseRequest);
const prePurchaseInventoryChanges: IInventoryChanges = {}; const prePurchaseInventoryChanges: IInventoryChanges = {};
let seed: bigint | undefined;
if (purchaseRequest.PurchaseParams.Source == 7) { if (purchaseRequest.PurchaseParams.Source == 7) {
const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
if (rawManifest) { if (rawManifest) {
@ -74,6 +75,9 @@ export const handlePurchase = async (
prePurchaseInventoryChanges prePurchaseInventoryChanges
); );
} }
if (offer.LocTagRandSeed !== undefined) {
seed = BigInt(offer.LocTagRandSeed);
}
if (!config.noVendorPurchaseLimits && ItemId) { if (!config.noVendorPurchaseLimits && ItemId) {
inventory.RecentVendorPurchases ??= []; inventory.RecentVendorPurchases ??= [];
let vendorPurchases = inventory.RecentVendorPurchases.find( let vendorPurchases = inventory.RecentVendorPurchases.find(
@ -136,7 +140,10 @@ export const handlePurchase = async (
const purchaseResponse = await handleStoreItemAcquisition( const purchaseResponse = await handleStoreItemAcquisition(
purchaseRequest.PurchaseParams.StoreItem, purchaseRequest.PurchaseParams.StoreItem,
inventory, inventory,
purchaseRequest.PurchaseParams.Quantity purchaseRequest.PurchaseParams.Quantity,
undefined,
undefined,
seed
); );
combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges);
@ -324,7 +331,8 @@ export const handleStoreItemAcquisition = async (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
quantity: number = 1, quantity: number = 1,
durability: TRarity = "COMMON", durability: TRarity = "COMMON",
ignorePurchaseQuantity: boolean = false ignorePurchaseQuantity: boolean = false,
seed?: bigint
): Promise<IPurchaseResponse> => { ): Promise<IPurchaseResponse> => {
let purchaseResponse = { let purchaseResponse = {
InventoryChanges: {} InventoryChanges: {}
@ -345,7 +353,7 @@ export const handleStoreItemAcquisition = async (
} }
switch (storeCategory) { switch (storeCategory) {
default: { default: {
purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true) }; purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) };
break; break;
} }
case "Types": case "Types":

View File

@ -127,4 +127,13 @@ export class SRng {
} }
return min; return min;
} }
randomElement<T>(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;
}
} }

View File

@ -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 { CRng, mixSeeds } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { IVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; import { IVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes";
import { JSONParse } from "json-with-bigint";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; const getVendorManifestJson = (name: string): IVendorManifest => {
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8"));
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 vendorManifests: IVendorManifest[] = [ const vendorManifests: IVendorManifest[] = [
ArchimedeanVendorManifest, getVendorManifestJson("ArchimedeanVendorManifest"),
DeimosEntratiFragmentVendorProductsManifest, getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"),
DeimosFishmongerVendorManifest, getVendorManifestJson("DeimosFishmongerVendorManifest"),
DeimosHivemindCommisionsManifestFishmonger, getVendorManifestJson("DeimosHivemindCommisionsManifestFishmonger"),
DeimosHivemindCommisionsManifestPetVendor, getVendorManifestJson("DeimosHivemindCommisionsManifestPetVendor"),
DeimosHivemindCommisionsManifestProspector, getVendorManifestJson("DeimosHivemindCommisionsManifestProspector"),
DeimosHivemindCommisionsManifestTokenVendor, getVendorManifestJson("DeimosHivemindCommisionsManifestTokenVendor"),
DeimosHivemindCommisionsManifestWeaponsmith, getVendorManifestJson("DeimosHivemindCommisionsManifestWeaponsmith"),
DeimosHivemindTokenVendorManifest, getVendorManifestJson("DeimosHivemindTokenVendorManifest"),
DeimosPetVendorManifest, getVendorManifestJson("DeimosPetVendorManifest"),
DeimosProspectorVendorManifest, getVendorManifestJson("DeimosProspectorVendorManifest"),
DuviriAcrithisVendorManifest, getVendorManifestJson("DuviriAcrithisVendorManifest"),
EntratiLabsEntratiLabsCommisionsManifest, getVendorManifestJson("EntratiLabsEntratiLabsCommisionsManifest"),
EntratiLabsEntratiLabVendorManifest, getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"),
GuildAdvertisementVendorManifest, // uses preprocessing getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing
HubsIronwakeDondaVendorManifest, // uses preprocessing getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing
HubsPerrinSequenceWeaponVendorManifest, getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"),
HubsRailjackCrewMemberVendorManifest, getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"),
MaskSalesmanManifest, getVendorManifestJson("InfestedLichWeaponVendorManifest"),
Nova1999ConquestShopManifest, getVendorManifestJson("MaskSalesmanManifest"),
OstronFishmongerVendorManifest, getVendorManifestJson("Nova1999ConquestShopManifest"),
OstronPetVendorManifest, getVendorManifestJson("OstronFishmongerVendorManifest"),
OstronProspectorVendorManifest, getVendorManifestJson("OstronPetVendorManifest"),
RadioLegionIntermission12VendorManifest, getVendorManifestJson("OstronProspectorVendorManifest"),
SolarisDebtTokenVendorManifest, getVendorManifestJson("RadioLegionIntermission12VendorManifest"),
SolarisDebtTokenVendorRepossessionsManifest, getVendorManifestJson("SolarisDebtTokenVendorManifest"),
SolarisFishmongerVendorManifest, getVendorManifestJson("SolarisDebtTokenVendorRepossessionsManifest"),
SolarisProspectorVendorManifest, getVendorManifestJson("SolarisFishmongerVendorManifest"),
TeshinHardModeVendorManifest, // uses preprocessing getVendorManifestJson("SolarisProspectorVendorManifest"),
ZarimanCommisionsManifestArchimedean getVendorManifestJson("TeshinHardModeVendorManifest"), // uses preprocessing
getVendorManifestJson("ZarimanCommisionsManifestArchimedean")
]; ];
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {

View File

@ -19,6 +19,7 @@ interface IItemManifest {
PurchaseQuantityLimit?: number; PurchaseQuantityLimit?: number;
RotatedWeekly?: boolean; RotatedWeekly?: boolean;
AllowMultipurchase: boolean; AllowMultipurchase: boolean;
LocTagRandSeed?: number | bigint;
Id: IOid; Id: IOid;
} }

View File

@ -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"
}
}
}
}