Compare commits

...

6 Commits

Author SHA1 Message Date
59b5ab5bac feat: classic lich ephemera reward
All checks were successful
Build / build (push) Successful in 1m1s
Build / build (pull_request) Successful in 1m47s
2025-05-14 05:40:22 +02:00
2ce5cc4562 fix: handle converted lich as crew member (#2071)
All checks were successful
Build Docker image / docker (push) Successful in 1m0s
Build / build (push) Successful in 1m46s
saveLoadout was missing bigint support to properly store NemesisFingerprint, and crewMembers was missing handling for liches being set on-call (the only option available for them)

Reviewed-on: #2071
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:39:03 -07:00
099f12a197 feat: bounty chemistry bonus (#2070)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Re #388

Reviewed-on: #2070
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:38:52 -07:00
bfe2e93c76 feat: resource reward along with duviri decree (#2066)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Closes #561

Reviewed-on: #2066
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:38:37 -07:00
8b97bb4b0a feat: classic lich hints (#2064)
Some checks failed
Build Docker image / docker (push) Successful in 1m18s
Build / build (push) Has been cancelled
Closes #1923

Reviewed-on: #2064
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:37:30 -07:00
85a45a04ea fix: ensure that only one CrewMember is ever on call (#2069)
All checks were successful
Build Docker image / docker (push) Successful in 1m16s
Build / build (push) Successful in 57s
Reviewed-on: #2069
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 08:25:09 -07:00
12 changed files with 327 additions and 95 deletions

8
package-lock.json generated
View File

@ -18,7 +18,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.60", "warframe-public-export-plus": "^0.5.62",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
@ -3703,9 +3703,9 @@
} }
}, },
"node_modules/warframe-public-export-plus": { "node_modules/warframe-public-export-plus": {
"version": "0.5.60", "version": "0.5.62",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.60.tgz", "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.62.tgz",
"integrity": "sha512-vMfytUc4xRi+b7RTSq+TJEl91vwEegpQKxLtXwRPfs9ZHhntxc4rmDYSNWJTvgf/aWXsFUxQlqL/GV5OLPGM7g==" "integrity": "sha512-D8ZzjkU9rrK/59VqCfpMoV31HVmwHZV1dNZxPO85AOlcjg/G81Fu3kgITQTaw9sdNagLPLQnFaiXY58pxxRwgA=="
}, },
"node_modules/warframe-riven-info": { "node_modules/warframe-riven-info": {
"version": "0.1.2", "version": "0.1.2",

View File

@ -25,7 +25,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.60", "warframe-public-export-plus": "^0.5.62",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"

View File

@ -1,4 +1,5 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
@ -7,15 +8,23 @@ import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => { export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers"); const inventory = await getInventory(accountId, "CrewMembers NemesisHistory");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body)); const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!; if (data.crewMember.SecondInCommand) {
dbCrewMember.AssignedRole = data.crewMember.AssignedRole; clearOnCall(inventory);
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency; }
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx; if (data.crewMember.ItemId.$oid == "000000000000000000000000") {
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid); const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!;
dbCrewMember.Configs = data.crewMember.Configs; convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand; } else {
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
}
await inventory.save(); await inventory.save();
res.json({ res.json({
crewMemberId: data.crewMember.ItemId.$oid, crewMemberId: data.crewMember.ItemId.$oid,
@ -26,3 +35,20 @@ export const crewMembersController: RequestHandler = async (req, res) => {
interface ICrewMembersRequest { interface ICrewMembersRequest {
crewMember: ICrewMemberClient; crewMember: ICrewMemberClient;
} }
const clearOnCall = (inventory: TInventoryDatabaseDocument): void => {
for (const cm of inventory.CrewMembers) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
if (inventory.NemesisHistory) {
for (const cm of inventory.NemesisHistory) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
}
};

View File

@ -61,7 +61,11 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
if ( if (
missionReport.MissionStatus !== "GS_SUCCESS" && missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId) !(
missionReport.RewardInfo?.jobId ||
missionReport.RewardInfo?.challengeMissionId ||
missionReport.RewardInfo?.T
)
) { ) {
if (missionReport.EndOfMatchUpload) { if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed(); inventory.RewardSeed = generateRewardSeed();

View File

@ -1,8 +1,7 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addEmailItem, getDialogue, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -107,26 +106,3 @@ interface IOtherDialogueInfo {
Tag: string; Tag: string;
Value: number; Value: number;
} }
const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory!.Dialogues![
inventory.DialogueHistory!.Dialogues!.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
QueuedDialogues: [],
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: dialogueName
}) - 1
];
}
return dialogue;
};

View File

@ -2,11 +2,12 @@ import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes"; import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService"; import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
export const saveLoadoutController: RequestHandler = async (req, res) => { export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest; const body: ISaveLoadoutRequest = getJSONfromString<ISaveLoadoutRequest>(String(req.body));
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true })); // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars

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

@ -1318,7 +1318,7 @@ const nemesisSchema = new Schema<INemesisDatabase>(
InfNodes: { type: [infNodeSchema], default: undefined }, InfNodes: { type: [infNodeSchema], default: undefined },
HenchmenKilled: Number, HenchmenKilled: Number,
HintProgress: Number, HintProgress: Number,
Hints: { type: [Number], default: undefined }, Hints: { type: [Number], default: [] },
GuessHistory: { type: [Number], default: undefined }, GuessHistory: { type: [Number], default: undefined },
MissionCount: Number, MissionCount: Number,
LastEnc: Number LastEnc: Number

View File

@ -28,7 +28,8 @@ import {
ITraits, ITraits,
ICalendarProgress, ICalendarProgress,
INemesisWeaponTargetFingerprint, INemesisWeaponTargetFingerprint,
INemesisPetTargetFingerprint INemesisPetTargetFingerprint,
IDialogueDatabase
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
@ -83,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,
@ -1906,6 +1907,29 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
} }
}; };
export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory!.Dialogues![
inventory.DialogueHistory!.Dialogues!.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
QueuedDialogues: [],
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: dialogueName
}) - 1
];
}
return dialogue;
};
export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => { export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => {
const currentSeason = getWorldState().KnownCalendarSeasons[0]; const currentSeason = getWorldState().KnownCalendarSeasons[0];
@ -1945,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;
@ -1967,8 +1990,8 @@ export const giveNemesisWeaponRecipe = (
compat: weaponType, compat: weaponType,
buffs: [ buffs: [
{ {
Tag: getInnateDamageTag(KillingSuit), Tag: profile.innateDamageTag,
Value: getInnateDamageValue(fp) Value: profile.innateDamageValue
} }
] ]
}, },
@ -1977,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

@ -35,6 +35,7 @@ import {
combineInventoryChanges, combineInventoryChanges,
generateRewardSeed, generateRewardSeed,
getCalendarProgress, getCalendarProgress,
getDialogue,
giveNemesisPetRecipe, giveNemesisPetRecipe,
giveNemesisWeaponRecipe, giveNemesisWeaponRecipe,
updateCurrency, updateCurrency,
@ -55,10 +56,24 @@ 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, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers"; import {
generateNemesisProfile,
getInfNodes,
getNemesisPasscode,
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, getSortie, idToBountyCycle, idToDay, idToWeek, pushClassicBounties } from "./worldStateService"; import {
getLiteSortie,
getSortie,
getWorldState,
idToBountyCycle,
idToDay,
idToWeek,
pushClassicBounties
} from "./worldStateService";
import { config } from "./configService"; import { config } from "./configService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { ISyndicateMissionInfo } from "../types/worldStateTypes"; import { ISyndicateMissionInfo } from "../types/worldStateTypes";
@ -168,6 +183,14 @@ export const addMissionInventoryUpdates = async (
} }
if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) { if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) {
inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress; inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress;
if (inventory.Nemesis.Faction != "FC_INFESTATION" && inventory.Nemesis.Hints.length != 3) {
const progressNeeded = [35, 60, 100][inventory.Nemesis.Hints.length];
if (inventory.Nemesis.HintProgress >= progressNeeded) {
inventory.Nemesis.HintProgress -= progressNeeded;
const passcode = getNemesisPasscode(inventory.Nemesis);
inventory.Nemesis.Hints.push(passcode[inventory.Nemesis.Hints.length]);
}
}
} }
if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) { if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) {
// e.g. for Profit-Taker Phase 1: // e.g. for Profit-Taker Phase 1:
@ -631,12 +654,18 @@ export const addMissionInventoryUpdates = async (
Rank: inventory.Nemesis.Rank, Rank: inventory.Nemesis.Rank,
Traded: inventory.Nemesis.Traded, Traded: inventory.Nemesis.Traded,
PrevOwners: inventory.Nemesis.PrevOwners, PrevOwners: inventory.Nemesis.PrevOwners,
SecondInCommand: inventory.Nemesis.SecondInCommand, SecondInCommand: false,
Weakened: inventory.Nemesis.Weakened, Weakened: inventory.Nemesis.Weakened,
// And set killed flag // And set killed flag
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 &&
@ -645,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);
@ -1175,8 +1202,9 @@ export const addMissionRewards = async (
} }
if (rewardInfo.challengeMissionId) { if (rewardInfo.challengeMissionId) {
const [syndicateTag, tierStr] = rewardInfo.challengeMissionId.split("_"); // TODO: third part in HexSyndicate jobs - Chemistry points const [syndicateTag, tierStr, chemistryStr] = rewardInfo.challengeMissionId.split("_");
const tier = Number(tierStr); const tier = Number(tierStr);
const chemistry = Number(chemistryStr);
const isSteelPath = missions?.Tier; const isSteelPath = missions?.Tier;
if (syndicateTag === "ZarimanSyndicate") { if (syndicateTag === "ZarimanSyndicate") {
let medallionAmount = tier + 1; let medallionAmount = tier + 1;
@ -1193,6 +1221,23 @@ export const addMissionRewards = async (
if (isSteelPath) standingAmount *= 1.5; if (isSteelPath) standingAmount *= 1.5;
AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount)); AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount));
} }
if (syndicateTag == "HexSyndicate" && chemistry && tier < 6) {
const seed = getWorldState().SyndicateMissions.find(x => x.Tag == "HexSyndicate")!.Seed;
const { nodes, buddies } = getHexBounties(seed);
const buddy = buddies[tier];
logger.debug(`Hex seed is ${seed}, giving chemistry for ${buddy}`);
if (missions?.Tag != nodes[tier]) {
logger.warn(
`Uh-oh, tier ${tier} bounty should've been on ${nodes[tier]} but you were just on ${missions?.Tag}`
);
}
const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
const dialogue = getDialogue(inventory, buddy);
dialogue.Chemistry += chemistry;
dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc);
}
if (isSteelPath) { if (isSteelPath) {
await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1); await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1);
MissionRewards.push({ MissionRewards.push({
@ -1403,6 +1448,37 @@ function getRandomMissionDrops(
} else { } else {
rewardManifests = []; rewardManifests = [];
} }
} else if (RewardInfo.T == 13) {
// Undercroft extra/side portal (normal mode), gives 1 Pathos Clamp + Duviri Arcane.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: 1
});
rewardManifests = [
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriStaticUndercroftResourceRewards"
];
} else if (RewardInfo.T == 14) {
// Undercroft extra/side portal (steel path), gives 3 Pathos Clamps + Eidolon Arcane.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: 3
});
rewardManifests = [
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriSteelPathStaticUndercroftResourceRewards"
];
} else if (RewardInfo.T == 15) {
rewardManifests = [
mission?.Tier == 1
? "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoSteelPathRNGRewards"
: "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoNormalRNGRewards"
];
} else if (RewardInfo.T == 70) {
// Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: mission?.Tier == 1 ? 15 : 10
});
rewardManifests = [];
} else { } else {
rewardManifests = region.rewardManifests; rewardManifests = region.rewardManifests;
} }
@ -1721,3 +1797,55 @@ const libraryPersonalTargetToAvatar: Record<string, string> = {
"/Lotus/Types/Game/Library/Targets/Research10Target": "/Lotus/Types/Game/Library/Targets/Research10Target":
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar" "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"
}; };
const node_excluded_buddies: Record<string, string> = {
SolNode856: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
SolNode852: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
SolNode851: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
SolNode850: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
SolNode853: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
SolNode854: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue"
};
const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } => {
// We're gonna shuffle these arrays, so they're not truly 'const'.
const nodes: string[] = [
"SolNode850",
"SolNode851",
"SolNode852",
"SolNode853",
"SolNode854",
"SolNode856",
"SolNode858"
];
const excludable_nodes: string[] = ["SolNode851", "SolNode852", "SolNode853", "SolNode854"];
const buddies: string[] = [
"/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue"
];
const rng = new SRng(seed);
rng.shuffleArray(nodes);
rng.shuffleArray(excludable_nodes);
while (nodes.length > buddies.length) {
nodes.splice(
nodes.findIndex(x => x == excludable_nodes[0]),
1
);
excludable_nodes.splice(0, 1);
}
rng.shuffleArray(buddies);
for (let i = 0; i != 6; ++i) {
if (buddies[i] == node_excluded_buddies[nodes[i]]) {
const swapIdx = (i + 1) % buddies.length;
const tmp = buddies[swapIdx];
buddies[swapIdx] = buddies[i];
buddies[i] = tmp;
}
}
return { nodes, buddies };
};

View File

@ -115,4 +115,19 @@ export class SRng {
randomReward<T extends { probability: number }>(pool: T[]): T | undefined { randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.randomFloat()); return getRewardAtPercentage(pool, this.randomFloat());
} }
churnSeed(its: number): void {
while (its--) {
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
}
}
shuffleArray<T>(arr: T[]): void {
for (let lastIdx = arr.length - 1; lastIdx >= 1; --lastIdx) {
const swapIdx = this.randomInt(0, lastIdx);
const tmp = arr[swapIdx];
arr[swapIdx] = arr[lastIdx];
arr[lastIdx] = tmp;
}
}
} }

View File

@ -177,6 +177,7 @@ export interface IRewardInfo {
PurgatoryRewardQualifications?: string; PurgatoryRewardQualifications?: string;
rewardSeed?: number | bigint; rewardSeed?: number | bigint;
periodicMissionTag?: string; periodicMissionTag?: string;
T?: number; // Duviri
ConquestType?: string; ConquestType?: string;
ConquestCompleted?: number; ConquestCompleted?: number;
ConquestEquipmentSuggestionsFulfilled?: number; ConquestEquipmentSuggestionsFulfilled?: number;