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 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) => {

View File

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

View File

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

View File

@ -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<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.
if (typeName in ExportBundles) {
@ -380,12 +381,11 @@ export const addItem = async (
defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY;
}
if (weapon.maxLevelCap == 40 && typeName.indexOf("BallasSword") == -1) {
defaultOverwrites.UpgradeType = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod";
defaultOverwrites.UpgradeFingerprint = JSON.stringify({
compat: typeName,
buffs: [
{
Tag: getRandomElement([
if (!seed) {
seed = BigInt(Math.round(Math.random() * Number.MAX_SAFE_INTEGER));
}
const rng = new SRng(seed);
const tag = rng.randomElement([
"InnateElectricityDamage",
"InnateFreezeDamage",
"InnateHeatDamage",
@ -393,8 +393,19 @@ export const addItem = async (
"InnateMagDamage",
"InnateRadDamage",
"InnateToxinDamage"
]),
Value: Math.trunc(Math.random() * 0x40000000)
]);
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: tag,
Value: Math.trunc(value * 0x40000000)
}
]
});

View File

@ -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<IPurchaseResponse> => {
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":

View File

@ -127,4 +127,13 @@ export class SRng {
}
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 { 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 => {

View File

@ -19,6 +19,7 @@ interface IItemManifest {
PurchaseQuantityLimit?: number;
RotatedWeekly?: boolean;
AllowMultipurchase: boolean;
LocTagRandSeed?: number | bigint;
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"
}
}
}
}