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

XFESpaceNinjaServer

A simple server for a small space ninja game

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

XFEstudio/XFESpaceNinjaServer

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
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

23 个文件 +374 -41
Modified config.json.example +2 -1
@@ -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
Modified src/controllers/api/getDailyDealStockLevelsController.ts +4 -2
@@ -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 };
Modified src/controllers/api/giftingController.ts +24 -12
@@ -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 {
Modified src/controllers/api/inventoryController.ts +26 -4
@@ -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
Modified src/controllers/dynamic/worldStateController.ts +6 -2
@@ -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 };
Modified src/models/inventoryModels/inventoryModel.ts +3 -1
@@ -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
Modified src/models/worldStateModel.ts +17 -1
@@ -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);
Modified src/services/configService.ts +1 -0
@@ -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;
Modified src/services/purchaseService.ts +28 -1
@@ -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,
Modified src/services/worldStateService.ts +57 -2
@@ -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 };
Modified src/types/inventoryTypes/inventoryTypes.ts +1 -1
@@ -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[];
Modified src/types/purchaseTypes.ts +1 -0
@@ -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 = {
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