From a90d3a515632d4e67c6668fd967e7a84939fa56c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:54:06 -0700 Subject: [PATCH] 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> --- src/controllers/api/gardeningController.ts | 84 ++++++++++++++++++++++ src/controllers/api/getShipController.ts | 13 +++- src/models/personalRoomsModel.ts | 49 +++++++++++-- src/routes/api.ts | 2 + src/services/inventoryService.ts | 25 +++++++ src/services/personalRoomsService.ts | 70 +++++++++++++++++- src/types/missionTypes.ts | 2 + src/types/personalRoomsTypes.ts | 8 +-- src/types/shipTypes.ts | 36 +++++++--- 9 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 src/controllers/api/gardeningController.ts diff --git a/src/controllers/api/gardeningController.ts b/src/controllers/api/gardeningController.ts new file mode 100644 index 00000000..1913bd63 --- /dev/null +++ b/src/controllers/api/gardeningController.ts @@ -0,0 +1,84 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addMiscItem, getInventory } from "@/src/services/inventoryService"; +import { toStoreItem } from "@/src/services/itemDataService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IMissionReward } from "@/src/types/missionTypes"; +import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IGardeningClient } from "@/src/types/shipTypes"; +import { RequestHandler } from "express"; +import { dict_en, ExportResources } from "warframe-public-export-plus"; + +export const gardeningController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + if (data.Mode != "HarvestAll") { + throw new Error(`unexpected gardening mode: ${data.Mode}`); + } + + const accountId = await getAccountIdForRequest(req); + const [inventory, personalRooms] = await Promise.all([ + getInventory(accountId, "MiscItems"), + getPersonalRooms(accountId, "Apartment") + ]); + + // Harvest plants + const inventoryChanges: IInventoryChanges = {}; + const rewards: Record = {}; + for (const planter of personalRooms.Apartment.Gardening.Planters) { + rewards[planter.Name] = []; + for (const plant of planter.Plants) { + const itemType = + "/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" + + plant.PlantType.substring(plant.PlantType.length - 1); + const itemCount = Math.random() < 0.775 ? 2 : 4; + + addMiscItem(inventory, itemType, itemCount, inventoryChanges); + + rewards[planter.Name].push([ + { + StoreItem: toStoreItem(itemType), + TypeName: itemType, + ItemCount: itemCount, + DailyCooldown: false, + Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564, + TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`, + ProductCategory: "MiscItems" + } + ]); + } + } + + // Refresh garden + personalRooms.Apartment.Gardening = createGarden(); + + await Promise.all([inventory.save(), personalRooms.save()]); + + const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1]; + const plant = planter.Plants[planter.Plants.length - 1]; + res.json({ + GardenTagName: planter.Name, + PlantType: plant.PlantType, + PlotIndex: plant.PlotIndex, + EndTime: toMongoDate(plant.EndTime), + InventoryChanges: inventoryChanges, + Gardening: personalRooms.toJSON().Apartment.Gardening, + Rewards: rewards + } satisfies IGardeningResponse); +}; + +interface IGardeningRequest { + Mode: string; +} + +interface IGardeningResponse { + GardenTagName: string; + PlantType: string; + PlotIndex: number; + EndTime: IMongoDate; + InventoryChanges: IInventoryChanges; + Gardening: IGardeningClient; + Rewards: Record; +} diff --git a/src/controllers/api/getShipController.ts b/src/controllers/api/getShipController.ts index ac1ebadf..c68ff789 100644 --- a/src/controllers/api/getShipController.ts +++ b/src/controllers/api/getShipController.ts @@ -2,17 +2,24 @@ import { RequestHandler } from "express"; import { config } from "@/src/services/configService"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getPersonalRooms } from "@/src/services/personalRoomsService"; +import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { getShip } from "@/src/services/shipService"; import { toOid } from "@/src/helpers/inventoryHelpers"; import { IGetShipResponse } from "@/src/types/shipTypes"; -import { IPersonalRooms } from "@/src/types/personalRoomsTypes"; +import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { getLoadout } from "@/src/services/loadoutService"; export const getShipController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const personalRoomsDb = await getPersonalRooms(accountId); - const personalRooms = personalRoomsDb.toJSON(); + + // Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future. + if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) { + personalRoomsDb.Apartment.Gardening = createGarden(); + await personalRoomsDb.save(); + } + + const personalRooms = personalRoomsDb.toJSON(); const loadout = await getLoadout(accountId); const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem"); diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index 0fcdda72..e6176ead 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -1,14 +1,17 @@ -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { colorSchema } from "@/src/models/inventoryModels/inventoryModel"; import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes"; import { IFavouriteLoadoutDatabase, - IGardening, + IGardeningDatabase, IPlacedDecosDatabase, IPictureFrameInfo, IRoom, ITailorShopDatabase, - IApartmentDatabase + IApartmentDatabase, + IPlanterDatabase, + IPlantDatabase, + IPlantClient } from "@/src/types/shipTypes"; import { Schema, model } from "mongoose"; @@ -77,15 +80,45 @@ favouriteLoadoutSchema.set("toJSON", { } }); -const gardeningSchema = new Schema({ - Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening +const plantSchema = new Schema( + { + PlantType: String, + EndTime: Date, + PlotIndex: Number + }, + { _id: false } +); + +plantSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const client = obj as IPlantClient; + const db = obj as IPlantDatabase; + + client.EndTime = toMongoDate(db.EndTime); + } }); +const planterSchema = new Schema( + { + Name: { type: String, required: true }, + Plants: { type: [plantSchema], default: [] } + }, + { _id: false } +); + +const gardeningSchema = new Schema( + { + Planters: { type: [planterSchema], default: [] } + }, + { _id: false } +); + const apartmentSchema = new Schema( { Rooms: [roomSchema], FavouriteLoadouts: [favouriteLoadoutSchema], - Gardening: gardeningSchema // TODO: ensure this is correct + Gardening: gardeningSchema }, { _id: false } ); @@ -98,7 +131,9 @@ const apartmentDefault: IApartmentDatabase = { { Name: "DuviriHallway", MaxCapacity: 1600 } ], FavouriteLoadouts: [], - Gardening: {} + Gardening: { + Planters: [] + } }; const orbiterSchema = new Schema( diff --git a/src/routes/api.ts b/src/routes/api.ts index b11af3e4..f594c845 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -48,6 +48,7 @@ import { findSessionsController } from "@/src/controllers/api/findSessionsContro import { fishmongerController } from "@/src/controllers/api/fishmongerController"; import { focusController } from "@/src/controllers/api/focusController"; import { fusionTreasuresController } from "@/src/controllers/api/fusionTreasuresController"; +import { gardeningController } from "@/src/controllers/api/gardeningController"; import { genericUpdateController } from "@/src/controllers/api/genericUpdateController"; import { getAllianceController } from "@/src/controllers/api/getAllianceController"; import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController"; @@ -240,6 +241,7 @@ apiRouter.post("/findSessions.php", findSessionsController); apiRouter.post("/fishmonger.php", fishmongerController); apiRouter.post("/focus.php", focusController); apiRouter.post("/fusionTreasures.php", fusionTreasuresController); +apiRouter.post("/gardening.php", gardeningController); apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/getAlliance.php", getAllianceController); apiRouter.post("/getFriends.php", getFriendsController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 31abb4ef..035984c2 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -213,6 +213,15 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del for (const key in delta) { if (!(key in InventoryChanges)) { InventoryChanges[key] = delta[key]; + } else if (key == "MiscItems") { + for (const deltaItem of delta[key]!) { + const existing = InventoryChanges[key]!.find(x => x.ItemType == deltaItem.ItemType); + if (existing) { + existing.ItemCount += deltaItem.ItemCount; + } else { + InventoryChanges[key]!.push(deltaItem); + } + } } else if (Array.isArray(delta[key])) { const left = InventoryChanges[key] as object[]; const right: object[] = delta[key]; @@ -1468,6 +1477,22 @@ export const addGearExpByCategory = ( }); }; +export const addMiscItem = ( + inventory: TInventoryDatabaseDocument, + type: string, + count: number, + inventoryChanges: IInventoryChanges +): void => { + const miscItemChanges: IMiscItem[] = [ + { + ItemType: type, + ItemCount: count + } + ]; + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); +}; + export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => { const { MiscItems } = inventory; diff --git a/src/services/personalRoomsService.ts b/src/services/personalRoomsService.ts index 24399655..97f1b41d 100644 --- a/src/services/personalRoomsService.ts +++ b/src/services/personalRoomsService.ts @@ -1,9 +1,14 @@ import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { addItem, getInventory } from "@/src/services/inventoryService"; import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes"; +import { IGardeningDatabase } from "../types/shipTypes"; +import { getRandomElement } from "./rngService"; -export const getPersonalRooms = async (accountId: string): Promise => { - const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }); +export const getPersonalRooms = async ( + accountId: string, + projection?: string +): Promise => { + const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }, projection); if (!personalRooms) { throw new Error(`personal rooms not found for account ${accountId}`); @@ -25,3 +30,64 @@ export const updateShipFeature = async (accountId: string, shipFeature: string): await addItem(inventory, shipFeature, -1); await inventory.save(); }; + +export const createGarden = (): IGardeningDatabase => { + const plantTypes = [ + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantA", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantB", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantC", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantD", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantE", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantF" + ]; + const endTime = new Date((Math.trunc(Date.now() / 1000) + 79200) * 1000); // Plants will take 22 hours to grow + return { + Planters: [ + { + Name: "Garden0", + Plants: [ + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 1 + } + ] + }, + { + Name: "Garden1", + Plants: [ + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 1 + } + ] + }, + { + Name: "Garden2", + Plants: [ + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 1 + } + ] + } + ] + }; +}; diff --git a/src/types/missionTypes.ts b/src/types/missionTypes.ts index be1d08bc..3de75318 100644 --- a/src/types/missionTypes.ts +++ b/src/types/missionTypes.ts @@ -8,6 +8,8 @@ export interface IMissionReward { TypeName?: string; UpgradeLevel?: number; ItemCount: number; + DailyCooldown?: boolean; + Rarity?: number; TweetText?: string; ProductCategory?: string; FromEnemyCache?: boolean; diff --git a/src/types/personalRoomsTypes.ts b/src/types/personalRoomsTypes.ts index 68239bb4..325ab9e4 100644 --- a/src/types/personalRoomsTypes.ts +++ b/src/types/personalRoomsTypes.ts @@ -1,12 +1,12 @@ import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { - IApartment, IRoom, IPlacedDecosDatabase, ITailorShop, ITailorShopDatabase, TBootLocation, - IApartmentDatabase + IApartmentDatabase, + IApartmentClient } from "@/src/types/shipTypes"; import { Document, Model, Types } from "mongoose"; @@ -21,10 +21,10 @@ export interface IOrbiter { BootLocation?: TBootLocation; } -export interface IPersonalRooms { +export interface IPersonalRoomsClient { ShipInteriorColors: IColor; Ship: IOrbiter; - Apartment: IApartment; + Apartment: IApartmentClient; TailorShop: ITailorShop; } diff --git a/src/types/shipTypes.ts b/src/types/shipTypes.ts index a097f3ca..74eaaeae 100644 --- a/src/types/shipTypes.ts +++ b/src/types/shipTypes.ts @@ -1,12 +1,12 @@ import { Types } from "mongoose"; -import { IOid } from "@/src/types/commonTypes"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ILoadoutClient } from "./saveLoadoutTypes"; export interface IGetShipResponse { ShipOwnerId: string; Ship: IShip; - Apartment: IApartment; + Apartment: IApartmentClient; TailorShop: ITailorShop; LoadOutInventory: { LoadOutPresets: ILoadoutClient }; } @@ -51,28 +51,42 @@ export interface IRoom { PlacedDecos?: IPlacedDecosDatabase[]; } -export interface IPlants { +export interface IPlantClient { PlantType: string; - EndTime: IOid; + EndTime: IMongoDate; PlotIndex: number; } -export interface IPlanters { + +export interface IPlantDatabase extends Omit { + EndTime: Date; +} + +export interface IPlanterClient { Name: string; - Plants: IPlants[]; + Plants: IPlantClient[]; } -export interface IGardening { - Planters?: IPlanters[]; +export interface IPlanterDatabase { + Name: string; + Plants: IPlantDatabase[]; } -export interface IApartment { - Gardening: IGardening; +export interface IGardeningClient { + Planters: IPlanterClient[]; +} + +export interface IGardeningDatabase { + Planters: IPlanterDatabase[]; +} + +export interface IApartmentClient { + Gardening: IGardeningClient; Rooms: IRoom[]; FavouriteLoadouts: IFavouriteLoadout[]; } export interface IApartmentDatabase { - Gardening: IGardening; + Gardening: IGardeningDatabase; Rooms: IRoom[]; FavouriteLoadouts: IFavouriteLoadoutDatabase[]; }