feat: classic lich ephemera reward (#2067)
All checks were successful
Build Docker image / docker (push) Successful in 1m16s
Build / build (push) Successful in 1m46s

Reviewed-on: #2067
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-13 20:41:49 -07:00 committed by Sainan
parent 2ce5cc4562
commit 8fb676c906
3 changed files with 110 additions and 48 deletions

View File

@ -47,6 +47,42 @@ export const showdownNodes: Record<TNemesisFaction, string> = {
FC_INFESTATION: "CrewBattleNode559" FC_INFESTATION: "CrewBattleNode559"
}; };
const ephemeraProbabilities: Record<TNemesisFaction, number> = {
FC_GRINEER: 0.05,
FC_CORPUS: 0.2,
FC_INFESTATION: 0
};
type TInnateDamageTag =
| "InnateElectricityDamage"
| "InnateHeatDamage"
| "InnateFreezeDamage"
| "InnateToxinDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateImpactDamage";
const ephmeraTypes: Record<"FC_GRINEER" | "FC_CORPUS", Record<TInnateDamageTag, string>> = {
FC_GRINEER: {
InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaLightningEphemera",
InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaFireEphemera",
InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaIceEphemera",
InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaToxinEphemera",
InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaMagneticEphemera",
InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaTricksterEphemera",
InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaImpactEphemera"
},
FC_CORPUS: {
InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraA",
InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraB",
InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraC",
InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraD",
InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraE",
InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraF",
InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraG"
}
};
// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. // Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis.
export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFaction }): number[] => { export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFaction }): number[] => {
const rng = new SRng(nemesis.fp); const rng = new SRng(nemesis.fp);
@ -271,21 +307,25 @@ export const isNemesisCompatibleWithVersion = (
return true; return true;
}; };
export const getInnateDamageTag = ( export const getInnateDamageTag = (KillingSuit: string): TInnateDamageTag => {
KillingSuit: string
):
| "InnateElectricityDamage"
| "InnateFreezeDamage"
| "InnateHeatDamage"
| "InnateImpactDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateToxinDamage" => {
return ExportWarframes[KillingSuit].nemesisUpgradeTag!; return ExportWarframes[KillingSuit].nemesisUpgradeTag!;
}; };
// TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003. export interface INemesisProfile {
export const getInnateDamageValue = (fp: bigint): number => { innateDamageTag: TInnateDamageTag;
innateDamageValue: number;
ephemera?: string;
petHead?: string;
petBody?: string;
petLegs?: string;
petTail?: string;
}
export const generateNemesisProfile = (
fp: bigint = generateRewardSeed(),
Faction: TNemesisFaction = "FC_CORPUS",
killingSuit: string = "/Lotus/Powersuits/Ember/Ember"
): INemesisProfile => {
const rng = new SRng(fp); const rng = new SRng(fp);
rng.randomFloat(); // used for the weapon index rng.randomFloat(); // used for the weapon index
const WeaponUpgradeValueAttenuationExponent = 2.25; const WeaponUpgradeValueAttenuationExponent = 2.25;
@ -293,7 +333,37 @@ export const getInnateDamageValue = (fp: bigint): number => {
if (value >= 0.941428) { if (value >= 0.941428) {
value = 1; value = 1;
} }
return Math.trunc(value * 0x40000000); const profile: INemesisProfile = {
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") {
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.petBody = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
profile.petLegs = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
profile.petTail = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
}
return profile;
}; };
export const getKillTokenRewardCount = (fp: bigint): number => { export const getKillTokenRewardCount = (fp: bigint): number => {

View File

@ -84,7 +84,7 @@ import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from ".
import { createMessage } from "./inboxService"; import { createMessage } from "./inboxService";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { getWorldState } from "./worldStateService"; import { getWorldState } from "./worldStateService";
import { getInnateDamageTag, getInnateDamageValue } from "../helpers/nemesisHelpers"; import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
export const createInventory = async ( export const createInventory = async (
accountOwnerId: Types.ObjectId, accountOwnerId: Types.ObjectId,
@ -1969,8 +1969,7 @@ export const giveNemesisWeaponRecipe = (
weaponType: string, weaponType: string,
nemesisName: string = "AGOR ROK", nemesisName: string = "AGOR ROK",
weaponLoc?: string, weaponLoc?: string,
KillingSuit: string = "/Lotus/Powersuits/Ember/Ember", profile: INemesisProfile = generateNemesisProfile()
fp: bigint = generateRewardSeed()
): void => { ): void => {
if (!weaponLoc) { if (!weaponLoc) {
weaponLoc = ExportWeapons[weaponType].name; weaponLoc = ExportWeapons[weaponType].name;
@ -1991,8 +1990,8 @@ export const giveNemesisWeaponRecipe = (
compat: weaponType, compat: weaponType,
buffs: [ buffs: [
{ {
Tag: getInnateDamageTag(KillingSuit), Tag: profile.innateDamageTag,
Value: getInnateDamageValue(fp) Value: profile.innateDamageValue
} }
] ]
}, },
@ -2001,27 +2000,15 @@ export const giveNemesisWeaponRecipe = (
}); });
}; };
export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, nemesisName: string = "AGOR ROK"): void => { export const giveNemesisPetRecipe = (
const head = getRandomElement([ inventory: TInventoryDatabaseDocument,
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA", nemesisName: string = "AGOR ROK",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB", profile: INemesisProfile = generateNemesisProfile()
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC" ): void => {
])!; const head = profile.petHead!;
const body = getRandomElement([ const body = profile.petBody!;
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA", const legs = profile.petLegs!;
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB", const tail = profile.petTail!;
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
const legs = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
const tail = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0]; const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0];
addRecipes(inventory, [ addRecipes(inventory, [
{ {

View File

@ -57,6 +57,7 @@ import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePe
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 { import {
generateNemesisProfile,
getInfNodes, getInfNodes,
getNemesisPasscode, getNemesisPasscode,
getWeaponsForManifest, getWeaponsForManifest,
@ -659,6 +660,12 @@ export const addMissionInventoryUpdates = async (
k: value.killed k: value.killed
}); });
const profile = generateNemesisProfile(
inventory.Nemesis.fp,
inventory.Nemesis.Faction,
inventory.Nemesis.KillingSuit
);
if (value.killed) { if (value.killed) {
if ( if (
value.weaponLoc && value.weaponLoc &&
@ -667,20 +674,18 @@ export const addMissionInventoryUpdates = async (
const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[ const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[
inventory.Nemesis.WeaponIdx inventory.Nemesis.WeaponIdx
]; ];
giveNemesisWeaponRecipe( giveNemesisWeaponRecipe(inventory, weaponType, value.nemesisName, value.weaponLoc, profile);
inventory,
weaponType,
value.nemesisName,
value.weaponLoc,
inventory.Nemesis.KillingSuit,
inventory.Nemesis.fp
);
} }
if (value.petLoc) { if (value.petLoc) {
giveNemesisPetRecipe(inventory); giveNemesisPetRecipe(inventory, value.nemesisName, profile);
} }
} }
// "Players will receive a Lich's Ephemera regardless of whether they Vanquish or Convert them."
if (profile.ephemera) {
addSkin(inventory, profile.ephemera);
}
// TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given? // TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given?
if (inventory.Nemesis.Faction == "FC_INFESTATION") { if (inventory.Nemesis.Faction == "FC_INFESTATION") {
await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed); await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed);