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, IPersonalTechProjectDatabase,
IPersonalTechProjectClient, IPersonalTechProjectClient,
ILastSortieRewardDatabase, ILastSortieRewardDatabase,
ILastSortieRewardClient ILastSortieRewardClient,
ICrewMemberSkill,
ICrewMemberSkillEfficiency,
ICrewMemberDatabase,
ICrewMemberClient
} from "../../types/inventoryTypes/inventoryTypes"; } from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes"; import { IOid } from "../../types/commonTypes";
import { 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>( const slotsBinSchema = new Schema<ISlots>(
{ {
Slots: Number, Slots: Number,
@ -1363,7 +1416,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
CrewShipSalvagedWeaponSkins: [upgradeSchema], CrewShipSalvagedWeaponSkins: [upgradeSchema],
//RailJack Crew //RailJack Crew
CrewMembers: [Schema.Types.Mixed], CrewMembers: [crewMemberSchema],
//Complete Mission\Quests //Complete Mission\Quests
Missions: [missionSchema], Missions: [missionSchema],
@ -1645,6 +1698,7 @@ export type InventoryDocumentProps = {
CrewShipWeaponSkins: Types.DocumentArray<IUpgradeDatabase>; CrewShipWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
CrewShipSalvagedWeaponSkins: Types.DocumentArray<IUpgradeDatabase>; CrewShipSalvagedWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>; PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>;
CrewMembers: Types.DocumentArray<ICrewMemberDatabase>;
} & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> }; } & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> };
// eslint-disable-next-line @typescript-eslint/no-empty-object-type // eslint-disable-next-line @typescript-eslint/no-empty-object-type

View File

@ -22,7 +22,8 @@ import {
IDroneClient, IDroneClient,
IUpgradeClient, IUpgradeClient,
TPartialStartingGear, TPartialStartingGear,
ILoreFragmentScan ILoreFragmentScan,
ICrewMemberClient
} 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";
@ -713,6 +714,15 @@ export const addItem = async (
return { return {
MiscItems: miscItemChanges 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") { } else if (typeName == "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") {
return addCrewShipHarness(inventory, typeName); return addCrewShipHarness(inventory, typeName);
} }
@ -1212,6 +1222,78 @@ const addDrone = (
return inventoryChanges; 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 ( export const addEmailItem = async (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
typeName: string, typeName: string,

View File

@ -141,7 +141,8 @@ export const handlePurchase = async (
inventory, inventory,
purchaseRequest.PurchaseParams.Quantity, purchaseRequest.PurchaseParams.Quantity,
undefined, undefined,
undefined, false,
purchaseRequest.PurchaseParams.UsePremium,
seed seed
); );
combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges);
@ -331,6 +332,7 @@ export const handleStoreItemAcquisition = async (
quantity: number = 1, quantity: number = 1,
durability: TRarity = "COMMON", durability: TRarity = "COMMON",
ignorePurchaseQuantity: boolean = false, ignorePurchaseQuantity: boolean = false,
premiumPurchase: boolean = true,
seed?: bigint seed?: bigint
): Promise<IPurchaseResponse> => { ): Promise<IPurchaseResponse> => {
let purchaseResponse = { let purchaseResponse = {
@ -352,11 +354,20 @@ export const handleStoreItemAcquisition = async (
} }
switch (storeCategory) { switch (storeCategory) {
default: { default: {
purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) }; purchaseResponse = {
InventoryChanges: await addItem(inventory, internalName, quantity, premiumPurchase, seed)
};
break; break;
} }
case "Types": case "Types":
purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity, ignorePurchaseQuantity); purchaseResponse = await handleTypesPurchase(
internalName,
inventory,
quantity,
ignorePurchaseQuantity,
premiumPurchase,
seed
);
break; break;
case "Boosters": case "Boosters":
purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability);
@ -478,13 +489,15 @@ const handleTypesPurchase = async (
typesName: string, typesName: string,
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
quantity: number, quantity: number,
ignorePurchaseQuantity: boolean ignorePurchaseQuantity: boolean,
premiumPurchase: boolean = true,
seed?: bigint
): Promise<IPurchaseResponse> => { ): Promise<IPurchaseResponse> => {
const typeCategory = getStoreItemTypesCategory(typesName); const typeCategory = getStoreItemTypesCategory(typesName);
logger.debug(`type category ${typeCategory}`); logger.debug(`type category ${typeCategory}`);
switch (typeCategory) { switch (typeCategory) {
default: default:
return { InventoryChanges: await addItem(inventory, typesName, quantity) }; return { InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed) };
case "BoosterPacks": case "BoosterPacks":
return handleBoosterPackPurchase(typesName, inventory, quantity); return handleBoosterPackPurchase(typesName, inventory, quantity);
case "SlotItems": case "SlotItems":

View File

@ -49,6 +49,7 @@ export interface IInventoryDatabase
| "PersonalTechProjects" | "PersonalTechProjects"
| "LastSortieReward" | "LastSortieReward"
| "LastLiteSortieReward" | "LastLiteSortieReward"
| "CrewMembers"
| TEquipmentKey | TEquipmentKey
>, >,
InventoryDatabaseEquipment { InventoryDatabaseEquipment {
@ -83,6 +84,7 @@ export interface IInventoryDatabase
PersonalTechProjects: IPersonalTechProjectDatabase[]; PersonalTechProjects: IPersonalTechProjectDatabase[];
LastSortieReward?: ILastSortieRewardDatabase[]; LastSortieReward?: ILastSortieRewardDatabase[];
LastLiteSortieReward?: ILastSortieRewardDatabase[]; LastLiteSortieReward?: ILastSortieRewardDatabase[];
CrewMembers: ICrewMemberDatabase[];
} }
export interface IQuestKeyDatabase { export interface IQuestKeyDatabase {
@ -324,7 +326,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
InfestedFoundry?: IInfestedFoundryClient; InfestedFoundry?: IInfestedFoundryClient;
BlessingCooldown?: IMongoDate; BlessingCooldown?: IMongoDate;
CrewShipRawSalvage: ITypeCount[]; CrewShipRawSalvage: ITypeCount[];
CrewMembers: ICrewMember[]; CrewMembers: ICrewMemberClient[];
LotusCustomization: ILotusCustomization; LotusCustomization: ILotusCustomization;
UseAdultOperatorLoadout?: boolean; UseAdultOperatorLoadout?: boolean;
NemesisAbandonedRewards: string[]; NemesisAbandonedRewards: string[];
@ -461,13 +463,24 @@ export interface ICompletedJob {
StageCompletions: number[]; 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; ItemType: string;
NemesisFingerprint: number; NemesisFingerprint: bigint;
Seed: number; Seed: bigint;
HireDate: IMongoDate; AssignedRole?: number;
AssignedRole: number; SkillEfficiency: ICrewMemberSkillEfficiency;
SkillEfficiency: ISkillEfficiency;
WeaponConfigIdx: number; WeaponConfigIdx: number;
WeaponId: IOid; WeaponId: IOid;
XP: number; XP: number;
@ -477,16 +490,9 @@ export interface ICrewMember {
ItemId: IOid; ItemId: IOid;
} }
export interface ISkillEfficiency { export interface ICrewMemberDatabase extends Omit<ICrewMemberClient, "WeaponId" | "ItemId"> {
PILOTING: ICombat; WeaponId: Types.ObjectId;
GUNNERY: ICombat; _id: Types.ObjectId;
ENGINEERING: ICombat;
COMBAT: ICombat;
SURVIVABILITY: ICombat;
}
export interface ICombat {
Assigned: number;
} }
export enum InventorySlot { export enum InventorySlot {

View File

@ -6,7 +6,8 @@ import {
INemesisClient, INemesisClient,
ITypeCount, ITypeCount,
IRecentVendorPurchaseClient, IRecentVendorPurchaseClient,
TEquipmentKey TEquipmentKey,
ICrewMemberClient
} from "./inventoryTypes/inventoryTypes"; } from "./inventoryTypes/inventoryTypes";
export interface IPurchaseRequest { export interface IPurchaseRequest {
@ -47,6 +48,7 @@ export type IInventoryChanges = {
Nemesis?: Partial<INemesisClient>; Nemesis?: Partial<INemesisClient>;
NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0
RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0
CrewMembers?: ICrewMemberClient[];
} & Record< } & Record<
Exclude< Exclude<
string, string,