merge upstream
This commit is contained in:
		
						commit
						6532fd1fe8
					
				
							
								
								
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
 | 
					import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
					import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
				
			||||||
import { createMessage } from "@/src/services/inboxService";
 | 
					import { createMessage } from "@/src/services/inboxService";
 | 
				
			||||||
import { getInventory } from "@/src/services/inventoryService";
 | 
					import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
					import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
				
			||||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
					import { GuildPermission } from "@/src/types/guildTypes";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
@ -95,7 +95,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
 | 
					            sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
 | 
				
			||||||
            icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
 | 
					            icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
 | 
				
			||||||
            contextInfo: alliance._id.toString(),
 | 
					            contextInfo: alliance._id.toString(),
 | 
				
			||||||
            highPriority: true,
 | 
					            highPriority: true,
 | 
				
			||||||
            acceptAction: "ALLIANCE_INVITE",
 | 
					            acceptAction: "ALLIANCE_INVITE",
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import { Account } from "@/src/models/loginModel";
 | 
				
			|||||||
import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService";
 | 
					import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService";
 | 
				
			||||||
import { hasGuildPermission } from "@/src/services/guildService";
 | 
					import { hasGuildPermission } from "@/src/services/guildService";
 | 
				
			||||||
import { createMessage } from "@/src/services/inboxService";
 | 
					import { createMessage } from "@/src/services/inboxService";
 | 
				
			||||||
import { getInventory } from "@/src/services/inventoryService";
 | 
					import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
					import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
				
			||||||
import { IOid } from "@/src/types/commonTypes";
 | 
					import { IOid } from "@/src/types/commonTypes";
 | 
				
			||||||
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
 | 
					import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
 | 
				
			||||||
@ -64,7 +64,7 @@ export const addToGuildController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
                sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
 | 
					                sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
 | 
				
			||||||
                icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
 | 
					                icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
 | 
				
			||||||
                contextInfo: payload.GuildId.$oid,
 | 
					                contextInfo: payload.GuildId.$oid,
 | 
				
			||||||
                highPriority: true,
 | 
					                highPriority: true,
 | 
				
			||||||
                acceptAction: "GUILD_INVITE",
 | 
					                acceptAction: "GUILD_INVITE",
 | 
				
			||||||
 | 
				
			|||||||
@ -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,8 +8,15 @@ 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));
 | 
				
			||||||
 | 
					    if (data.crewMember.SecondInCommand) {
 | 
				
			||||||
 | 
					        clearOnCall(inventory);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (data.crewMember.ItemId.$oid == "000000000000000000000000") {
 | 
				
			||||||
 | 
					        const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!;
 | 
				
			||||||
 | 
					        convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
        const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
 | 
					        const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
 | 
				
			||||||
        dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
 | 
					        dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
 | 
				
			||||||
        dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
 | 
					        dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
 | 
				
			||||||
@ -16,6 +24,7 @@ export const crewMembersController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
 | 
					        dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
 | 
				
			||||||
        dbCrewMember.Configs = data.crewMember.Configs;
 | 
					        dbCrewMember.Configs = data.crewMember.Configs;
 | 
				
			||||||
        dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
 | 
					        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;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,20 @@
 | 
				
			|||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
 | 
					import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
 | 
				
			||||||
 | 
					import { getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getVendorInfoController: RequestHandler = (req, res) => {
 | 
					export const getVendorInfoController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    if (typeof req.query.vendor == "string") {
 | 
					    let manifest = getVendorManifestByTypeName(req.query.vendor as string);
 | 
				
			||||||
        const manifest = getVendorManifestByTypeName(req.query.vendor);
 | 
					 | 
				
			||||||
    if (!manifest) {
 | 
					    if (!manifest) {
 | 
				
			||||||
            throw new Error(`Unknown vendor: ${req.query.vendor}`);
 | 
					        throw new Error(`Unknown vendor: ${req.query.vendor as string}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // For testing purposes, authenticating with this endpoint is optional here, but would be required on live.
 | 
				
			||||||
 | 
					    if (req.query.accountId) {
 | 
				
			||||||
 | 
					        const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					        const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					        manifest = applyStandingToVendorManifest(inventory, manifest);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json(manifest);
 | 
					    res.json(manifest);
 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        res.status(400).end();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,12 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			|||||||
import { Account } from "@/src/models/loginModel";
 | 
					import { Account } from "@/src/models/loginModel";
 | 
				
			||||||
import { areFriends } from "@/src/services/friendService";
 | 
					import { areFriends } from "@/src/services/friendService";
 | 
				
			||||||
import { createMessage } from "@/src/services/inboxService";
 | 
					import { createMessage } from "@/src/services/inboxService";
 | 
				
			||||||
import { combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
					import {
 | 
				
			||||||
 | 
					    combineInventoryChanges,
 | 
				
			||||||
 | 
					    getEffectiveAvatarImageType,
 | 
				
			||||||
 | 
					    getInventory,
 | 
				
			||||||
 | 
					    updateCurrency
 | 
				
			||||||
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
					import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
				
			||||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
					import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
				
			||||||
import { IOid } from "@/src/types/commonTypes";
 | 
					import { IOid } from "@/src/types/commonTypes";
 | 
				
			||||||
@ -85,7 +90,7 @@ export const giftingController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            sub: "/Lotus/Language/Menu/GiftReceivedSubject",
 | 
					            sub: "/Lotus/Language/Menu/GiftReceivedSubject",
 | 
				
			||||||
            icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
 | 
					            icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
 | 
				
			||||||
            gifts: [
 | 
					            gifts: [
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    GiftType: data.PurchaseParams.StoreItem
 | 
					                    GiftType: data.PurchaseParams.StoreItem
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,12 @@ import {
 | 
				
			|||||||
    getMessage
 | 
					    getMessage
 | 
				
			||||||
} from "@/src/services/inboxService";
 | 
					} from "@/src/services/inboxService";
 | 
				
			||||||
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
 | 
					import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
 | 
				
			||||||
import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
 | 
					import {
 | 
				
			||||||
 | 
					    addItems,
 | 
				
			||||||
 | 
					    combineInventoryChanges,
 | 
				
			||||||
 | 
					    getEffectiveAvatarImageType,
 | 
				
			||||||
 | 
					    getInventory
 | 
				
			||||||
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { ExportFlavour } from "warframe-public-export-plus";
 | 
					import { ExportFlavour } from "warframe-public-export-plus";
 | 
				
			||||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
					import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
				
			||||||
@ -88,7 +93,7 @@ export const inboxController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            ],
 | 
					                            ],
 | 
				
			||||||
                            sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject",
 | 
					                            sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject",
 | 
				
			||||||
                            icon: ExportFlavour[inventory.ActiveAvatarImageType].icon,
 | 
					                            icon: ExportFlavour[getEffectiveAvatarImageType(inventory)].icon,
 | 
				
			||||||
                            highPriority: true
 | 
					                            highPriority: true
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    ]);
 | 
					                    ]);
 | 
				
			||||||
 | 
				
			|||||||
@ -340,7 +340,7 @@ export const getInventoryResponse = async (
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const addString = (arr: string[], str: string): void => {
 | 
					const addString = (arr: string[], str: string): void => {
 | 
				
			||||||
    if (!arr.find(x => x == str)) {
 | 
					    if (arr.indexOf(str) == -1) {
 | 
				
			||||||
        arr.push(str);
 | 
					        arr.push(str);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,7 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
 | 
				
			|||||||
            StoreItemType: body.ChosenReward
 | 
					            StoreItemType: body.ChosenReward
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
 | 
					        inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
 | 
				
			||||||
        if (!evergreenRewards.find(x => x == body.ChosenReward)) {
 | 
					        if (evergreenRewards.indexOf(body.ChosenReward) == -1) {
 | 
				
			||||||
            inventory.LoginMilestoneRewards.push(body.ChosenReward);
 | 
					            inventory.LoginMilestoneRewards.push(body.ChosenReward);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ import {
 | 
				
			|||||||
    getNemesisPasscodeModTypes,
 | 
					    getNemesisPasscodeModTypes,
 | 
				
			||||||
    getWeaponsForManifest,
 | 
					    getWeaponsForManifest,
 | 
				
			||||||
    IKnifeResponse,
 | 
					    IKnifeResponse,
 | 
				
			||||||
    showdownNodes
 | 
					    nemesisFactionInfos
 | 
				
			||||||
} from "@/src/helpers/nemesisHelpers";
 | 
					} from "@/src/helpers/nemesisHelpers";
 | 
				
			||||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
 | 
					import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
 | 
				
			||||||
@ -24,7 +24,8 @@ import {
 | 
				
			|||||||
    IUpgradeClient,
 | 
					    IUpgradeClient,
 | 
				
			||||||
    IWeaponSkinClient,
 | 
					    IWeaponSkinClient,
 | 
				
			||||||
    LoadoutIndex,
 | 
					    LoadoutIndex,
 | 
				
			||||||
    TEquipmentKey
 | 
					    TEquipmentKey,
 | 
				
			||||||
 | 
					    TNemesisFaction
 | 
				
			||||||
} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
@ -176,7 +177,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            weaponIdx = initialWeaponIdx;
 | 
					            weaponIdx = initialWeaponIdx;
 | 
				
			||||||
            do {
 | 
					            do {
 | 
				
			||||||
                const weapon = weapons[weaponIdx];
 | 
					                const weapon = weapons[weaponIdx];
 | 
				
			||||||
                if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
 | 
					                if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                weaponIdx = (weaponIdx + 1) % weapons.length;
 | 
					                weaponIdx = (weaponIdx + 1) % weapons.length;
 | 
				
			||||||
@ -222,7 +223,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        inventory.Nemesis!.InfNodes = [
 | 
					        inventory.Nemesis!.InfNodes = [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Node: showdownNodes[inventory.Nemesis!.Faction],
 | 
					                Node: nemesisFactionInfos[inventory.Nemesis!.Faction].showdownNode,
 | 
				
			||||||
                Influence: 1
 | 
					                Influence: 1
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
@ -269,7 +270,7 @@ interface INemesisStartRequest {
 | 
				
			|||||||
        WeaponIdx: number;
 | 
					        WeaponIdx: number;
 | 
				
			||||||
        AgentIdx: number;
 | 
					        AgentIdx: number;
 | 
				
			||||||
        BirthNode: string;
 | 
					        BirthNode: string;
 | 
				
			||||||
        Faction: string;
 | 
					        Faction: TNemesisFaction;
 | 
				
			||||||
        Rank: number;
 | 
					        Rank: number;
 | 
				
			||||||
        k: boolean;
 | 
					        k: boolean;
 | 
				
			||||||
        Traded: boolean;
 | 
					        Traded: boolean;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
 | 
					import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
 | 
				
			||||||
import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { IInfNode, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { getRewardAtPercentage, 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";
 | 
				
			||||||
@ -7,13 +7,51 @@ import { IOid } from "../types/commonTypes";
 | 
				
			|||||||
import { Types } from "mongoose";
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
import { addMods, generateRewardSeed } 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";
 | 
					 | 
				
			||||||
import { version_compare } from "./inventoryHelpers";
 | 
					import { version_compare } from "./inventoryHelpers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
 | 
					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 infNodes = [];
 | 
				
			||||||
    const systemIndex = systemIndexes[faction][rank];
 | 
					    const systemIndex = nemesisFactionInfos[faction].systemIndexes[rank];
 | 
				
			||||||
    for (const [key, value] of Object.entries(ExportRegions)) {
 | 
					    for (const [key, value] of Object.entries(ExportRegions)) {
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            value.systemIndex === systemIndex &&
 | 
					            value.systemIndex === systemIndex &&
 | 
				
			||||||
@ -35,20 +73,38 @@ export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
 | 
				
			|||||||
    return infNodes;
 | 
					    return infNodes;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const systemIndexes: Record<string, number[]> = {
 | 
					type TInnateDamageTag =
 | 
				
			||||||
    FC_GRINEER: [2, 3, 9, 11, 18],
 | 
					    | "InnateElectricityDamage"
 | 
				
			||||||
    FC_CORPUS: [1, 15, 4, 7, 8],
 | 
					    | "InnateHeatDamage"
 | 
				
			||||||
    FC_INFESTATION: [23]
 | 
					    | "InnateFreezeDamage"
 | 
				
			||||||
};
 | 
					    | "InnateToxinDamage"
 | 
				
			||||||
 | 
					    | "InnateMagDamage"
 | 
				
			||||||
 | 
					    | "InnateRadDamage"
 | 
				
			||||||
 | 
					    | "InnateImpactDamage";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const showdownNodes: Record<string, string> = {
 | 
					const ephmeraTypes: Record<"FC_GRINEER" | "FC_CORPUS", Record<TInnateDamageTag, string>> = {
 | 
				
			||||||
    FC_GRINEER: "CrewBattleNode557",
 | 
					    FC_GRINEER: {
 | 
				
			||||||
    FC_CORPUS: "CrewBattleNode558",
 | 
					        InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaLightningEphemera",
 | 
				
			||||||
    FC_INFESTATION: "CrewBattleNode559"
 | 
					        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: string }): number[] => {
 | 
					export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFaction }): number[] => {
 | 
				
			||||||
    const rng = new SRng(nemesis.fp);
 | 
					    const rng = new SRng(nemesis.fp);
 | 
				
			||||||
    const choices = [0, 1, 2, 3, 5, 6, 7];
 | 
					    const choices = [0, 1, 2, 3, 5, 6, 7];
 | 
				
			||||||
    let choiceIndex = rng.randomInt(0, choices.length - 1);
 | 
					    let choiceIndex = rng.randomInt(0, choices.length - 1);
 | 
				
			||||||
@ -87,7 +143,7 @@ const antivirusMods: readonly string[] = [
 | 
				
			|||||||
    "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
 | 
					    "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: string }): string[] => {
 | 
					export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => {
 | 
				
			||||||
    const passcode = getNemesisPasscode(nemesis);
 | 
					    const passcode = getNemesisPasscode(nemesis);
 | 
				
			||||||
    return nemesis.Faction == "FC_INFESTATION"
 | 
					    return nemesis.Faction == "FC_INFESTATION"
 | 
				
			||||||
        ? passcode.map(i => antivirusMods[i])
 | 
					        ? passcode.map(i => antivirusMods[i])
 | 
				
			||||||
@ -248,7 +304,7 @@ export const getWeaponsForManifest = (manifest: string): readonly string[] => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isNemesisCompatibleWithVersion = (
 | 
					export const isNemesisCompatibleWithVersion = (
 | 
				
			||||||
    nemesis: { manifest: string; Faction: string },
 | 
					    nemesis: { manifest: string; Faction: TNemesisFaction },
 | 
				
			||||||
    buildLabel: string
 | 
					    buildLabel: string
 | 
				
			||||||
): boolean => {
 | 
					): boolean => {
 | 
				
			||||||
    // Anything below 35.6.0 is not going to be okay given our set of supported manifests.
 | 
					    // Anything below 35.6.0 is not going to be okay given our set of supported manifests.
 | 
				
			||||||
@ -271,21 +327,31 @@ 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.
 | 
					const petHeads = [
 | 
				
			||||||
export const getInnateDamageValue = (fp: bigint): number => {
 | 
					    "/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?: (typeof petHeads)[number];
 | 
				
			||||||
 | 
					    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 +359,33 @@ 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() <= 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(petHeads)!;
 | 
				
			||||||
 | 
					        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 => {
 | 
				
			||||||
@ -352,52 +444,3 @@ export const getInfestedLichItemRewards = (fp: bigint): string[] => {
 | 
				
			|||||||
    const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type;
 | 
					    const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type;
 | 
				
			||||||
    return [rotAReward, rotBReward];
 | 
					    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
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
@ -1621,7 +1621,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
				
			|||||||
        Drones: [droneSchema],
 | 
					        Drones: [droneSchema],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        //Active profile ico
 | 
					        //Active profile ico
 | 
				
			||||||
        ActiveAvatarImageType: { type: String, default: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault" },
 | 
					        ActiveAvatarImageType: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable
 | 
					        // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable
 | 
				
			||||||
        DiscoveredMarkers: [discoveredMarkerSchema],
 | 
					        DiscoveredMarkers: [discoveredMarkerSchema],
 | 
				
			||||||
 | 
				
			|||||||
@ -445,7 +445,7 @@ export const addGuildMemberShipDecoContribution = (guildMember: IGuildMemberData
 | 
				
			|||||||
export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => {
 | 
					export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => {
 | 
				
			||||||
    if (build.guildXpValue) {
 | 
					    if (build.guildXpValue) {
 | 
				
			||||||
        guild.ClaimedXP ??= [];
 | 
					        guild.ClaimedXP ??= [];
 | 
				
			||||||
        if (!guild.ClaimedXP.find(x => x == build.resultType)) {
 | 
					        if (guild.ClaimedXP.indexOf(build.resultType) == -1) {
 | 
				
			||||||
            guild.ClaimedXP.push(build.resultType);
 | 
					            guild.ClaimedXP.push(build.resultType);
 | 
				
			||||||
            guild.XP += build.guildXpValue;
 | 
					            guild.XP += build.guildXpValue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -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, [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@ -2014,3 +2025,7 @@ export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, neme
 | 
				
			|||||||
        } satisfies INemesisPetTargetFingerprint)
 | 
					        } satisfies INemesisPetTargetFingerprint)
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getEffectiveAvatarImageType = (inventory: TInventoryDatabaseDocument): string => {
 | 
				
			||||||
 | 
					    return inventory.ActiveAvatarImageType ?? "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault";
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -90,7 +90,7 @@ export const getAccountIdForRequest = async (req: Request): Promise<string> => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isAdministrator = (account: TAccountDocument): boolean => {
 | 
					export const isAdministrator = (account: TAccountDocument): boolean => {
 | 
				
			||||||
    return !!config.administratorNames?.find(x => x == account.DisplayName);
 | 
					    return config.administratorNames?.indexOf(account.DisplayName) != -1;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const platform_magics = [753, 639, 247, 37, 60];
 | 
					const platform_magics = [753, 639, 247, 37, 60];
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ import {
 | 
				
			|||||||
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
 | 
					import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService";
 | 
					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 {
 | 
					import {
 | 
				
			||||||
    addBooster,
 | 
					    addBooster,
 | 
				
			||||||
    addChallenges,
 | 
					    addChallenges,
 | 
				
			||||||
@ -35,6 +35,7 @@ import {
 | 
				
			|||||||
    combineInventoryChanges,
 | 
					    combineInventoryChanges,
 | 
				
			||||||
    generateRewardSeed,
 | 
					    generateRewardSeed,
 | 
				
			||||||
    getCalendarProgress,
 | 
					    getCalendarProgress,
 | 
				
			||||||
 | 
					    getDialogue,
 | 
				
			||||||
    giveNemesisPetRecipe,
 | 
					    giveNemesisPetRecipe,
 | 
				
			||||||
    giveNemesisWeaponRecipe,
 | 
					    giveNemesisWeaponRecipe,
 | 
				
			||||||
    updateCurrency,
 | 
					    updateCurrency,
 | 
				
			||||||
@ -43,7 +44,7 @@ import {
 | 
				
			|||||||
import { updateQuestKey } from "@/src/services/questService";
 | 
					import { updateQuestKey } from "@/src/services/questService";
 | 
				
			||||||
import { Types } from "mongoose";
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
					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 { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
				
			||||||
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
 | 
					import { getEntriesUnsafe } from "@/src/utils/ts-utils";
 | 
				
			||||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
					import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
				
			||||||
@ -55,10 +56,26 @@ 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,
 | 
				
			||||||
 | 
					    getInfestedLichItemRewards,
 | 
				
			||||||
 | 
					    getInfNodes,
 | 
				
			||||||
 | 
					    getKillTokenRewardCount,
 | 
				
			||||||
 | 
					    getNemesisPasscode,
 | 
				
			||||||
 | 
					    getWeaponsForManifest,
 | 
				
			||||||
 | 
					    nemesisFactionInfos
 | 
				
			||||||
 | 
					} 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 +185,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:
 | 
				
			||||||
@ -358,7 +383,7 @@ export const addMissionInventoryUpdates = async (
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    if (
 | 
					                    if (
 | 
				
			||||||
                        inventory.LibraryActiveDailyTaskInfo &&
 | 
					                        inventory.LibraryActiveDailyTaskInfo &&
 | 
				
			||||||
                        inventory.LibraryActiveDailyTaskInfo.EnemyTypes.find(x => x == scan.EnemyType)
 | 
					                        inventory.LibraryActiveDailyTaskInfo.EnemyTypes.indexOf(scan.EnemyType) != -1
 | 
				
			||||||
                    ) {
 | 
					                    ) {
 | 
				
			||||||
                        inventory.LibraryActiveDailyTaskInfo.Scans ??= 0;
 | 
					                        inventory.LibraryActiveDailyTaskInfo.Scans ??= 0;
 | 
				
			||||||
                        inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count;
 | 
					                        inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count;
 | 
				
			||||||
@ -631,12 +656,21 @@ 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
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    const nemesisFactionInfo = nemesisFactionInfos[inventory.Nemesis.Faction];
 | 
				
			||||||
 | 
					                    const att: string[] = [];
 | 
				
			||||||
 | 
					                    let countedAtt: ITypeCount[] | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (value.killed) {
 | 
					                    if (value.killed) {
 | 
				
			||||||
                        if (
 | 
					                        if (
 | 
				
			||||||
                            value.weaponLoc &&
 | 
					                            value.weaponLoc &&
 | 
				
			||||||
@ -645,23 +679,79 @@ 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,
 | 
					                            att.push(weaponType);
 | 
				
			||||||
                                weaponType,
 | 
					 | 
				
			||||||
                                value.nemesisName,
 | 
					 | 
				
			||||||
                                value.weaponLoc,
 | 
					 | 
				
			||||||
                                inventory.Nemesis.KillingSuit,
 | 
					 | 
				
			||||||
                                inventory.Nemesis.fp
 | 
					 | 
				
			||||||
                            );
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        if (value.petLoc) {
 | 
					                        //if (value.petLoc) {
 | 
				
			||||||
                            giveNemesisPetRecipe(inventory);
 | 
					                        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]
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given?
 | 
					                    // "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);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    const skinRewardStoreItem = value.killed
 | 
				
			||||||
 | 
					                        ? nemesisFactionInfo.firstKillReward
 | 
				
			||||||
 | 
					                        : nemesisFactionInfo.firstConvertReward;
 | 
				
			||||||
 | 
					                    if (Object.keys(addSkin(inventory, fromStoreItem(skinRewardStoreItem))).length != 0) {
 | 
				
			||||||
 | 
					                        att.push(skinRewardStoreItem);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (inventory.Nemesis.Faction == "FC_INFESTATION") {
 | 
					                    if (inventory.Nemesis.Faction == "FC_INFESTATION") {
 | 
				
			||||||
                        await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed);
 | 
					                        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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        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;
 | 
					                    inventory.Nemesis = undefined;
 | 
				
			||||||
@ -1175,8 +1265,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 +1284,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 +1511,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 +1860,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 };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ import {
 | 
				
			|||||||
    updateSlots
 | 
					    updateSlots
 | 
				
			||||||
} from "@/src/services/inventoryService";
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
import { getRandomWeightedRewardUc } from "@/src/services/rngService";
 | 
					import { getRandomWeightedRewardUc } from "@/src/services/rngService";
 | 
				
			||||||
import { getVendorManifestByOid } from "@/src/services/serversideVendorsService";
 | 
					import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService";
 | 
				
			||||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
					import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
@ -53,8 +53,9 @@ export const handlePurchase = async (
 | 
				
			|||||||
    const prePurchaseInventoryChanges: IInventoryChanges = {};
 | 
					    const prePurchaseInventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
    let seed: bigint | undefined;
 | 
					    let seed: bigint | undefined;
 | 
				
			||||||
    if (purchaseRequest.PurchaseParams.Source == 7) {
 | 
					    if (purchaseRequest.PurchaseParams.Source == 7) {
 | 
				
			||||||
        const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
 | 
					        let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
 | 
				
			||||||
        if (manifest) {
 | 
					        if (manifest) {
 | 
				
			||||||
 | 
					            manifest = applyStandingToVendorManifest(inventory, manifest);
 | 
				
			||||||
            let ItemId: string | undefined;
 | 
					            let ItemId: string | undefined;
 | 
				
			||||||
            if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
 | 
					            if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
 | 
				
			||||||
                ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string })
 | 
					                ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string })
 | 
				
			||||||
@ -92,7 +93,7 @@ export const handlePurchase = async (
 | 
				
			|||||||
            if (!config.noVendorPurchaseLimits && ItemId) {
 | 
					            if (!config.noVendorPurchaseLimits && ItemId) {
 | 
				
			||||||
                inventory.RecentVendorPurchases ??= [];
 | 
					                inventory.RecentVendorPurchases ??= [];
 | 
				
			||||||
                let vendorPurchases = inventory.RecentVendorPurchases.find(
 | 
					                let vendorPurchases = inventory.RecentVendorPurchases.find(
 | 
				
			||||||
                    x => x.VendorType == manifest.VendorInfo.TypeName
 | 
					                    x => x.VendorType == manifest!.VendorInfo.TypeName
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
                if (!vendorPurchases) {
 | 
					                if (!vendorPurchases) {
 | 
				
			||||||
                    vendorPurchases =
 | 
					                    vendorPurchases =
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
					import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
				
			||||||
import { catBreadHash } from "@/src/helpers/stringHelpers";
 | 
					import { catBreadHash } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
 | 
					import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
				
			||||||
import { mixSeeds, SRng } from "@/src/services/rngService";
 | 
					import { mixSeeds, SRng } from "@/src/services/rngService";
 | 
				
			||||||
import { IMongoDate } from "@/src/types/commonTypes";
 | 
					import { IMongoDate } from "@/src/types/commonTypes";
 | 
				
			||||||
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
 | 
					import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
 | 
				
			||||||
import { ExportVendors, IRange } from "warframe-public-export-plus";
 | 
					import { ExportVendors, IRange, IVendor } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
 | 
					import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
 | 
				
			||||||
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
 | 
					import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
 | 
				
			||||||
@ -81,12 +82,6 @@ const generatableVendors: IGeneratableVendorInfo[] = [
 | 
				
			|||||||
        WeaponUpgradeValueAttenuationExponent: 2.25,
 | 
					        WeaponUpgradeValueAttenuationExponent: 2.25,
 | 
				
			||||||
        cycleOffset: 1744934400_000,
 | 
					        cycleOffset: 1744934400_000,
 | 
				
			||||||
        cycleDuration: 4 * unixTimesInMs.day
 | 
					        cycleDuration: 4 * unixTimesInMs.day
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        _id: { $oid: "61ba123467e5d37975aeeb03" },
 | 
					 | 
				
			||||||
        TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
 | 
					 | 
				
			||||||
        RandomSeedType: "VRST_FLAVOUR_TEXT",
 | 
					 | 
				
			||||||
        cycleDuration: unixTimesInMs.week // TODO: Auto-detect this based on the items, so we don't need to specify it explicitly.
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // {
 | 
					    // {
 | 
				
			||||||
    //     _id: { $oid: "5dbb4c41e966f7886c3ce939" },
 | 
					    //     _id: { $oid: "5dbb4c41e966f7886c3ce939" },
 | 
				
			||||||
@ -98,6 +93,25 @@ const getVendorOid = (typeName: string): string => {
 | 
				
			|||||||
    return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0");
 | 
					    return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0");
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://stackoverflow.com/a/17445304
 | 
				
			||||||
 | 
					const gcd = (a: number, b: number): number => {
 | 
				
			||||||
 | 
					    return b ? gcd(b, a % b) : a;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getCycleDuration = (manifest: IVendor): number => {
 | 
				
			||||||
 | 
					    let dur = 0;
 | 
				
			||||||
 | 
					    for (const item of manifest.items) {
 | 
				
			||||||
 | 
					        if (typeof item.durationHours != "number") {
 | 
				
			||||||
 | 
					            dur = 1;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (dur != item.durationHours) {
 | 
				
			||||||
 | 
					            dur = gcd(dur, item.durationHours);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return dur * unixTimesInMs.hour;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
 | 
					export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
 | 
				
			||||||
    for (const vendorManifest of rawVendorManifests) {
 | 
					    for (const vendorManifest of rawVendorManifests) {
 | 
				
			||||||
        if (vendorManifest.VendorInfo.TypeName == typeName) {
 | 
					        if (vendorManifest.VendorInfo.TypeName == typeName) {
 | 
				
			||||||
@ -110,11 +124,12 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest |
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (typeName in ExportVendors) {
 | 
					    if (typeName in ExportVendors) {
 | 
				
			||||||
 | 
					        const manifest = ExportVendors[typeName];
 | 
				
			||||||
        return generateVendorManifest({
 | 
					        return generateVendorManifest({
 | 
				
			||||||
            _id: { $oid: getVendorOid(typeName) },
 | 
					            _id: { $oid: getVendorOid(typeName) },
 | 
				
			||||||
            TypeName: typeName,
 | 
					            TypeName: typeName,
 | 
				
			||||||
            RandomSeedType: ExportVendors[typeName].randomSeedType,
 | 
					            RandomSeedType: manifest.randomSeedType,
 | 
				
			||||||
            cycleDuration: unixTimesInMs.hour
 | 
					            cycleDuration: getCycleDuration(manifest)
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return undefined;
 | 
					    return undefined;
 | 
				
			||||||
@ -138,13 +153,50 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined
 | 
				
			|||||||
                _id: { $oid: typeNameOid },
 | 
					                _id: { $oid: typeNameOid },
 | 
				
			||||||
                TypeName: typeName,
 | 
					                TypeName: typeName,
 | 
				
			||||||
                RandomSeedType: manifest.randomSeedType,
 | 
					                RandomSeedType: manifest.randomSeedType,
 | 
				
			||||||
                cycleDuration: unixTimesInMs.hour
 | 
					                cycleDuration: getCycleDuration(manifest)
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return undefined;
 | 
					    return undefined;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const applyStandingToVendorManifest = (
 | 
				
			||||||
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
 | 
					    vendorManifest: IVendorManifest
 | 
				
			||||||
 | 
					): IVendorManifest => {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        VendorInfo: {
 | 
				
			||||||
 | 
					            ...vendorManifest.VendorInfo,
 | 
				
			||||||
 | 
					            ItemManifest: [...vendorManifest.VendorInfo.ItemManifest].map(offer => {
 | 
				
			||||||
 | 
					                if (offer.Affiliation && offer.ReductionPerPositiveRank && offer.IncreasePerNegativeRank) {
 | 
				
			||||||
 | 
					                    const title: number = inventory.Affiliations.find(x => x.Tag == offer.Affiliation)?.Title ?? 0;
 | 
				
			||||||
 | 
					                    const factor =
 | 
				
			||||||
 | 
					                        1 + (title < 0 ? offer.IncreasePerNegativeRank : offer.ReductionPerPositiveRank) * title * -1;
 | 
				
			||||||
 | 
					                    //console.log(offer.Affiliation, title, factor);
 | 
				
			||||||
 | 
					                    if (factor) {
 | 
				
			||||||
 | 
					                        offer = { ...offer };
 | 
				
			||||||
 | 
					                        if (offer.RegularPrice) {
 | 
				
			||||||
 | 
					                            offer.RegularPriceBeforeDiscount = offer.RegularPrice;
 | 
				
			||||||
 | 
					                            offer.RegularPrice = [
 | 
				
			||||||
 | 
					                                Math.trunc(offer.RegularPriceBeforeDiscount[0] * factor),
 | 
				
			||||||
 | 
					                                Math.trunc(offer.RegularPriceBeforeDiscount[1] * factor)
 | 
				
			||||||
 | 
					                            ];
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (offer.ItemPrices) {
 | 
				
			||||||
 | 
					                            offer.ItemPricesBeforeDiscount = offer.ItemPrices;
 | 
				
			||||||
 | 
					                            offer.ItemPrices = [];
 | 
				
			||||||
 | 
					                            for (const item of offer.ItemPricesBeforeDiscount) {
 | 
				
			||||||
 | 
					                                offer.ItemPrices.push({ ...item, ItemCount: Math.trunc(item.ItemCount * factor) });
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return offer;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
 | 
					const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
 | 
				
			||||||
    if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
 | 
					    if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
 | 
				
			||||||
        const manifest = structuredClone(originalManifest);
 | 
					        const manifest = structuredClone(originalManifest);
 | 
				
			||||||
@ -176,24 +228,27 @@ const toRange = (value: IRange | number): IRange => {
 | 
				
			|||||||
    return value;
 | 
					    return value;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const vendorInfoCache: Record<string, IVendorInfo> = {};
 | 
					const vendorManifestCache: Record<string, IVendorManifest> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
 | 
					const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
 | 
				
			||||||
    if (!(vendorInfo.TypeName in vendorInfoCache)) {
 | 
					    if (!(vendorInfo.TypeName in vendorManifestCache)) {
 | 
				
			||||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
        const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
 | 
					        const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
 | 
				
			||||||
        vendorInfoCache[vendorInfo.TypeName] = {
 | 
					        vendorManifestCache[vendorInfo.TypeName] = {
 | 
				
			||||||
 | 
					            VendorInfo: {
 | 
				
			||||||
                ...clientVendorInfo,
 | 
					                ...clientVendorInfo,
 | 
				
			||||||
                ItemManifest: [],
 | 
					                ItemManifest: [],
 | 
				
			||||||
                Expiry: { $date: { $numberLong: "0" } }
 | 
					                Expiry: { $date: { $numberLong: "0" } }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const processed = vendorInfoCache[vendorInfo.TypeName];
 | 
					    const cacheEntry = vendorManifestCache[vendorInfo.TypeName];
 | 
				
			||||||
    if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) {
 | 
					    const info = cacheEntry.VendorInfo;
 | 
				
			||||||
 | 
					    if (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) {
 | 
				
			||||||
        // Remove expired offers
 | 
					        // Remove expired offers
 | 
				
			||||||
        for (let i = 0; i != processed.ItemManifest.length; ) {
 | 
					        for (let i = 0; i != info.ItemManifest.length; ) {
 | 
				
			||||||
            if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) {
 | 
					            if (Date.now() >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) {
 | 
				
			||||||
                processed.ItemManifest.splice(i, 1);
 | 
					                info.ItemManifest.splice(i, 1);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                ++i;
 | 
					                ++i;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -207,9 +262,14 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
				
			|||||||
        const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
 | 
					        const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
 | 
				
			||||||
        const manifest = ExportVendors[vendorInfo.TypeName];
 | 
					        const manifest = ExportVendors[vendorInfo.TypeName];
 | 
				
			||||||
        const offersToAdd = [];
 | 
					        const offersToAdd = [];
 | 
				
			||||||
        if (manifest.numItems && !manifest.isOneBinPerCycle) {
 | 
					        if (
 | 
				
			||||||
 | 
					            manifest.numItems &&
 | 
				
			||||||
 | 
					            (manifest.numItems.minValue != manifest.numItems.maxValue ||
 | 
				
			||||||
 | 
					                manifest.items.length != manifest.numItems.minValue) &&
 | 
				
			||||||
 | 
					            !manifest.isOneBinPerCycle
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
            const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
 | 
					            const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
 | 
				
			||||||
            while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) {
 | 
					            while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
 | 
				
			||||||
                // TODO: Consider per-bin item limits
 | 
					                // TODO: Consider per-bin item limits
 | 
				
			||||||
                // TODO: Consider item probability weightings
 | 
					                // TODO: Consider item probability weightings
 | 
				
			||||||
                offersToAdd.push(rng.randomElement(manifest.items)!);
 | 
					                offersToAdd.push(rng.randomElement(manifest.items)!);
 | 
				
			||||||
@ -288,20 +348,18 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
				
			|||||||
                    item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
 | 
					                    item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            processed.ItemManifest.push(item);
 | 
					            info.ItemManifest.push(item);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Update vendor expiry
 | 
					        // Update vendor expiry
 | 
				
			||||||
        let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
 | 
					        let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
 | 
				
			||||||
        for (const offer of processed.ItemManifest) {
 | 
					        for (const offer of info.ItemManifest) {
 | 
				
			||||||
            const offerExpiry = parseInt(offer.Expiry.$date.$numberLong);
 | 
					            const offerExpiry = parseInt(offer.Expiry.$date.$numberLong);
 | 
				
			||||||
            if (soonestOfferExpiry > offerExpiry) {
 | 
					            if (soonestOfferExpiry > offerExpiry) {
 | 
				
			||||||
                soonestOfferExpiry = offerExpiry;
 | 
					                soonestOfferExpiry = offerExpiry;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
 | 
					        info.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return {
 | 
					    return cacheEntry;
 | 
				
			||||||
        VendorInfo: processed
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -296,7 +296,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
 | 
				
			|||||||
    SortieRewardAttenuation?: ISortieRewardAttenuation[];
 | 
					    SortieRewardAttenuation?: ISortieRewardAttenuation[];
 | 
				
			||||||
    Drones: IDroneClient[];
 | 
					    Drones: IDroneClient[];
 | 
				
			||||||
    StepSequencers: IStepSequencer[];
 | 
					    StepSequencers: IStepSequencer[];
 | 
				
			||||||
    ActiveAvatarImageType: string;
 | 
					    ActiveAvatarImageType?: string;
 | 
				
			||||||
    ShipDecorations: ITypeCount[];
 | 
					    ShipDecorations: ITypeCount[];
 | 
				
			||||||
    DiscoveredMarkers: IDiscoveredMarker[];
 | 
					    DiscoveredMarkers: IDiscoveredMarker[];
 | 
				
			||||||
    //CompletedJobs: ICompletedJob[];
 | 
					    //CompletedJobs: ICompletedJob[];
 | 
				
			||||||
@ -863,6 +863,8 @@ export interface IMission extends IMissionDatabase {
 | 
				
			|||||||
    RewardsCooldownTime?: IMongoDate;
 | 
					    RewardsCooldownTime?: IMongoDate;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type TNemesisFaction = "FC_GRINEER" | "FC_CORPUS" | "FC_INFESTATION";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface INemesisBaseClient {
 | 
					export interface INemesisBaseClient {
 | 
				
			||||||
    fp: bigint | number;
 | 
					    fp: bigint | number;
 | 
				
			||||||
    manifest: string;
 | 
					    manifest: string;
 | 
				
			||||||
@ -872,7 +874,7 @@ export interface INemesisBaseClient {
 | 
				
			|||||||
    WeaponIdx: number;
 | 
					    WeaponIdx: number;
 | 
				
			||||||
    AgentIdx: number;
 | 
					    AgentIdx: number;
 | 
				
			||||||
    BirthNode: string;
 | 
					    BirthNode: string;
 | 
				
			||||||
    Faction: string;
 | 
					    Faction: TNemesisFaction;
 | 
				
			||||||
    Rank: number;
 | 
					    Rank: number;
 | 
				
			||||||
    k: boolean;
 | 
					    k: boolean;
 | 
				
			||||||
    Traded: boolean;
 | 
					    Traded: boolean;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -15,10 +15,16 @@ export interface IItemManifest {
 | 
				
			|||||||
    QuantityMultiplier: number;
 | 
					    QuantityMultiplier: number;
 | 
				
			||||||
    Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
 | 
					    Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
 | 
				
			||||||
    PurchaseQuantityLimit?: number;
 | 
					    PurchaseQuantityLimit?: number;
 | 
				
			||||||
 | 
					    Affiliation?: string;
 | 
				
			||||||
 | 
					    MinAffiliationRank?: number;
 | 
				
			||||||
 | 
					    ReductionPerPositiveRank?: number;
 | 
				
			||||||
 | 
					    IncreasePerNegativeRank?: number;
 | 
				
			||||||
    RotatedWeekly?: boolean;
 | 
					    RotatedWeekly?: boolean;
 | 
				
			||||||
    AllowMultipurchase: boolean;
 | 
					    AllowMultipurchase: boolean;
 | 
				
			||||||
    LocTagRandSeed?: number | bigint;
 | 
					    LocTagRandSeed?: number | bigint;
 | 
				
			||||||
    Id: IOid;
 | 
					    Id: IOid;
 | 
				
			||||||
 | 
					    RegularPriceBeforeDiscount?: number[];
 | 
				
			||||||
 | 
					    ItemPricesBeforeDiscount?: IItemPrice[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IVendorInfo {
 | 
					export interface IVendorInfo {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user