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: gardening (#1849)

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

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

代码差异

9 个文件 +262 -27
Added src/controllers/api/gardeningController.ts +84 -0
@@ -0,0 +1,84 @@
1 import { toMongoDate } from "@/src/helpers/inventoryHelpers";
2 import { getJSONfromString } from "@/src/helpers/stringHelpers";
3 import { addMiscItem, getInventory } from "@/src/services/inventoryService";
4 import { toStoreItem } from "@/src/services/itemDataService";
5 import { getAccountIdForRequest } from "@/src/services/loginService";
6 import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
7 import { IMongoDate } from "@/src/types/commonTypes";
8 import { IMissionReward } from "@/src/types/missionTypes";
9 import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
10 import { IInventoryChanges } from "@/src/types/purchaseTypes";
11 import { IGardeningClient } from "@/src/types/shipTypes";
12 import { RequestHandler } from "express";
13 import { dict_en, ExportResources } from "warframe-public-export-plus";
14
15 export const gardeningController: RequestHandler = async (req, res) => {
16 const data = getJSONfromString<IGardeningRequest>(String(req.body));
17 if (data.Mode != "HarvestAll") {
18 throw new Error(`unexpected gardening mode: ${data.Mode}`);
19 }
20
21 const accountId = await getAccountIdForRequest(req);
22 const [inventory, personalRooms] = await Promise.all([
23 getInventory(accountId, "MiscItems"),
24 getPersonalRooms(accountId, "Apartment")
25 ]);
26
27 // Harvest plants
28 const inventoryChanges: IInventoryChanges = {};
29 const rewards: Record<string, IMissionReward[][]> = {};
30 for (const planter of personalRooms.Apartment.Gardening.Planters) {
31 rewards[planter.Name] = [];
32 for (const plant of planter.Plants) {
33 const itemType =
34 "/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" +
35 plant.PlantType.substring(plant.PlantType.length - 1);
36 const itemCount = Math.random() < 0.775 ? 2 : 4;
37
38 addMiscItem(inventory, itemType, itemCount, inventoryChanges);
39
40 rewards[planter.Name].push([
41 {
42 StoreItem: toStoreItem(itemType),
43 TypeName: itemType,
44 ItemCount: itemCount,
45 DailyCooldown: false,
46 Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564,
47 TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`,
48 ProductCategory: "MiscItems"
49 }
50 ]);
51 }
52 }
53
54 // Refresh garden
55 personalRooms.Apartment.Gardening = createGarden();
56
57 await Promise.all([inventory.save(), personalRooms.save()]);
58
59 const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1];
60 const plant = planter.Plants[planter.Plants.length - 1];
61 res.json({
62 GardenTagName: planter.Name,
63 PlantType: plant.PlantType,
64 PlotIndex: plant.PlotIndex,
65 EndTime: toMongoDate(plant.EndTime),
66 InventoryChanges: inventoryChanges,
67 Gardening: personalRooms.toJSON<IPersonalRoomsClient>().Apartment.Gardening,
68 Rewards: rewards
69 } satisfies IGardeningResponse);
70 };
71
72 interface IGardeningRequest {
73 Mode: string;
74 }
75
76 interface IGardeningResponse {
77 GardenTagName: string;
78 PlantType: string;
79 PlotIndex: number;
80 EndTime: IMongoDate;
81 InventoryChanges: IInventoryChanges;
82 Gardening: IGardeningClient;
83 Rewards: Record<string, IMissionReward[][]>;
84 }
Modified src/controllers/api/getShipController.ts +10 -3
@@ -2,17 +2,24 @@ import { RequestHandler } from "express";
2 2 import { config } from "@/src/services/configService";
3 3 import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
4 4 import { getAccountIdForRequest } from "@/src/services/loginService";
5 import { getPersonalRooms } from "@/src/services/personalRoomsService";
5 import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
6 6 import { getShip } from "@/src/services/shipService";
7 7 import { toOid } from "@/src/helpers/inventoryHelpers";
8 8 import { IGetShipResponse } from "@/src/types/shipTypes";
9 import { IPersonalRooms } from "@/src/types/personalRoomsTypes";
9 import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
10 10 import { getLoadout } from "@/src/services/loadoutService";
11 11
12 12 export const getShipController: RequestHandler = async (req, res) => {
13 13 const accountId = await getAccountIdForRequest(req);
14 14 const personalRoomsDb = await getPersonalRooms(accountId);
15 const personalRooms = personalRoomsDb.toJSON<IPersonalRooms>();
15
16 // Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future.
17 if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) {
18 personalRoomsDb.Apartment.Gardening = createGarden();
19 await personalRoomsDb.save();
20 }
21
22 const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
16 23 const loadout = await getLoadout(accountId);
17 24 const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
18 25
Modified src/models/personalRoomsModel.ts +42 -7
@@ -1,14 +1,17 @@
1 import { toOid } from "@/src/helpers/inventoryHelpers";
1 import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
2 2 import { colorSchema } from "@/src/models/inventoryModels/inventoryModel";
3 3 import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes";
4 4 import {
5 5 IFavouriteLoadoutDatabase,
6 IGardening,
6 IGardeningDatabase,
7 7 IPlacedDecosDatabase,
8 8 IPictureFrameInfo,
9 9 IRoom,
10 10 ITailorShopDatabase,
11 IApartmentDatabase
11 IApartmentDatabase,
12 IPlanterDatabase,
13 IPlantDatabase,
14 IPlantClient
12 15 } from "@/src/types/shipTypes";
13 16 import { Schema, model } from "mongoose";
14 17
@@ -77,15 +80,45 @@ favouriteLoadoutSchema.set("toJSON", {
77 80 }
78 81 });
79 82
80 const gardeningSchema = new Schema<IGardening>({
81 Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening
83 const plantSchema = new Schema<IPlantDatabase>(
84 {
85 PlantType: String,
86 EndTime: Date,
87 PlotIndex: Number
88 },
89 { _id: false }
90 );
91
92 plantSchema.set("toJSON", {
93 virtuals: true,
94 transform(_doc, obj) {
95 const client = obj as IPlantClient;
96 const db = obj as IPlantDatabase;
97
98 client.EndTime = toMongoDate(db.EndTime);
99 }
82 100 });
83 101
102 const planterSchema = new Schema<IPlanterDatabase>(
103 {
104 Name: { type: String, required: true },
105 Plants: { type: [plantSchema], default: [] }
106 },
107 { _id: false }
108 );
109
110 const gardeningSchema = new Schema<IGardeningDatabase>(
111 {
112 Planters: { type: [planterSchema], default: [] }
113 },
114 { _id: false }
115 );
116
84 117 const apartmentSchema = new Schema<IApartmentDatabase>(
85 118 {
86 119 Rooms: [roomSchema],
87 120 FavouriteLoadouts: [favouriteLoadoutSchema],
88 Gardening: gardeningSchema // TODO: ensure this is correct
121 Gardening: gardeningSchema
89 122 },
90 123 { _id: false }
91 124 );
@@ -98,7 +131,9 @@ const apartmentDefault: IApartmentDatabase = {
98 131 { Name: "DuviriHallway", MaxCapacity: 1600 }
99 132 ],
100 133 FavouriteLoadouts: [],
101 Gardening: {}
134 Gardening: {
135 Planters: []
136 }
102 137 };
103 138
104 139 const orbiterSchema = new Schema<IOrbiter>(
Modified src/routes/api.ts +2 -0
@@ -48,6 +48,7 @@ import { findSessionsController } from "@/src/controllers/api/findSessionsContro
48 48 import { fishmongerController } from "@/src/controllers/api/fishmongerController";
49 49 import { focusController } from "@/src/controllers/api/focusController";
50 50 import { fusionTreasuresController } from "@/src/controllers/api/fusionTreasuresController";
51 import { gardeningController } from "@/src/controllers/api/gardeningController";
51 52 import { genericUpdateController } from "@/src/controllers/api/genericUpdateController";
52 53 import { getAllianceController } from "@/src/controllers/api/getAllianceController";
53 54 import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController";
@@ -240,6 +241,7 @@ apiRouter.post("/findSessions.php", findSessionsController);
240 241 apiRouter.post("/fishmonger.php", fishmongerController);
241 242 apiRouter.post("/focus.php", focusController);
242 243 apiRouter.post("/fusionTreasures.php", fusionTreasuresController);
244 apiRouter.post("/gardening.php", gardeningController);
243 245 apiRouter.post("/genericUpdate.php", genericUpdateController);
244 246 apiRouter.post("/getAlliance.php", getAllianceController);
245 247 apiRouter.post("/getFriends.php", getFriendsController);
Modified src/services/inventoryService.ts +25 -0
@@ -213,6 +213,15 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del
213 213 for (const key in delta) {
214 214 if (!(key in InventoryChanges)) {
215 215 InventoryChanges[key] = delta[key];
216 } else if (key == "MiscItems") {
217 for (const deltaItem of delta[key]!) {
218 const existing = InventoryChanges[key]!.find(x => x.ItemType == deltaItem.ItemType);
219 if (existing) {
220 existing.ItemCount += deltaItem.ItemCount;
221 } else {
222 InventoryChanges[key]!.push(deltaItem);
223 }
224 }
216 225 } else if (Array.isArray(delta[key])) {
217 226 const left = InventoryChanges[key] as object[];
218 227 const right: object[] = delta[key];
@@ -1468,6 +1477,22 @@ export const addGearExpByCategory = (
1468 1477 });
1469 1478 };
1470 1479
1480 export const addMiscItem = (
1481 inventory: TInventoryDatabaseDocument,
1482 type: string,
1483 count: number,
1484 inventoryChanges: IInventoryChanges
1485 ): void => {
1486 const miscItemChanges: IMiscItem[] = [
1487 {
1488 ItemType: type,
1489 ItemCount: count
1490 }
1491 ];
1492 addMiscItems(inventory, miscItemChanges);
1493 combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
1494 };
1495
1471 1496 export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => {
1472 1497 const { MiscItems } = inventory;
1473 1498
Modified src/services/personalRoomsService.ts +68 -2
@@ -1,9 +1,14 @@
1 1 import { PersonalRooms } from "@/src/models/personalRoomsModel";
2 2 import { addItem, getInventory } from "@/src/services/inventoryService";
3 3 import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes";
4 import { IGardeningDatabase } from "../types/shipTypes";
5 import { getRandomElement } from "./rngService";
4 6
5 export const getPersonalRooms = async (accountId: string): Promise<TPersonalRoomsDatabaseDocument> => {
6 const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId });
7 export const getPersonalRooms = async (
8 accountId: string,
9 projection?: string
10 ): Promise<TPersonalRoomsDatabaseDocument> => {
11 const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }, projection);
7 12
8 13 if (!personalRooms) {
9 14 throw new Error(`personal rooms not found for account ${accountId}`);
@@ -25,3 +30,64 @@ export const updateShipFeature = async (accountId: string, shipFeature: string):
25 30 await addItem(inventory, shipFeature, -1);
26 31 await inventory.save();
27 32 };
33
34 export const createGarden = (): IGardeningDatabase => {
35 const plantTypes = [
36 "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantA",
37 "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantB",
38 "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantC",
39 "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantD",
40 "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantE",
41 "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantF"
42 ];
43 const endTime = new Date((Math.trunc(Date.now() / 1000) + 79200) * 1000); // Plants will take 22 hours to grow
44 return {
45 Planters: [
46 {
47 Name: "Garden0",
48 Plants: [
49 {
50 PlantType: getRandomElement(plantTypes),
51 EndTime: endTime,
52 PlotIndex: 0
53 },
54 {
55 PlantType: getRandomElement(plantTypes),
56 EndTime: endTime,
57 PlotIndex: 1
58 }
59 ]
60 },
61 {
62 Name: "Garden1",
63 Plants: [
64 {
65 PlantType: getRandomElement(plantTypes),
66 EndTime: endTime,
67 PlotIndex: 0
68 },
69 {
70 PlantType: getRandomElement(plantTypes),
71 EndTime: endTime,
72 PlotIndex: 1
73 }
74 ]
75 },
76 {
77 Name: "Garden2",
78 Plants: [
79 {
80 PlantType: getRandomElement(plantTypes),
81 EndTime: endTime,
82 PlotIndex: 0
83 },
84 {
85 PlantType: getRandomElement(plantTypes),
86 EndTime: endTime,
87 PlotIndex: 1
88 }
89 ]
90 }
91 ]
92 };
93 };
Modified src/types/missionTypes.ts +2 -0
@@ -8,6 +8,8 @@ export interface IMissionReward {
8 8 TypeName?: string;
9 9 UpgradeLevel?: number;
10 10 ItemCount: number;
11 DailyCooldown?: boolean;
12 Rarity?: number;
11 13 TweetText?: string;
12 14 ProductCategory?: string;
13 15 FromEnemyCache?: boolean;
Modified src/types/personalRoomsTypes.ts +4 -4
@@ -1,12 +1,12 @@
1 1 import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes";
2 2 import {
3 IApartment,
4 3 IRoom,
5 4 IPlacedDecosDatabase,
6 5 ITailorShop,
7 6 ITailorShopDatabase,
8 7 TBootLocation,
9 IApartmentDatabase
8 IApartmentDatabase,
9 IApartmentClient
10 10 } from "@/src/types/shipTypes";
11 11 import { Document, Model, Types } from "mongoose";
12 12
@@ -21,10 +21,10 @@ export interface IOrbiter {
21 21 BootLocation?: TBootLocation;
22 22 }
23 23
24 export interface IPersonalRooms {
24 export interface IPersonalRoomsClient {
25 25 ShipInteriorColors: IColor;
26 26 Ship: IOrbiter;
27 Apartment: IApartment;
27 Apartment: IApartmentClient;
28 28 TailorShop: ITailorShop;
29 29 }
30 30
Modified src/types/shipTypes.ts +25 -11
@@ -1,12 +1,12 @@
1 1 import { Types } from "mongoose";
2 import { IOid } from "@/src/types/commonTypes";
2 import { IMongoDate, IOid } from "@/src/types/commonTypes";
3 3 import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes";
4 4 import { ILoadoutClient } from "./saveLoadoutTypes";
5 5
6 6 export interface IGetShipResponse {
7 7 ShipOwnerId: string;
8 8 Ship: IShip;
9 Apartment: IApartment;
9 Apartment: IApartmentClient;
10 10 TailorShop: ITailorShop;
11 11 LoadOutInventory: { LoadOutPresets: ILoadoutClient };
12 12 }
@@ -51,28 +51,42 @@ export interface IRoom {
51 51 PlacedDecos?: IPlacedDecosDatabase[];
52 52 }
53 53
54 export interface IPlants {
54 export interface IPlantClient {
55 55 PlantType: string;
56 EndTime: IOid;
56 EndTime: IMongoDate;
57 57 PlotIndex: number;
58 58 }
59 export interface IPlanters {
59
60 export interface IPlantDatabase extends Omit<IPlantClient, "EndTime"> {
61 EndTime: Date;
62 }
63
64 export interface IPlanterClient {
65 Name: string;
66 Plants: IPlantClient[];
67 }
68
69 export interface IPlanterDatabase {
60 70 Name: string;
61 Plants: IPlants[];
71 Plants: IPlantDatabase[];
72 }
73
74 export interface IGardeningClient {
75 Planters: IPlanterClient[];
62 76 }
63 77
64 export interface IGardening {
65 Planters?: IPlanters[];
78 export interface IGardeningDatabase {
79 Planters: IPlanterDatabase[];
66 80 }
67 81
68 export interface IApartment {
69 Gardening: IGardening;
82 export interface IApartmentClient {
83 Gardening: IGardeningClient;
70 84 Rooms: IRoom[];
71 85 FavouriteLoadouts: IFavouriteLoadout[];
72 86 }
73 87
74 88 export interface IApartmentDatabase {
75 Gardening: IGardening;
89 Gardening: IGardeningDatabase;
76 90 Rooms: IRoom[];
77 91 FavouriteLoadouts: IFavouriteLoadoutDatabase[];
78 92 }