feat: infested lich rewards (#1898)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled

Closes #1884

Reviewed-on: #1898
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-04-28 14:00:38 -07:00 committed by Sainan
parent 66ee550ccd
commit 9042e85355
4 changed files with 127 additions and 7 deletions

View File

@ -13,6 +13,7 @@ import { addItems, combineInventoryChanges, getInventory } from "@/src/services/
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFlavour, ExportGear } from "warframe-public-export-plus"; import { ExportFlavour, ExportGear } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
export const inboxController: RequestHandler = async (req, res) => { export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
@ -48,7 +49,7 @@ export const inboxController: RequestHandler = async (req, res) => {
await addItems( await addItems(
inventory, inventory,
attachmentItems.map(attItem => ({ attachmentItems.map(attItem => ({
ItemType: attItem, ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem,
ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1 ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
})), })),
inventoryChanges inventoryChanges

View File

@ -1,12 +1,14 @@
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus"; import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { SRng } from "@/src/services/rngService"; import { getRewardAtPercentage, SRng } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { IOid } from "../types/commonTypes"; import { IOid } from "../types/commonTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { addMods } from "../services/inventoryService"; import { addMods, generateRewardSeed } from "../services/inventoryService";
import { isArchwingMission } from "../services/worldStateService"; import { isArchwingMission } from "../services/worldStateService";
import { fromStoreItem, toStoreItem } from "../services/itemDataService";
import { createMessage } from "../services/inboxService";
export const getInfNodes = (faction: string, rank: number): IInfNode[] => { export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
const infNodes = []; const infNodes = [];
@ -340,3 +342,109 @@ export const getInnateDamageValue = (fp: bigint): number => {
} }
return Math.trunc(value * 0x40000000); return Math.trunc(value * 0x40000000);
}; };
export const getKillTokenRewardCount = (fp: bigint): number => {
const rng = new SRng(fp);
return rng.randomInt(10, 15);
};
// /Lotus/Types/Enemies/InfestedLich/InfestedLichRewardManifest
const infestedLichRotA = [
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterA", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterB", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandDespairPoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandGridPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandHuddlePoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandJumpPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLimoPoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterDay", probability: 0.046 },
{
type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterNight",
probability: 0.045
},
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandSillyPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhiteBluePoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhitePinkPoster", probability: 0.045 }
];
const infestedLichRotB = [
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraA", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraB", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraC", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraD", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraE", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraF", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraG", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraH", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDJRomHype", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DancePacketWindmillShuffle", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceHarddrivePony", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDrillbitCrisscross", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceZekeCanthavethis", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/PhotoBooth/PhotoboothTileRJLasXStadiumBossArena", probability: 0.071 }
];
export const getInfestedLichItemRewards = (fp: bigint): string[] => {
const rng = new SRng(fp);
const rotAReward = getRewardAtPercentage(infestedLichRotA, rng.randomFloat())!.type;
rng.randomFloat(); // unused afaict
const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type;
return [rotAReward, rotBReward];
};
export const sendCodaFinishedMessage = async (
inventory: TInventoryDatabaseDocument,
fp: bigint = generateRewardSeed(),
name: string = "ZEKE_BEATWOMAN_TM.1999",
killed: boolean = true
): Promise<void> => {
const att: string[] = [];
// First vanquish/convert gives a sigil
const sigil = killed
? "/Lotus/Upgrades/Skins/Sigils/InfLichVanquishedSigil"
: "/Lotus/Upgrades/Skins/Sigils/InfLichConvertedSigil";
if (!inventory.WeaponSkins.find(x => x.ItemType == sigil)) {
att.push(toStoreItem(sigil));
}
const [rotAReward, rotBReward] = getInfestedLichItemRewards(fp);
att.push(fromStoreItem(rotAReward));
att.push(fromStoreItem(rotBReward));
let countedAtt: ITypeCount[] | undefined;
if (killed) {
countedAtt = [
{
ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks",
ItemCount: getKillTokenRewardCount(fp)
}
];
}
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/VanquishBandMsgBody",
arg: [
{
Key: "LICH_NAME",
Tag: name
}
],
att: att,
countedAtt: countedAtt,
sub: "/Lotus/Language/Inbox/VanquishBandMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
};

View File

@ -55,7 +55,7 @@ import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.
import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json";
import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json";
import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json"; import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json";
import { getInfNodes, getWeaponsForManifest } from "@/src/helpers/nemesisHelpers"; import { getInfNodes, getWeaponsForManifest, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers";
import { Loadout } from "../models/inventoryModels/loadoutModel"; import { Loadout } from "../models/inventoryModels/loadoutModel";
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService"; import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService";
@ -639,7 +639,10 @@ export const addMissionInventoryUpdates = async (
}); });
if (value.killed) { if (value.killed) {
if (value.weaponLoc) { if (
value.weaponLoc &&
inventory.Nemesis.Faction != "FC_INFESTATION" // weaponLoc is "/Lotus/Language/Weapons/DerelictCernosName" for these for some reason
) {
const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[ const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[
inventory.Nemesis.WeaponIdx inventory.Nemesis.WeaponIdx
]; ];
@ -657,6 +660,11 @@ export const addMissionInventoryUpdates = async (
} }
} }
// TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given?
if (inventory.Nemesis.Faction == "FC_INFESTATION") {
await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed);
}
inventory.Nemesis = undefined; inventory.Nemesis = undefined;
} }
break; break;

View File

@ -18,7 +18,10 @@ export const getRandomInt = (min: number, max: number): number => {
return Math.floor(Math.random() * (max - min + 1)) + min; return Math.floor(Math.random() * (max - min + 1)) + min;
}; };
const getRewardAtPercentage = <T extends { probability: number }>(pool: T[], percentage: number): T | undefined => { export const getRewardAtPercentage = <T extends { probability: number }>(
pool: T[],
percentage: number
): T | undefined => {
if (pool.length == 0) return; if (pool.length == 0) return;
const totalChance = pool.reduce((accum, item) => accum + item.probability, 0); const totalChance = pool.reduce((accum, item) => accum + item.probability, 0);