feat: acquisition of CrewMembers (#1705)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled

Reviewed-on: #1705
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-04-18 11:15:27 -07:00 committed by Sainan
parent 379f57be2c
commit 196182f9a8
5 changed files with 183 additions and 26 deletions

View File

@ -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<ICrewMemberSkill>(
{
Assigned: Number
},
{ _id: false }
);
const crewMemberSkillEfficiencySchema = new Schema<ICrewMemberSkillEfficiency>(
{
PILOTING: crewMemberSkillSchema,
GUNNERY: crewMemberSkillSchema,
ENGINEERING: crewMemberSkillSchema,
COMBAT: crewMemberSkillSchema,
SURVIVABILITY: crewMemberSkillSchema
},
{ _id: false }
);
const crewMemberSchema = new Schema<ICrewMemberDatabase>(
{
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<ISlots>(
{
Slots: Number,
@ -1363,7 +1416,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
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<IUpgradeDatabase>;
CrewShipSalvagedWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>;
CrewMembers: Types.DocumentArray<ICrewMemberDatabase>;
} & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> };
// eslint-disable-next-line @typescript-eslint/no-empty-object-type

View File

@ -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<string, number> => {
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<string, number> = {};
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<ICrewMemberClient>());
return inventoryChanges;
};
export const addEmailItem = async (
inventory: TInventoryDatabaseDocument,
typeName: string,

View File

@ -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<IPurchaseResponse> => {
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<IPurchaseResponse> => {
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":

View File

@ -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<ICrewMemberClient, "WeaponId" | "ItemId"> {
WeaponId: Types.ObjectId;
_id: Types.ObjectId;
}
export enum InventorySlot {

View File

@ -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<INemesisClient>;
NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0
RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0
CrewMembers?: ICrewMemberClient[];
} & Record<
Exclude<
string,