XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

SpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 1 Star 0
返回提交历史

XFEstudio/SpaceNinjaServer

feat: baro's void surplus (#2334)

Closes #2284 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2334 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>

c4c622d8
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

8 个文件 +165 -62
Modified src/controllers/api/purchaseController.ts +1 -0
@@ -11,6 +11,7 @@ export const purchaseController: RequestHandler = async (req, res) => {
11 11 const inventory = await getInventory(accountId);
12 12 const response = await handlePurchase(purchaseRequest, inventory);
13 13 await inventory.save();
14 //console.log(JSON.stringify(response, null, 2));
14 15 res.json(response);
15 16 sendWsBroadcastTo(accountId, { update_inventory: true });
16 17 };
Modified src/models/inventoryModels/inventoryModel.ts +8 -6
@@ -91,7 +91,7 @@ import {
91 91 ICrewMemberSkillEfficiency,
92 92 ICrewMemberDatabase,
93 93 ICrewMemberClient,
94 ISortieRewardAttenuation,
94 IRewardAttenuation,
95 95 IInvasionProgressDatabase,
96 96 IInvasionProgressClient,
97 97 IAccolades,
@@ -1417,10 +1417,10 @@ lastSortieRewardSchema.set("toJSON", {
1417 1417 }
1418 1418 });
1419 1419
1420 const sortieRewardAttenutationSchema = new Schema<ISortieRewardAttenuation>(
1420 const rewardAttenutationSchema = new Schema<IRewardAttenuation>(
1421 1421 {
1422 Tag: String,
1423 Atten: Number
1422 Tag: { type: String, required: true },
1423 Atten: { type: Number, required: true }
1424 1424 },
1425 1425 { _id: false }
1426 1426 );
@@ -1666,7 +1666,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
1666 1666 CompletedSorties: [String],
1667 1667 LastSortieReward: { type: [lastSortieRewardSchema], default: undefined },
1668 1668 LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined },
1669 SortieRewardAttenuation: { type: [sortieRewardAttenutationSchema], default: undefined },
1669 SortieRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined },
1670 1670
1671 1671 // Resource Extractor Drones
1672 1672 Drones: [droneSchema],
@@ -1805,7 +1805,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
1805 1805
1806 1806 HubNpcCustomizations: { type: [hubNpcCustomizationSchema], default: undefined },
1807 1807
1808 ClaimedJunctionChallengeRewards: { type: [String], default: undefined }
1808 ClaimedJunctionChallengeRewards: { type: [String], default: undefined },
1809
1810 SpecialItemRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined }
1809 1811 },
1810 1812 { timestamps: { createdAt: "Created", updatedAt: false } }
1811 1813 );
Modified src/services/importService.ts +6 -0
@@ -296,6 +296,12 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
296 296 db[key] = client[key];
297 297 }
298 298 }
299 // IRewardAtten[]
300 for (const key of ["SortieRewardAttenuation", "SpecialItemRewardAttenuation"] as const) {
301 if (client[key] !== undefined) {
302 db[key] = client[key];
303 }
304 }
299 305 if (client.XPInfo !== undefined) {
300 306 db.XPInfo = client.XPInfo;
301 307 }
Modified src/services/inventoryService.ts +14 -2
@@ -44,6 +44,7 @@ import {
44 44 } from "../types/inventoryTypes/commonInventoryTypes";
45 45 import {
46 46 ExportArcanes,
47 ExportBoosters,
47 48 ExportBundles,
48 49 ExportChallenges,
49 50 ExportCustoms,
@@ -671,6 +672,17 @@ export const addItem = async (
671 672 return await addEmailItem(inventory, typeName);
672 673 }
673 674
675 // Boosters are an odd case. They're only added like this via Baro's Void Surplus afaik.
676 {
677 const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == typeName);
678 if (boosterEntry) {
679 addBooster(typeName, quantity, inventory);
680 return {
681 Boosters: [{ ItemType: typeName, ExpiryDate: quantity }]
682 };
683 }
684 }
685
674 686 // Path-based duck typing
675 687 switch (typeName.substr(1).split("/")[1]) {
676 688 case "Powersuits":
@@ -1354,7 +1366,7 @@ export const addCustomization = (
1354 1366 customizationName: string,
1355 1367 inventoryChanges: IInventoryChanges = {}
1356 1368 ): IInventoryChanges => {
1357 if (!inventory.FlavourItems.find(x => x.ItemType == customizationName)) {
1369 if (!inventory.FlavourItems.some(x => x.ItemType == customizationName)) {
1358 1370 const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1;
1359 1371 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1360 1372 inventoryChanges.FlavourItems ??= [];
@@ -1370,7 +1382,7 @@ export const addSkin = (
1370 1382 typeName: string,
1371 1383 inventoryChanges: IInventoryChanges = {}
1372 1384 ): IInventoryChanges => {
1373 if (inventory.WeaponSkins.find(x => x.ItemType == typeName)) {
1385 if (inventory.WeaponSkins.some(x => x.ItemType == typeName)) {
1374 1386 logger.debug(`refusing to add WeaponSkin ${typeName} because account already owns it`);
1375 1387 } else {
1376 1388 const index = inventory.WeaponSkins.push({ ItemType: typeName, IsNew: true }) - 1;
Modified src/services/purchaseService.ts +130 -51
@@ -8,7 +8,7 @@ import {
8 8 updateCurrency,
9 9 updateSlots
10 10 } from "@/src/services/inventoryService";
11 import { getRandomWeightedRewardUc } from "@/src/services/rngService";
11 import { getRandomReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
12 12 import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService";
13 13 import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
14 14 import {
@@ -37,6 +37,7 @@ import { config } from "./configService";
37 37 import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
38 38 import { fromStoreItem, toStoreItem } from "./itemDataService";
39 39 import { DailyDeal } from "../models/worldStateModel";
40 import { fromMongoDate, toMongoDate } from "../helpers/inventoryHelpers";
40 41
41 42 export const getStoreItemCategory = (storeItem: string): string => {
42 43 const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/");
@@ -53,6 +54,58 @@ export const getStoreItemTypesCategory = (typesItem: string): string => {
53 54 return typeElements[1];
54 55 };
55 56
57 const tallyVendorPurchase = (
58 inventory: TInventoryDatabaseDocument,
59 inventoryChanges: IInventoryChanges,
60 VendorType: string,
61 ItemId: string,
62 numPurchased: number,
63 Expiry: Date
64 ): void => {
65 if (!config.noVendorPurchaseLimits) {
66 inventory.RecentVendorPurchases ??= [];
67 let vendorPurchases = inventory.RecentVendorPurchases.find(x => x.VendorType == VendorType);
68 if (!vendorPurchases) {
69 vendorPurchases =
70 inventory.RecentVendorPurchases[
71 inventory.RecentVendorPurchases.push({
72 VendorType: VendorType,
73 PurchaseHistory: []
74 }) - 1
75 ];
76 }
77 let historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId);
78 if (historyEntry) {
79 if (Date.now() >= historyEntry.Expiry.getTime()) {
80 historyEntry.NumPurchased = numPurchased;
81 historyEntry.Expiry = Expiry;
82 } else {
83 historyEntry.NumPurchased += numPurchased;
84 }
85 } else {
86 historyEntry =
87 vendorPurchases.PurchaseHistory[
88 vendorPurchases.PurchaseHistory.push({
89 ItemId: ItemId,
90 NumPurchased: numPurchased,
91 Expiry: Expiry
92 }) - 1
93 ];
94 }
95 inventoryChanges.NewVendorPurchase = {
96 VendorType: VendorType,
97 PurchaseHistory: [
98 {
99 ItemId: ItemId,
100 NumPurchased: historyEntry.NumPurchased,
101 Expiry: toMongoDate(Expiry)
102 }
103 ]
104 };
105 inventoryChanges.RecentVendorPurchases = inventoryChanges.NewVendorPurchase;
106 }
107 };
108
56 109 export const handlePurchase = async (
57 110 purchaseRequest: IPurchaseRequest,
58 111 inventory: TInventoryDatabaseDocument
@@ -99,20 +152,7 @@ export const handlePurchase = async (
99 152 if (offer.LocTagRandSeed !== undefined) {
100 153 seed = BigInt(offer.LocTagRandSeed);
101 154 }
102 if (!config.noVendorPurchaseLimits && ItemId) {
103 inventory.RecentVendorPurchases ??= [];
104 let vendorPurchases = inventory.RecentVendorPurchases.find(
105 x => x.VendorType == manifest!.VendorInfo.TypeName
106 );
107 if (!vendorPurchases) {
108 vendorPurchases =
109 inventory.RecentVendorPurchases[
110 inventory.RecentVendorPurchases.push({
111 VendorType: manifest.VendorInfo.TypeName,
112 PurchaseHistory: []
113 }) - 1
114 ];
115 }
155 if (ItemId) {
116 156 let expiry = parseInt(offer.Expiry.$date.$numberLong);
117 157 if (purchaseRequest.PurchaseParams.IsWeekly) {
118 158 const EPOCH = 1734307200 * 1000; // Monday
@@ -120,34 +160,14 @@ export const handlePurchase = async (
120 160 const weekStart = EPOCH + week * 604800000;
121 161 expiry = weekStart + 604800000;
122 162 }
123 const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId);
124 let numPurchased = purchaseRequest.PurchaseParams.Quantity;
125 if (historyEntry) {
126 if (Date.now() >= historyEntry.Expiry.getTime()) {
127 historyEntry.NumPurchased = numPurchased;
128 historyEntry.Expiry = new Date(expiry);
129 } else {
130 numPurchased += historyEntry.NumPurchased;
131 historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity;
132 }
133 } else {
134 vendorPurchases.PurchaseHistory.push({
135 ItemId: ItemId,
136 NumPurchased: purchaseRequest.PurchaseParams.Quantity,
137 Expiry: new Date(expiry)
138 });
139 }
140 prePurchaseInventoryChanges.NewVendorPurchase = {
141 VendorType: manifest.VendorInfo.TypeName,
142 PurchaseHistory: [
143 {
144 ItemId: ItemId,
145 NumPurchased: numPurchased,
146 Expiry: { $date: { $numberLong: expiry.toString() } }
147 }
148 ]
149 };
150 prePurchaseInventoryChanges.RecentVendorPurchases = prePurchaseInventoryChanges.NewVendorPurchase;
163 tallyVendorPurchase(
164 inventory,
165 prePurchaseInventoryChanges,
166 manifest.VendorInfo.TypeName,
167 ItemId,
168 purchaseRequest.PurchaseParams.Quantity,
169 new Date(expiry)
170 );
151 171 }
152 172 purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier;
153 173 } else {
@@ -193,7 +213,7 @@ export const handlePurchase = async (
193 213 throw new Error(`vendor purchase should not have an expected price`);
194 214 }
195 215
196 if (!config.dontSubtractPurchaseItemCost) {
216 if (offer.PrimePrice && !config.dontSubtractPurchaseItemCost) {
197 217 const invItem: IMiscItem = {
198 218 ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks",
199 219 ItemCount: offer.PrimePrice * purchaseRequest.PurchaseParams.Quantity * -1
@@ -202,6 +222,17 @@ export const handlePurchase = async (
202 222 purchaseResponse.InventoryChanges.MiscItems ??= [];
203 223 purchaseResponse.InventoryChanges.MiscItems.push(invItem);
204 224 }
225
226 if (offer.Limit) {
227 tallyVendorPurchase(
228 inventory,
229 purchaseResponse.InventoryChanges,
230 "VoidTrader",
231 offer.ItemType,
232 purchaseRequest.PurchaseParams.Quantity,
233 fromMongoDate(worldState.VoidTraders[0].Expiry)
234 );
235 }
205 236 }
206 237 break;
207 238 }
@@ -509,12 +540,57 @@ const handleBoosterPackPurchase = async (
509 540 "attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server."
510 541 );
511 542 }
543 const specialItemReward = pack.components.find(x => x.PityIncreaseRate);
512 544 for (let i = 0; i != quantity; ++i) {
513 const disallowedItems = new Set();
514 for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) {
515 const weights = pack.rarityWeightsPerRoll[roll];
516 const result = getRandomWeightedRewardUc(pack.components, weights);
517 if (result) {
545 if (specialItemReward) {
546 {
547 const normalComponents = [];
548 for (const comp of pack.components) {
549 if (!comp.PityIncreaseRate) {
550 const { Probability, ...rest } = comp;
551 normalComponents.push({
552 ...rest,
553 probability: Probability!
554 });
555 }
556 }
557 const result = getRandomReward(normalComponents)!;
558 logger.debug(`booster pack rolled`, result);
559 purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
560 combineInventoryChanges(
561 purchaseResponse.InventoryChanges,
562 await addItem(inventory, result.Item, result.Amount)
563 );
564 }
565
566 if (!inventory.WeaponSkins.some(x => x.ItemType == specialItemReward.Item)) {
567 inventory.SpecialItemRewardAttenuation ??= [];
568 let atten = inventory.SpecialItemRewardAttenuation.find(x => x.Tag == specialItemReward.Item);
569 if (!atten) {
570 atten =
571 inventory.SpecialItemRewardAttenuation[
572 inventory.SpecialItemRewardAttenuation.push({
573 Tag: specialItemReward.Item,
574 Atten: specialItemReward.Probability!
575 }) - 1
576 ];
577 }
578 if (Math.random() < atten.Atten) {
579 purchaseResponse.BoosterPackItems += toStoreItem(specialItemReward.Item) + ',{"lvl":0};';
580 combineInventoryChanges(
581 purchaseResponse.InventoryChanges,
582 await addItem(inventory, specialItemReward.Item)
583 );
584 // TOVERIFY: Is the SpecialItemRewardAttenuation entry removed now?
585 } else {
586 atten.Atten += specialItemReward.PityIncreaseRate!;
587 }
588 }
589 } else {
590 const disallowedItems = new Set();
591 for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) {
592 const weights = pack.rarityWeightsPerRoll[roll];
593 const result = getRandomWeightedRewardUc(pack.components, weights)!;
518 594 logger.debug(`booster pack rolled`, result);
519 595 if (disallowedItems.has(result.Item)) {
520 596 logger.debug(`oops, can't use that one; trying again`);
@@ -524,9 +600,12 @@ const handleBoosterPackPurchase = async (
524 600 disallowedItems.add(result.Item);
525 601 }
526 602 purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
527 combineInventoryChanges(purchaseResponse.InventoryChanges, await addItem(inventory, result.Item, 1));
603 combineInventoryChanges(
604 purchaseResponse.InventoryChanges,
605 await addItem(inventory, result.Item, result.Amount)
606 );
607 ++roll;
528 608 }
529 ++roll;
530 609 }
531 610 }
532 611 return purchaseResponse;
Modified src/types/inventoryTypes/inventoryTypes.ts +3 -2
@@ -298,7 +298,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
298 298 CompletedSorties: string[];
299 299 LastSortieReward?: ILastSortieRewardClient[];
300 300 LastLiteSortieReward?: ILastSortieRewardClient[];
301 SortieRewardAttenuation?: ISortieRewardAttenuation[];
301 SortieRewardAttenuation?: IRewardAttenuation[];
302 302 Drones: IDroneClient[];
303 303 StepSequencers: IStepSequencer[];
304 304 ActiveAvatarImageType?: string;
@@ -383,6 +383,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
383 383 HubNpcCustomizations?: IHubNpcCustomization[];
384 384 Ship?: IOrbiter; // U22 and below, response only
385 385 ClaimedJunctionChallengeRewards?: string[]; // U39
386 SpecialItemRewardAttenuation?: IRewardAttenuation[]; // Baro's Void Surplus
386 387 }
387 388
388 389 export interface IAffiliation {
@@ -785,7 +786,7 @@ export interface ILastSortieRewardDatabase extends Omit<ILastSortieRewardClient,
785 786 SortieId: Types.ObjectId;
786 787 }
787 788
788 export interface ISortieRewardAttenuation {
789 export interface IRewardAttenuation {
789 790 Tag: string;
790 791 Atten: number;
791 792 }
Modified src/types/worldStateTypes.ts +1 -0
@@ -160,6 +160,7 @@ export interface IVoidTraderOffer {
160 160 ItemType: string;
161 161 PrimePrice: number;
162 162 RegularPrice: number;
163 Limit?: number;
163 164 }
164 165
165 166 export interface IVoidStorm {
Modified static/fixed_responses/worldState/baro.json +2 -1
@@ -1,7 +1,8 @@
1 1 {
2 2 "evergreen": [
3 3 { "ItemType": "/Lotus/StoreItems/Types/Keys/MummyQuestKeyBlueprint", "PrimePrice": 100, "RegularPrice": 25000 },
4 { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/FootstepsMaple", "PrimePrice": 15, "RegularPrice": 1000 }
4 { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/FootstepsMaple", "PrimePrice": 15, "RegularPrice": 1000 },
5 { "ItemType": "/Lotus/StoreItems/Types/BoosterPacks/BaroTreasureBox", "PrimePrice": 0, "RegularPrice": 50000, "Limit": 1 }
5 6 ],
6 7 "armorSets": [
7 8 [