2025-04-13 05:50:51 -07:00
|
|
|
import {
|
|
|
|
consumeModCharge,
|
|
|
|
encodeNemesisGuess,
|
|
|
|
getInfNodes,
|
|
|
|
getNemesisPasscode,
|
|
|
|
IKnifeResponse
|
|
|
|
} from "@/src/helpers/nemesisHelpers";
|
2025-03-20 05:36:09 -07:00
|
|
|
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
2025-04-13 05:50:51 -07:00
|
|
|
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
|
2025-03-20 15:27:37 -07:00
|
|
|
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
|
2025-03-20 05:36:09 -07:00
|
|
|
import { getAccountIdForRequest } from "@/src/services/loginService";
|
|
|
|
import { SRng } from "@/src/services/rngService";
|
2025-03-20 15:27:37 -07:00
|
|
|
import { IMongoDate, IOid } from "@/src/types/commonTypes";
|
2025-04-13 05:50:51 -07:00
|
|
|
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
|
|
|
import {
|
|
|
|
IInnateDamageFingerprint,
|
|
|
|
InventorySlot,
|
|
|
|
IUpgradeClient,
|
|
|
|
IWeaponSkinClient,
|
|
|
|
LoadoutIndex,
|
|
|
|
TEquipmentKey
|
|
|
|
} from "@/src/types/inventoryTypes/inventoryTypes";
|
2025-03-20 05:36:09 -07:00
|
|
|
import { logger } from "@/src/utils/logger";
|
|
|
|
import { RequestHandler } from "express";
|
|
|
|
|
|
|
|
export const nemesisController: RequestHandler = async (req, res) => {
|
2025-03-20 15:27:37 -07:00
|
|
|
const accountId = await getAccountIdForRequest(req);
|
|
|
|
if ((req.query.mode as string) == "f") {
|
|
|
|
const body = getJSONfromString<IValenceFusionRequest>(String(req.body));
|
|
|
|
const inventory = await getInventory(accountId, body.Category + " WeaponBin");
|
|
|
|
const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!;
|
|
|
|
const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!;
|
|
|
|
const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
|
|
|
|
const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
|
|
|
|
|
2025-03-30 13:50:59 -07:00
|
|
|
// Update destination damage type if desired
|
2025-03-20 15:27:37 -07:00
|
|
|
if (body.UseSourceDmgType) {
|
|
|
|
destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upgrade destination damage value
|
|
|
|
const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
|
|
|
|
const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
|
|
|
|
let newDamage = Math.max(destDamage, sourceDamage) * 1.1;
|
2025-04-01 02:29:51 -07:00
|
|
|
if (newDamage >= 0.5794998) {
|
2025-03-20 15:27:37 -07:00
|
|
|
newDamage = 0.6;
|
|
|
|
}
|
|
|
|
destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff);
|
|
|
|
|
|
|
|
// Commit fingerprint
|
|
|
|
destWeapon.UpgradeFingerprint = JSON.stringify(destFingerprint);
|
|
|
|
|
|
|
|
// Remove source weapon
|
|
|
|
inventory[body.Category].pull({ _id: body.SourceWeapon.$oid });
|
|
|
|
freeUpSlot(inventory, InventorySlot.WEAPONS);
|
|
|
|
|
|
|
|
await inventory.save();
|
|
|
|
res.json({
|
|
|
|
InventoryChanges: {
|
2025-03-30 13:50:59 -07:00
|
|
|
[body.Category]: [destWeapon.toJSON()],
|
|
|
|
RemovedIdItems: [{ ItemId: body.SourceWeapon }]
|
2025-03-20 15:27:37 -07:00
|
|
|
}
|
|
|
|
});
|
2025-03-22 07:30:16 -07:00
|
|
|
} else if ((req.query.mode as string) == "p") {
|
|
|
|
const inventory = await getInventory(accountId, "Nemesis");
|
|
|
|
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
|
2025-04-13 05:50:51 -07:00
|
|
|
const passcode = getNemesisPasscode(inventory.Nemesis!);
|
2025-03-22 07:30:16 -07:00
|
|
|
let guessResult = 0;
|
|
|
|
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
|
|
|
|
for (let i = 0; i != 3; ++i) {
|
|
|
|
if (body.guess[i] == passcode[0]) {
|
|
|
|
guessResult = 1 + i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (let i = 0; i != 3; ++i) {
|
|
|
|
if (body.guess[i] == passcode[i]) {
|
|
|
|
++guessResult;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
res.json({ GuessResult: guessResult });
|
2025-04-13 05:50:51 -07:00
|
|
|
} else if (req.query.mode == "r") {
|
|
|
|
const inventory = await getInventory(
|
|
|
|
accountId,
|
|
|
|
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
|
|
|
|
);
|
|
|
|
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
|
|
|
|
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
|
|
|
|
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
|
|
|
|
const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
|
|
|
|
|
|
|
|
// Add to GuessHistory
|
|
|
|
const result1 = passcode == guess[0] ? 0 : 1;
|
|
|
|
const result2 = passcode == guess[1] ? 0 : 1;
|
|
|
|
const result3 = passcode == guess[2] ? 0 : 1;
|
|
|
|
inventory.Nemesis!.GuessHistory.push(
|
|
|
|
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Increase antivirus
|
|
|
|
let antivirusGain = 5;
|
|
|
|
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
|
|
|
|
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
|
|
|
|
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
|
|
|
|
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
|
|
|
|
const response: IKnifeResponse = {};
|
|
|
|
for (const upgrade of body.knife!.AttachedUpgrades) {
|
|
|
|
switch (upgrade.ItemType) {
|
|
|
|
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
|
|
|
|
antivirusGain += 10;
|
|
|
|
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
|
|
|
break;
|
|
|
|
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
|
|
|
|
antivirusGain += 10;
|
|
|
|
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
|
|
|
break;
|
|
|
|
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
|
|
|
|
antivirusGain += 15;
|
|
|
|
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
|
|
|
break;
|
|
|
|
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
|
|
|
|
antivirusGain += 15;
|
|
|
|
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
|
|
|
break;
|
|
|
|
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
|
|
|
|
antivirusGain += 10;
|
|
|
|
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
inventory.Nemesis!.HenchmenKilled += antivirusGain;
|
|
|
|
if (inventory.Nemesis!.HenchmenKilled >= 100) {
|
|
|
|
inventory.Nemesis!.HenchmenKilled = 100;
|
|
|
|
inventory.Nemesis!.InfNodes = [
|
|
|
|
{
|
|
|
|
Node: "CrewBattleNode559",
|
|
|
|
Influence: 1
|
|
|
|
}
|
|
|
|
];
|
|
|
|
inventory.Nemesis!.Weakened = true;
|
|
|
|
} else {
|
|
|
|
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
await inventory.save();
|
|
|
|
res.json(response);
|
|
|
|
} else {
|
|
|
|
const passcode = getNemesisPasscode(inventory.Nemesis!);
|
|
|
|
if (passcode[body.position] != body.guess) {
|
|
|
|
res.end();
|
|
|
|
} else {
|
|
|
|
inventory.Nemesis!.Rank += 1;
|
|
|
|
inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
|
|
|
|
await inventory.save();
|
|
|
|
res.json({ RankIncrease: 1 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if ((req.query.mode as string) == "rs") {
|
|
|
|
// report spawn; POST but no application data in body
|
|
|
|
const inventory = await getInventory(accountId, "Nemesis");
|
|
|
|
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
|
|
|
|
await inventory.save();
|
|
|
|
res.json({ LastEnc: inventory.Nemesis!.LastEnc });
|
2025-03-20 15:27:37 -07:00
|
|
|
} else if ((req.query.mode as string) == "s") {
|
2025-03-22 07:30:29 -07:00
|
|
|
const inventory = await getInventory(accountId, "Nemesis");
|
2025-03-20 05:36:09 -07:00
|
|
|
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
|
2025-03-20 15:27:15 -07:00
|
|
|
body.target.fp = BigInt(body.target.fp);
|
|
|
|
|
|
|
|
let weaponIdx = -1;
|
2025-03-22 06:08:00 -07:00
|
|
|
if (body.target.Faction != "FC_INFESTATION") {
|
2025-03-20 15:27:15 -07:00
|
|
|
let weapons: readonly string[];
|
|
|
|
if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") {
|
|
|
|
weapons = kuvaLichVersionSixWeapons;
|
|
|
|
} else if (
|
|
|
|
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" ||
|
|
|
|
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"
|
2025-03-20 05:36:09 -07:00
|
|
|
) {
|
2025-03-20 15:27:15 -07:00
|
|
|
weapons = corpusVersionThreeWeapons;
|
|
|
|
} else {
|
|
|
|
throw new Error(`unknown nemesis manifest: ${body.target.manifest}`);
|
2025-03-20 05:36:09 -07:00
|
|
|
}
|
|
|
|
|
2025-03-20 15:27:15 -07:00
|
|
|
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
|
|
|
|
weaponIdx = initialWeaponIdx;
|
|
|
|
do {
|
|
|
|
const weapon = weapons[weaponIdx];
|
|
|
|
if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
weaponIdx = (weaponIdx + 1) % weapons.length;
|
|
|
|
} while (weaponIdx != initialWeaponIdx);
|
2025-03-20 05:36:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
inventory.Nemesis = {
|
|
|
|
fp: body.target.fp,
|
|
|
|
manifest: body.target.manifest,
|
|
|
|
KillingSuit: body.target.KillingSuit,
|
|
|
|
killingDamageType: body.target.killingDamageType,
|
|
|
|
ShoulderHelmet: body.target.ShoulderHelmet,
|
|
|
|
WeaponIdx: weaponIdx,
|
|
|
|
AgentIdx: body.target.AgentIdx,
|
|
|
|
BirthNode: body.target.BirthNode,
|
|
|
|
Faction: body.target.Faction,
|
|
|
|
Rank: 0,
|
|
|
|
k: false,
|
|
|
|
Traded: false,
|
|
|
|
d: new Date(),
|
2025-03-22 06:08:00 -07:00
|
|
|
InfNodes: getInfNodes(body.target.Faction, 0),
|
2025-03-20 05:36:09 -07:00
|
|
|
GuessHistory: [],
|
|
|
|
Hints: [],
|
|
|
|
HintProgress: 0,
|
|
|
|
Weakened: body.target.Weakened,
|
|
|
|
PrevOwners: 0,
|
|
|
|
HenchmenKilled: 0,
|
2025-03-22 06:08:00 -07:00
|
|
|
SecondInCommand: body.target.SecondInCommand,
|
|
|
|
MissionCount: 0,
|
|
|
|
LastEnc: 0
|
2025-03-20 05:36:09 -07:00
|
|
|
};
|
|
|
|
await inventory.save();
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
target: inventory.toJSON().Nemesis
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
|
|
|
|
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-03-20 15:27:37 -07:00
|
|
|
interface IValenceFusionRequest {
|
|
|
|
DestWeapon: IOid;
|
|
|
|
SourceWeapon: IOid;
|
|
|
|
Category: TEquipmentKey;
|
|
|
|
UseSourceDmgType: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface INemesisStartRequest {
|
2025-03-20 05:36:09 -07:00
|
|
|
target: {
|
|
|
|
fp: number | bigint;
|
|
|
|
manifest: string;
|
|
|
|
KillingSuit: string;
|
|
|
|
killingDamageType: number;
|
|
|
|
ShoulderHelmet: string;
|
|
|
|
DisallowedWeapons: string[];
|
|
|
|
WeaponIdx: number;
|
|
|
|
AgentIdx: number;
|
|
|
|
BirthNode: string;
|
|
|
|
Faction: string;
|
|
|
|
Rank: number;
|
|
|
|
k: boolean;
|
|
|
|
Traded: boolean;
|
|
|
|
d: IMongoDate;
|
|
|
|
InfNodes: [];
|
|
|
|
GuessHistory: [];
|
|
|
|
Hints: [];
|
|
|
|
HintProgress: number;
|
|
|
|
Weakened: boolean;
|
|
|
|
PrevOwners: number;
|
|
|
|
HenchmenKilled: number;
|
2025-03-20 15:27:15 -07:00
|
|
|
MissionCount?: number; // Added in 38.5.0
|
|
|
|
LastEnc?: number; // Added in 38.5.0
|
2025-03-20 05:36:09 -07:00
|
|
|
SecondInCommand: boolean;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2025-03-22 07:30:16 -07:00
|
|
|
interface INemesisPrespawnCheckRequest {
|
|
|
|
guess: number[]; // .length == 3
|
|
|
|
potency?: number[];
|
|
|
|
}
|
|
|
|
|
2025-04-13 05:50:51 -07:00
|
|
|
interface INemesisRequiemRequest {
|
|
|
|
guess: number; // grn/crp: 4 bits | coda: 3x 4 bits
|
|
|
|
position: number; // grn/crp: 0-2 | coda: 0
|
|
|
|
// knife field provided for coda only
|
|
|
|
knife?: {
|
|
|
|
Item: IEquipmentClient;
|
|
|
|
Skins: IWeaponSkinClient[];
|
|
|
|
ModSlot: number;
|
|
|
|
CustSlot: number;
|
|
|
|
AttachedUpgrades: IUpgradeClient[];
|
|
|
|
HiddenWhenHolstered: boolean;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2025-03-20 05:36:09 -07:00
|
|
|
const kuvaLichVersionSixWeapons = [
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
|
|
|
|
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
|
|
|
|
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
|
|
|
|
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
|
|
|
|
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
|
|
|
|
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
|
|
|
|
];
|
|
|
|
|
|
|
|
const corpusVersionThreeWeapons = [
|
|
|
|
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
|
|
|
|
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
|
|
|
|
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
|
|
|
|
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
|
|
|
|
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
|
|
|
|
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
|
|
|
|
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
|
|
|
|
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
|
|
|
|
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
|
|
|
|
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
|
|
|
|
];
|