From 3bb57dcd87837e1ea7887676ae0fdeccbbec8def Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 22:02:48 +0200 Subject: [PATCH] feat: acquisition of CrewMembers --- src/models/inventoryModels/inventoryModel.ts | 58 +++++++++++++- src/services/inventoryService.ts | 84 +++++++++++++++++++- src/services/purchaseService.ts | 23 ++++-- src/types/inventoryTypes/inventoryTypes.ts | 40 ++++++---- src/types/purchaseTypes.ts | 4 +- 5 files changed, 183 insertions(+), 26 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index dcd59ff7..5faee74a 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -88,7 +88,11 @@ import { IPersonalTechProjectDatabase, IPersonalTechProjectClient, ILastSortieRewardDatabase, - ILastSortieRewardClient + ILastSortieRewardClient, + ICrewMemberSkill, + ICrewMemberSkillEfficiency, + ICrewMemberDatabase, + ICrewMemberClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -294,6 +298,55 @@ upgradeSchema.set("toJSON", { } }); +const crewMemberSkillSchema = new Schema( + { + Assigned: Number + }, + { _id: false } +); + +const crewMemberSkillEfficiencySchema = new Schema( + { + PILOTING: crewMemberSkillSchema, + GUNNERY: crewMemberSkillSchema, + ENGINEERING: crewMemberSkillSchema, + COMBAT: crewMemberSkillSchema, + SURVIVABILITY: crewMemberSkillSchema + }, + { _id: false } +); + +const crewMemberSchema = new Schema( + { + ItemType: { type: String, required: true }, + NemesisFingerprint: { type: BigInt, default: 0n }, + Seed: { type: BigInt, default: 0n }, + AssignedRole: Number, + SkillEfficiency: crewMemberSkillEfficiencySchema, + WeaponConfigIdx: Number, + WeaponId: { type: Schema.Types.ObjectId, default: "000000000000000000000000" }, + XP: { type: Number, default: 0 }, + PowersuitType: { type: String, required: true }, + Configs: [ItemConfigSchema], + SecondInCommand: { type: Boolean, default: false } + }, + { id: false } +); + +crewMemberSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as ICrewMemberDatabase; + const client = obj as ICrewMemberClient; + + client.WeaponId = toOid(db.WeaponId); + client.ItemId = toOid(db._id); + + delete obj._id; + delete obj.__v; + } +}); + const slotsBinSchema = new Schema( { Slots: Number, @@ -1363,7 +1416,7 @@ const inventorySchema = new Schema( CrewShipSalvagedWeaponSkins: [upgradeSchema], //RailJack Crew - CrewMembers: [Schema.Types.Mixed], + CrewMembers: [crewMemberSchema], //Complete Mission\Quests Missions: [missionSchema], @@ -1645,6 +1698,7 @@ export type InventoryDocumentProps = { CrewShipWeaponSkins: Types.DocumentArray; CrewShipSalvagedWeaponSkins: Types.DocumentArray; PersonalTechProjects: Types.DocumentArray; + CrewMembers: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 2dd4c0d3..7537c8d6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -22,7 +22,8 @@ import { IDroneClient, IUpgradeClient, TPartialStartingGear, - ILoreFragmentScan + ILoreFragmentScan, + ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -713,6 +714,15 @@ export const addItem = async ( return { MiscItems: miscItemChanges }; + } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) { + if (!seed) { + throw new Error(`Expected crew member to have a seed`); + } + seed |= 0x33b81en << 32n; + return { + ...addCrewMember(inventory, typeName, seed), + ...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase) + }; } else if (typeName == "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") { return addCrewShipHarness(inventory, typeName); } @@ -1212,6 +1222,78 @@ const addDrone = ( return inventoryChanges; }; +/*const getCrewMemberSkills = (seed: bigint, skillPointsToAssign: number): Record => { + const rng = new SRng(seed); + + const skills = ["PILOTING", "GUNNERY", "ENGINEERING", "COMBAT", "SURVIVABILITY"]; + for (let i = 1; i != 5; ++i) { + const swapIndex = rng.randomInt(0, i); + if (swapIndex != i) { + const tmp = skills[i]; + skills[i] = skills[swapIndex]; + skills[swapIndex] = tmp; + } + } + + rng.randomFloat(); // unused afaict + + const skillAssignments = [0, 0, 0, 0, 0]; + for (let skill = 0; skillPointsToAssign; skill = (skill + 1) % 5) { + const maxIncrease = Math.min(5 - skillAssignments[skill], skillPointsToAssign); + const increase = rng.randomInt(0, maxIncrease); + skillAssignments[skill] += increase; + skillPointsToAssign -= increase; + } + + skillAssignments.sort((a, b) => b - a); + + const combined: Record = {}; + for (let i = 0; i != 5; ++i) { + combined[skills[i]] = skillAssignments[i]; + } + return combined; +};*/ + +const addCrewMember = ( + inventory: TInventoryDatabaseDocument, + itemType: string, + seed: bigint, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + // SkillEfficiency is additional to the base stats, so we don't need to compute this + //const skillPointsToAssign = itemType.endsWith("Strong") ? 12 : itemType.indexOf("Medium") != -1 ? 10 : 8; + //const skills = getCrewMemberSkills(seed, skillPointsToAssign); + + // Arbiters = male + // CephalonSuda = female + // NewLoka = female + // Perrin = male + // RedVeil = male + // SteelMeridian = female + const powersuitType = + itemType.indexOf("Arbiters") != -1 || itemType.indexOf("Perrin") != -1 || itemType.indexOf("RedVeil") != -1 + ? "/Lotus/Powersuits/NpcPowersuits/CrewMemberMaleSuit" + : "/Lotus/Powersuits/NpcPowersuits/CrewMemberFemaleSuit"; + + const index = + inventory.CrewMembers.push({ + ItemType: itemType, + NemesisFingerprint: 0n, + Seed: seed, + SkillEfficiency: { + PILOTING: { Assigned: 0 }, + GUNNERY: { Assigned: 0 }, + ENGINEERING: { Assigned: 0 }, + COMBAT: { Assigned: 0 }, + SURVIVABILITY: { Assigned: 0 } + }, + PowersuitType: powersuitType + }) - 1; + inventoryChanges.CrewMembers ??= []; + inventoryChanges.CrewMembers.push(inventory.CrewMembers[index].toJSON()); + return inventoryChanges; +}; + export const addEmailItem = async ( inventory: TInventoryDatabaseDocument, typeName: string, diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 32f3af38..6c3b6d8c 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -141,7 +141,8 @@ export const handlePurchase = async ( inventory, purchaseRequest.PurchaseParams.Quantity, undefined, - undefined, + false, + purchaseRequest.PurchaseParams.UsePremium, seed ); combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); @@ -331,6 +332,7 @@ export const handleStoreItemAcquisition = async ( quantity: number = 1, durability: TRarity = "COMMON", ignorePurchaseQuantity: boolean = false, + premiumPurchase: boolean = true, seed?: bigint ): Promise => { let purchaseResponse = { @@ -352,11 +354,20 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) }; + purchaseResponse = { + InventoryChanges: await addItem(inventory, internalName, quantity, premiumPurchase, seed) + }; break; } case "Types": - purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity, ignorePurchaseQuantity); + purchaseResponse = await handleTypesPurchase( + internalName, + inventory, + quantity, + ignorePurchaseQuantity, + premiumPurchase, + seed + ); break; case "Boosters": purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); @@ -478,13 +489,15 @@ const handleTypesPurchase = async ( typesName: string, inventory: TInventoryDatabaseDocument, quantity: number, - ignorePurchaseQuantity: boolean + ignorePurchaseQuantity: boolean, + premiumPurchase: boolean = true, + seed?: bigint ): Promise => { const typeCategory = getStoreItemTypesCategory(typesName); logger.debug(`type category ${typeCategory}`); switch (typeCategory) { default: - return { InventoryChanges: await addItem(inventory, typesName, quantity) }; + return { InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed) }; case "BoosterPacks": return handleBoosterPackPurchase(typesName, inventory, quantity); case "SlotItems": diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index ef90dbf5..d2201b73 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -49,6 +49,7 @@ export interface IInventoryDatabase | "PersonalTechProjects" | "LastSortieReward" | "LastLiteSortieReward" + | "CrewMembers" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -83,6 +84,7 @@ export interface IInventoryDatabase PersonalTechProjects: IPersonalTechProjectDatabase[]; LastSortieReward?: ILastSortieRewardDatabase[]; LastLiteSortieReward?: ILastSortieRewardDatabase[]; + CrewMembers: ICrewMemberDatabase[]; } export interface IQuestKeyDatabase { @@ -324,7 +326,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu InfestedFoundry?: IInfestedFoundryClient; BlessingCooldown?: IMongoDate; CrewShipRawSalvage: ITypeCount[]; - CrewMembers: ICrewMember[]; + CrewMembers: ICrewMemberClient[]; LotusCustomization: ILotusCustomization; UseAdultOperatorLoadout?: boolean; NemesisAbandonedRewards: string[]; @@ -461,13 +463,24 @@ export interface ICompletedJob { StageCompletions: number[]; } -export interface ICrewMember { +export interface ICrewMemberSkill { + Assigned: number; +} + +export interface ICrewMemberSkillEfficiency { + PILOTING: ICrewMemberSkill; + GUNNERY: ICrewMemberSkill; + ENGINEERING: ICrewMemberSkill; + COMBAT: ICrewMemberSkill; + SURVIVABILITY: ICrewMemberSkill; +} + +export interface ICrewMemberClient { ItemType: string; - NemesisFingerprint: number; - Seed: number; - HireDate: IMongoDate; - AssignedRole: number; - SkillEfficiency: ISkillEfficiency; + NemesisFingerprint: bigint; + Seed: bigint; + AssignedRole?: number; + SkillEfficiency: ICrewMemberSkillEfficiency; WeaponConfigIdx: number; WeaponId: IOid; XP: number; @@ -477,16 +490,9 @@ export interface ICrewMember { ItemId: IOid; } -export interface ISkillEfficiency { - PILOTING: ICombat; - GUNNERY: ICombat; - ENGINEERING: ICombat; - COMBAT: ICombat; - SURVIVABILITY: ICombat; -} - -export interface ICombat { - Assigned: number; +export interface ICrewMemberDatabase extends Omit { + WeaponId: Types.ObjectId; + _id: Types.ObjectId; } export enum InventorySlot { diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 1b4c9aca..eac812a2 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -6,7 +6,8 @@ import { INemesisClient, ITypeCount, IRecentVendorPurchaseClient, - TEquipmentKey + TEquipmentKey, + ICrewMemberClient } from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { @@ -47,6 +48,7 @@ export type IInventoryChanges = { Nemesis?: Partial; NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 + CrewMembers?: ICrewMemberClient[]; } & Record< Exclude< string,