返回提交历史
Modified
config.json.example
+2
-1
Modified
src/controllers/api/getDailyDealStockLevelsController.ts
+4
-2
Modified
src/controllers/api/giftingController.ts
+24
-12
Modified
src/controllers/api/inventoryController.ts
+26
-4
Modified
src/controllers/dynamic/worldStateController.ts
+6
-2
Modified
src/models/inventoryModels/inventoryModel.ts
+3
-1
Modified
src/models/worldStateModel.ts
+17
-1
Modified
src/services/configService.ts
+1
-0
Modified
src/services/purchaseService.ts
+28
-1
Modified
src/services/worldStateService.ts
+57
-2
Modified
src/types/inventoryTypes/inventoryTypes.ts
+1
-1
Modified
src/types/purchaseTypes.ts
+1
-0
Modified
src/types/worldStateTypes.ts
+23
-0
Added
static/fixed_responses/worldState/darvoDeals.json
+158
-0
Modified
static/fixed_responses/worldState/worldState.json
+0
-12
Modified
static/webui/index.html
+7
-0
Modified
static/webui/script.js
+10
-2
Modified
static/webui/translations/de.js
+1
-0
Modified
static/webui/translations/en.js
+1
-0
Modified
static/webui/translations/es.js
+1
-0
Modified
static/webui/translations/fr.js
+1
-0
Modified
static/webui/translations/ru.js
+1
-0
Modified
static/webui/translations/zh.js
+1
-0
XFEstudio/SpaceNinjaServer
feat: darvo deal (#2261)
Closes #2260 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2261 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
ef3d3b92
代码差异
23 个文件
+374
-41
@@ -75,7 +75,8 @@
75
75
"duviriOverride": "",
76
76
"nightwaveOverride": "",
77
77
"allTheFissures": "",
78
"circuitGameModes": null
78
"circuitGameModes": null,
79
"darvoStockMultiplier": 1
79
80
},
80
81
"dev": {
81
82
"keepVendorsExpired": false
@@ -1,8 +1,10 @@
1
import { DailyDeal } from "@/src/models/worldStateModel";
1
2
import { RequestHandler } from "express";
2
3
3
export const getDailyDealStockLevelsController: RequestHandler = (req, res) => {
4
export const getDailyDealStockLevelsController: RequestHandler = async (req, res) => {
5
const dailyDeal = (await DailyDeal.findOne({ StoreItem: req.query.productName }, "AmountSold"))!;
4
6
res.json({
5
7
StoreItem: req.query.productName,
6
AmountSold: 0
8
AmountSold: dailyDeal.AmountSold
7
9
});
8
10
};
@@ -9,15 +9,26 @@ import {
9
9
updateCurrency
10
10
} from "@/src/services/inventoryService";
11
11
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
12
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
12
import { handleDailyDealPurchase, handleStoreItemAcquisition } from "@/src/services/purchaseService";
13
13
import { IOid } from "@/src/types/commonTypes";
14
import { IInventoryChanges, IPurchaseParams, PurchaseSource } from "@/src/types/purchaseTypes";
14
import { IPurchaseParams, IPurchaseResponse, PurchaseSource } from "@/src/types/purchaseTypes";
15
15
import { RequestHandler } from "express";
16
16
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
17
17
18
const checkPurchaseParams = (params: IPurchaseParams): boolean => {
19
switch (params.Source) {
20
case PurchaseSource.Market:
21
return params.UsePremium;
22
23
case PurchaseSource.DailyDeal:
24
return true;
25
}
26
return false;
27
};
28
18
29
export const giftingController: RequestHandler = async (req, res) => {
19
30
const data = getJSONfromString<IGiftingRequest>(String(req.body));
20
if (data.PurchaseParams.Source != PurchaseSource.Market || !data.PurchaseParams.UsePremium) {
31
if (!checkPurchaseParams(data.PurchaseParams)) {
21
32
throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
22
33
}
23
34
@@ -58,16 +69,19 @@ export const giftingController: RequestHandler = async (req, res) => {
58
69
}
59
70
senderInventory.GiftsRemaining -= 1;
60
71
61
const inventoryChanges: IInventoryChanges = updateCurrency(
62
senderInventory,
63
data.PurchaseParams.ExpectedPrice,
64
true
65
);
72
const response: IPurchaseResponse = {
73
InventoryChanges: {}
74
};
75
if (data.PurchaseParams.Source == PurchaseSource.DailyDeal) {
76
await handleDailyDealPurchase(senderInventory, data.PurchaseParams, response);
77
} else {
78
updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true, response.InventoryChanges);
79
}
66
80
if (data.PurchaseParams.StoreItem in ExportBundles) {
67
81
const bundle = ExportBundles[data.PurchaseParams.StoreItem];
68
82
if (bundle.giftingBonus) {
69
83
combineInventoryChanges(
70
inventoryChanges,
84
response.InventoryChanges,
71
85
(await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
72
86
);
73
87
}
@@ -99,9 +113,7 @@ export const giftingController: RequestHandler = async (req, res) => {
99
113
}
100
114
]);
101
115
102
res.json({
103
InventoryChanges: inventoryChanges
104
});
116
res.json(response);
105
117
};
106
118
107
119
interface IGiftingRequest {
@@ -24,6 +24,8 @@ import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
24
24
import { Ship } from "@/src/models/shipModel";
25
25
import { toLegacyOid, toOid, version_compare } from "@/src/helpers/inventoryHelpers";
26
26
import { Inbox } from "@/src/models/inboxModel";
27
import { unixTimesInMs } from "@/src/constants/timeConstants";
28
import { DailyDeal } from "@/src/models/worldStateModel";
27
29
28
30
export const inventoryController: RequestHandler = async (request, response) => {
29
31
const account = await getAccountForRequest(request);
@@ -37,6 +39,8 @@ export const inventoryController: RequestHandler = async (request, response) =>
37
39
38
40
// Handle daily reset
39
41
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
42
const today = Math.trunc(Date.now() / 86400000);
43
40
44
for (const key of allDailyAffiliationKeys) {
41
45
inventory[key] = 16000 + inventory.PlayerLevel * 500;
42
46
}
@@ -47,12 +51,12 @@ export const inventoryController: RequestHandler = async (request, response) =>
47
51
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
48
52
49
53
if (inventory.NextRefill) {
54
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
55
const daysPassed = today - lastLoginDay;
56
50
57
if (config.noArgonCrystalDecay) {
51
58
inventory.FoundToday = undefined;
52
59
} else {
53
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
54
const today = Math.trunc(Date.now() / 86400000);
55
const daysPassed = today - lastLoginDay;
56
60
for (let i = 0; i != daysPassed; ++i) {
57
61
const numArgonCrystals =
58
62
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
@@ -84,11 +88,29 @@ export const inventoryController: RequestHandler = async (request, response) =>
84
88
inventory.FoundToday = undefined;
85
89
}
86
90
}
91
92
if (inventory.UsedDailyDeals.length != 0) {
93
if (daysPassed == 1) {
94
const todayAt0Utc = today * 86400000;
95
const darvoIndex = Math.trunc((todayAt0Utc - 25200000) / (26 * unixTimesInMs.hour));
96
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
97
const darvoOid =
98
((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
99
const deal = await DailyDeal.findById(darvoOid);
100
if (deal) {
101
inventory.UsedDailyDeals = inventory.UsedDailyDeals.filter(x => x == deal.StoreItem); // keep only the deal that came into this new day with us
102
} else {
103
inventory.UsedDailyDeals = [];
104
}
105
} else {
106
inventory.UsedDailyDeals = [];
107
}
108
}
87
109
}
88
110
89
111
cleanupInventory(inventory);
90
112
91
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
113
inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
92
114
//await inventory.save();
93
115
}
94
116
@@ -1,15 +1,19 @@
1
1
import { RequestHandler } from "express";
2
import { getWorldState, populateFissures } from "@/src/services/worldStateService";
2
import { getWorldState, populateDailyDeal, populateFissures } from "@/src/services/worldStateService";
3
3
import { version_compare } from "@/src/helpers/inventoryHelpers";
4
4
5
5
export const worldStateController: RequestHandler = async (req, res) => {
6
6
const buildLabel = req.query.buildLabel as string | undefined;
7
7
const worldState = getWorldState(buildLabel);
8
8
9
const populatePromises = [populateDailyDeal(worldState)];
10
9
11
// Omitting void fissures for versions prior to Dante Unbound to avoid script errors.
10
12
if (!buildLabel || version_compare(buildLabel, "2024.03.24.20.00") >= 0) {
11
await populateFissures(worldState);
13
populatePromises.push(populateFissures(worldState));
12
14
}
13
15
16
await Promise.all(populatePromises);
17
14
18
res.json(worldState);
15
19
};
@@ -1625,6 +1625,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
1625
1625
PendingSpectreLoadouts: { type: [spectreLoadoutsSchema], default: undefined },
1626
1626
SpectreLoadouts: { type: [spectreLoadoutsSchema], default: undefined },
1627
1627
1628
//Darvo Deal
1629
UsedDailyDeals: [String],
1630
1628
1631
//New Quest Email
1629
1632
EmailItems: [typeCountSchema],
1630
1633
@@ -1741,7 +1744,6 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
1741
1744
//ChallengeInstanceStates: [Schema.Types.Mixed],
1742
1745
RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined },
1743
1746
//Robotics: [Schema.Types.Mixed],
1744
//UsedDailyDeals: [Schema.Types.Mixed],
1745
1747
CollectibleSeries: { type: [collectibleEntrySchema], default: undefined },
1746
1748
HasResetAccount: { type: Boolean, default: false },
1747
1749
@@ -1,4 +1,4 @@
1
import { IFissureDatabase } from "@/src/types/worldStateTypes";
1
import { IDailyDealDatabase, IFissureDatabase } from "@/src/types/worldStateTypes";
2
2
import { model, Schema } from "mongoose";
3
3
4
4
const fissureSchema = new Schema<IFissureDatabase>({
@@ -12,3 +12,19 @@ const fissureSchema = new Schema<IFissureDatabase>({
12
12
fissureSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); // With this, MongoDB will automatically delete expired entries.
13
13
14
14
export const Fissure = model<IFissureDatabase>("Fissure", fissureSchema);
15
16
const dailyDealSchema = new Schema<IDailyDealDatabase>({
17
StoreItem: { type: String, required: true },
18
Activation: { type: Date, required: true },
19
Expiry: { type: Date, required: true },
20
Discount: { type: Number, required: true },
21
OriginalPrice: { type: Number, required: true },
22
SalePrice: { type: Number, required: true },
23
AmountTotal: { type: Number, required: true },
24
AmountSold: { type: Number, required: true }
25
});
26
27
dailyDealSchema.index({ StoreItem: 1 }, { unique: true });
28
dailyDealSchema.index({ Expiry: 1 }, { expireAfterSeconds: 86400 });
29
30
export const DailyDeal = model<IDailyDealDatabase>("DailyDeal", dailyDealSchema);
@@ -83,6 +83,7 @@ export interface IConfig {
83
83
nightwaveOverride?: string;
84
84
allTheFissures?: string;
85
85
circuitGameModes?: string[];
86
darvoStockMultiplier?: number;
86
87
};
87
88
dev?: {
88
89
keepVendorsExpired?: boolean;
@@ -16,7 +16,8 @@ import {
16
16
IPurchaseResponse,
17
17
SlotPurchase,
18
18
IInventoryChanges,
19
PurchaseSource
19
PurchaseSource,
20
IPurchaseParams
20
21
} from "@/src/types/purchaseTypes";
21
22
import { logger } from "@/src/utils/logger";
22
23
import { getWorldState } from "./worldStateService";
@@ -35,6 +36,7 @@ import {
35
36
import { config } from "./configService";
36
37
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
37
38
import { fromStoreItem, toStoreItem } from "./itemDataService";
39
import { DailyDeal } from "../models/worldStateModel";
38
40
39
41
export const getStoreItemCategory = (storeItem: string): string => {
40
42
const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/");
@@ -240,6 +242,12 @@ export const handlePurchase = async (
240
242
}
241
243
}
242
244
break;
245
case PurchaseSource.DailyDeal:
246
if (purchaseRequest.PurchaseParams.ExpectedPrice) {
247
throw new Error(`daily deal purchase should not have an expected price`);
248
}
249
await handleDailyDealPurchase(inventory, purchaseRequest.PurchaseParams, purchaseResponse);
250
break;
243
251
case PurchaseSource.Vendor:
244
252
if (purchaseRequest.PurchaseParams.SourceId! in ExportVendors) {
245
253
const vendor = ExportVendors[purchaseRequest.PurchaseParams.SourceId!];
@@ -328,6 +336,25 @@ const handleItemPrices = (
328
336
}
329
337
};
330
338
339
export const handleDailyDealPurchase = async (
340
inventory: TInventoryDatabaseDocument,
341
purchaseParams: IPurchaseParams,
342
purchaseResponse: IPurchaseResponse
343
): Promise<void> => {
344
const dailyDeal = (await DailyDeal.findOne({ StoreItem: purchaseParams.StoreItem }))!;
345
dailyDeal.AmountSold += 1;
346
await dailyDeal.save();
347
348
if (!config.dontSubtractPurchasePlatinumCost) {
349
updateCurrency(inventory, dailyDeal.SalePrice, true, purchaseResponse.InventoryChanges);
350
}
351
352
if (!config.noVendorPurchaseLimits) {
353
inventory.UsedDailyDeals.push(purchaseParams.StoreItem);
354
purchaseResponse.DailyDealUsed = purchaseParams.StoreItem;
355
}
356
};
357
331
358
export const handleBundleAcqusition = async (
332
359
storeItemName: string,
333
360
inventory: TInventoryDatabaseDocument,
@@ -4,6 +4,7 @@ import fissureMissions from "@/static/fixed_responses/worldState/fissureMissions
4
4
import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json";
5
5
import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json";
6
6
import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json";
7
import darvoDeals from "@/static/fixed_responses/worldState/darvoDeals.json";
7
8
import { buildConfig } from "@/src/services/buildConfigService";
8
9
import { unixTimesInMs } from "@/src/constants/timeConstants";
9
10
import { config } from "@/src/services/configService";
@@ -27,7 +28,7 @@ import {
27
28
} from "../types/worldStateTypes";
28
29
import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers";
29
30
import { logger } from "../utils/logger";
30
import { Fissure } from "../models/worldStateModel";
31
import { DailyDeal, Fissure } from "../models/worldStateModel";
31
32
32
33
const sortieBosses = [
33
34
"SORTIE_BOSS_HYENA",
@@ -1122,6 +1123,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
1122
1123
GlobalUpgrades: [],
1123
1124
VoidTraders: [],
1124
1125
VoidStorms: [],
1126
DailyDeals: [],
1125
1127
EndlessXpChoices: [],
1126
1128
KnownCalendarSeasons: [],
1127
1129
...staticWorldState,
@@ -1561,6 +1563,24 @@ export const populateFissures = async (worldState: IWorldState): Promise<void> =
1561
1563
}
1562
1564
};
1563
1565
1566
export const populateDailyDeal = async (worldState: IWorldState): Promise<void> => {
1567
const dailyDeals = await DailyDeal.find({});
1568
for (const dailyDeal of dailyDeals) {
1569
if (dailyDeal.Expiry.getTime() > Date.now()) {
1570
worldState.DailyDeals.push({
1571
StoreItem: dailyDeal.StoreItem,
1572
Activation: toMongoDate(dailyDeal.Activation),
1573
Expiry: toMongoDate(dailyDeal.Expiry),
1574
Discount: dailyDeal.Discount,
1575
OriginalPrice: dailyDeal.OriginalPrice,
1576
SalePrice: dailyDeal.SalePrice,
1577
AmountTotal: Math.round(dailyDeal.AmountTotal * (config.worldState?.darvoStockMultiplier ?? 1)),
1578
AmountSold: dailyDeal.AmountSold
1579
});
1580
}
1581
}
1582
};
1583
1564
1584
export const idToBountyCycle = (id: string): number => {
1565
1585
return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000) / 9000_000);
1566
1586
};
@@ -1689,7 +1709,7 @@ const nightwaveTagToSeason: Record<string, number> = {
1689
1709
RadioLegionSyndicate: 0 // The Wolf of Saturn Six
1690
1710
};
1691
1711
1692
export const updateWorldStateCollections = async (): Promise<void> => {
1712
const updateFissures = async (): Promise<void> => {
1693
1713
const fissures = await Fissure.find();
1694
1714
1695
1715
const activeNodes = new Set<string>();
@@ -1742,3 +1762,38 @@ export const updateWorldStateCollections = async (): Promise<void> => {
1742
1762
}
1743
1763
}
1744
1764
};
1765
1766
const updateDailyDeal = async (): Promise<void> => {
1767
let darvoIndex = Math.trunc((Date.now() - 25200000) / (26 * unixTimesInMs.hour));
1768
let darvoEnd;
1769
do {
1770
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
1771
darvoEnd = darvoStart + 26 * unixTimesInMs.hour;
1772
const darvoOid = ((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
1773
if (!(await DailyDeal.findById(darvoOid))) {
1774
const seed = new SRng(darvoIndex).randomInt(0, 100_000);
1775
const rng = new SRng(seed);
1776
let deal;
1777
do {
1778
deal = rng.randomReward(darvoDeals)!; // Using an actual sampling collected over roughly a year because I can't extrapolate an algorithm from it with enough certainty.
1779
//const [storeItem, meta] = rng.randomElement(Object.entries(darvoDeals))!;
1780
//const discount = Math.min(rng.randomInt(1, 9) * 10, (meta as { MaxDiscount?: number }).MaxDiscount ?? 1);
1781
} while (await DailyDeal.exists({ StoreItem: deal.StoreItem }));
1782
await DailyDeal.insertOne({
1783
_id: darvoOid,
1784
StoreItem: deal.StoreItem,
1785
Activation: new Date(darvoStart),
1786
Expiry: new Date(darvoEnd),
1787
Discount: deal.Discount,
1788
OriginalPrice: deal.OriginalPrice,
1789
SalePrice: deal.SalePrice, //Math.trunc(deal.OriginalPrice * (1 - discount))
1790
AmountTotal: deal.AmountTotal,
1791
AmountSold: 0
1792
});
1793
}
1794
} while (darvoEnd < Date.now() + 6 * unixTimesInMs.minute && ++darvoIndex);
1795
};
1796
1797
export const updateWorldStateCollections = async (): Promise<void> => {
1798
await Promise.all([updateFissures(), updateDailyDeal()]);
1799
};
@@ -287,6 +287,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
287
287
ArchwingEnabled?: boolean;
288
288
PendingSpectreLoadouts?: ISpectreLoadout[];
289
289
SpectreLoadouts?: ISpectreLoadout[];
290
UsedDailyDeals: string[];
290
291
EmailItems: ITypeCount[];
291
292
CompletedSyndicates: string[];
292
293
FocusXP?: IFocusXP;
@@ -351,7 +352,6 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
351
352
//LeagueTickets: any[];
352
353
//Quests: any[];
353
354
//Robotics: any[];
354
//UsedDailyDeals: any[];
355
355
LibraryPersonalTarget?: string;
356
356
LibraryPersonalProgress: ILibraryPersonalProgress[];
357
357
CollectibleSeries?: ICollectibleEntry[];
@@ -105,6 +105,7 @@ export interface IPurchaseResponse {
105
105
Standing?: IAffiliationMods[];
106
106
FreeFavorsUsed?: IAffiliationMods[];
107
107
BoosterPackItems?: string;
108
DailyDealUsed?: string;
108
109
}
109
110
110
111
export type IBinChanges = {