feat: classic lich vanquish inbox mesage (#2074)
All checks were successful
Build Docker image / docker (push) Successful in 1m15s
Build / build (push) Successful in 1m41s

Closes #1897

Reviewed-on: #2074
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-05-14 21:14:04 -07:00 committed by Sainan
parent daf721f7cd
commit 52c8802d57
3 changed files with 121 additions and 110 deletions

View File

@ -7,7 +7,7 @@ import {
getNemesisPasscodeModTypes,
getWeaponsForManifest,
IKnifeResponse,
showdownNodes
nemesisFactionInfos
} from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
@ -223,7 +223,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
inventory.Nemesis!.InfNodes = [
{
Node: showdownNodes[inventory.Nemesis!.Faction],
Node: nemesisFactionInfos[inventory.Nemesis!.Faction].showdownNode,
Influence: 1
}
];

View File

@ -1,5 +1,5 @@
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
import { IInfNode, ITypeCount, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInfNode, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes";
import { getRewardAtPercentage, SRng } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { logger } from "../utils/logger";
@ -7,13 +7,51 @@ import { IOid } from "../types/commonTypes";
import { Types } from "mongoose";
import { addMods, generateRewardSeed } from "../services/inventoryService";
import { isArchwingMission } from "../services/worldStateService";
import { fromStoreItem, toStoreItem } from "../services/itemDataService";
import { createMessage } from "../services/inboxService";
import { version_compare } from "./inventoryHelpers";
export interface INemesisFactionInfo {
systemIndexes: number[];
showdownNode: string;
ephemeraChance: number;
firstKillReward: string;
firstConvertReward: string;
messageTitle: string;
messageBody: string;
}
export const nemesisFactionInfos: Record<TNemesisFaction, INemesisFactionInfo> = {
FC_GRINEER: {
systemIndexes: [2, 3, 9, 11, 18],
showdownNode: "CrewBattleNode557",
ephemeraChance: 0.05,
firstKillReward: "/Lotus/StoreItems/Upgrades/Skins/Clan/LichKillerBadgeItem",
firstConvertReward: "/Lotus/StoreItems/Upgrades/Skins/Sigils/KuvaLichSigil",
messageTitle: "/Lotus/Language/Inbox/VanquishKuvaMsgTitle",
messageBody: "/Lotus/Language/Inbox/VanquishLichMsgBody"
},
FC_CORPUS: {
systemIndexes: [1, 15, 4, 7, 8],
showdownNode: "CrewBattleNode558",
ephemeraChance: 0.2,
firstKillReward: "/Lotus/StoreItems/Upgrades/Skins/Clan/CorpusLichBadgeItem",
firstConvertReward: "/Lotus/StoreItems/Upgrades/Skins/Sigils/CorpusLichSigil",
messageTitle: "/Lotus/Language/Inbox/VanquishLawyerMsgTitle",
messageBody: "/Lotus/Language/Inbox/VanquishLichMsgBody"
},
FC_INFESTATION: {
systemIndexes: [23],
showdownNode: "CrewBattleNode559",
ephemeraChance: 0,
firstKillReward: "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichVanquishedSigil",
firstConvertReward: "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichConvertedSigil",
messageTitle: "/Lotus/Language/Inbox/VanquishBandMsgTitle",
messageBody: "/Lotus/Language/Inbox/VanquishBandMsgBody"
}
};
export const getInfNodes = (faction: TNemesisFaction, rank: number): IInfNode[] => {
const infNodes = [];
const systemIndex = systemIndexes[faction][rank];
const systemIndex = nemesisFactionInfos[faction].systemIndexes[rank];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex === systemIndex &&
@ -35,24 +73,6 @@ export const getInfNodes = (faction: TNemesisFaction, rank: number): IInfNode[]
return infNodes;
};
const systemIndexes: Record<TNemesisFaction, number[]> = {
FC_GRINEER: [2, 3, 9, 11, 18],
FC_CORPUS: [1, 15, 4, 7, 8],
FC_INFESTATION: [23]
};
export const showdownNodes: Record<TNemesisFaction, string> = {
FC_GRINEER: "CrewBattleNode557",
FC_CORPUS: "CrewBattleNode558",
FC_INFESTATION: "CrewBattleNode559"
};
const ephemeraProbabilities: Record<TNemesisFaction, number> = {
FC_GRINEER: 0.05,
FC_CORPUS: 0.2,
FC_INFESTATION: 0
};
type TInnateDamageTag =
| "InnateElectricityDamage"
| "InnateHeatDamage"
@ -311,11 +331,17 @@ export const getInnateDamageTag = (KillingSuit: string): TInnateDamageTag => {
return ExportWarframes[KillingSuit].nemesisUpgradeTag!;
};
const petHeads = [
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
] as const;
export interface INemesisProfile {
innateDamageTag: TInnateDamageTag;
innateDamageValue: number;
ephemera?: string;
petHead?: string;
petHead?: (typeof petHeads)[number];
petBody?: string;
petLegs?: string;
petTail?: string;
@ -337,16 +363,12 @@ export const generateNemesisProfile = (
innateDamageTag: getInnateDamageTag(killingSuit),
innateDamageValue: Math.trunc(value * 0x40000000) // TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003.
};
if (rng.randomFloat() <= ephemeraProbabilities[Faction] && Faction != "FC_INFESTATION") {
if (rng.randomFloat() <= nemesisFactionInfos[Faction].ephemeraChance && Faction != "FC_INFESTATION") {
profile.ephemera = ephmeraTypes[Faction][profile.innateDamageTag];
}
rng.randomFloat(); // something related to sentinel agent maybe
if (Faction == "FC_CORPUS") {
profile.petHead = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
])!;
profile.petHead = rng.randomElement(petHeads)!;
profile.petBody = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
@ -422,52 +444,3 @@ export const getInfestedLichItemRewards = (fp: bigint): string[] => {
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

@ -10,7 +10,7 @@ import {
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
import { logger } from "@/src/utils/logger";
import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService";
import { equipmentKeys, IMission, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { equipmentKeys, IMission, ITypeCount, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import {
addBooster,
addChallenges,
@ -44,7 +44,7 @@ import {
import { updateQuestKey } from "@/src/services/questService";
import { Types } from "mongoose";
import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes";
import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService";
import { fromStoreItem, getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
@ -58,10 +58,12 @@ import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPe
import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json";
import {
generateNemesisProfile,
getInfestedLichItemRewards,
getInfNodes,
getKillTokenRewardCount,
getNemesisPasscode,
getWeaponsForManifest,
sendCodaFinishedMessage
nemesisFactionInfos
} from "@/src/helpers/nemesisHelpers";
import { Loadout } from "../models/inventoryModels/loadoutModel";
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
@ -665,6 +667,9 @@ export const addMissionInventoryUpdates = async (
inventory.Nemesis.Faction,
inventory.Nemesis.KillingSuit
);
const nemesisFactionInfo = nemesisFactionInfos[inventory.Nemesis.Faction];
const att: string[] = [];
let countedAtt: ITypeCount[] | undefined;
if (value.killed) {
if (
@ -675,45 +680,78 @@ export const addMissionInventoryUpdates = async (
inventory.Nemesis.WeaponIdx
];
giveNemesisWeaponRecipe(inventory, weaponType, value.nemesisName, value.weaponLoc, profile);
att.push(weaponType);
}
if (value.petLoc) {
//if (value.petLoc) {
if (profile.petHead) {
giveNemesisPetRecipe(inventory, value.nemesisName, profile);
att.push(
{
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA":
"/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadABlueprint",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB":
"/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadBBlueprint",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC":
"/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadCBlueprint"
}[profile.petHead]
);
}
}
// "Players will receive a Lich's Ephemera regardless of whether they Vanquish or Convert them."
if (profile.ephemera) {
addSkin(inventory, profile.ephemera);
att.push(profile.ephemera);
}
switch (inventory.Nemesis.Faction) {
case "FC_GRINEER":
addSkin(
inventory,
value.killed
? "/Lotus/Upgrades/Skins/Clan/LichKillerBadgeItem"
: "/Lotus/Upgrades/Skins/Sigils/KuvaLichSigil"
);
break;
const skinRewardStoreItem = value.killed
? nemesisFactionInfo.firstKillReward
: nemesisFactionInfo.firstConvertReward;
if (Object.keys(addSkin(inventory, fromStoreItem(skinRewardStoreItem))).length != 0) {
att.push(skinRewardStoreItem);
}
case "FC_CORPUS":
addSkin(
inventory,
value.killed
? "/Lotus/Upgrades/Skins/Clan/CorpusLichBadgeItem"
: "/Lotus/Upgrades/Skins/Sigils/CorpusLichSigil"
);
break;
if (inventory.Nemesis.Faction == "FC_INFESTATION") {
const [rotARewardStoreItem, rotBRewardStoreItem] = getInfestedLichItemRewards(
inventory.Nemesis.fp
);
const rotAReward = fromStoreItem(rotARewardStoreItem);
const rotBReward = fromStoreItem(rotBRewardStoreItem);
await addItem(inventory, rotAReward);
await addItem(inventory, rotBReward);
att.push(rotAReward);
att.push(rotBReward);
case "FC_INFESTATION":
// TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given?
await sendCodaFinishedMessage(
inventory,
inventory.Nemesis.fp,
value.nemesisName,
value.killed
);
break;
if (value.killed) {
countedAtt = [
{
ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks",
ItemCount: getKillTokenRewardCount(inventory.Nemesis.fp)
}
];
addMiscItems(inventory, countedAtt);
}
}
if (value.killed) {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: nemesisFactionInfo.messageBody,
arg: [
{
Key: "LICH_NAME",
Tag: value.nemesisName
}
],
att: att,
countedAtt: countedAtt,
attVisualOnly: true,
sub: nemesisFactionInfo.messageTitle,
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
}
inventory.Nemesis = undefined;