feat: gardening #1849

Merged
Sainan merged 5 commits from gardening-setup into main 2025-04-26 11:54:07 -07:00
9 changed files with 262 additions and 27 deletions

View File

@ -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<IGardeningRequest>(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<string, IMissionReward[][]> = {};
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<IPersonalRoomsClient>().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<string, IMissionReward[][]>;
}

View File

@ -2,17 +2,24 @@ import { RequestHandler } from "express";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService"; 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 { getShip } from "@/src/services/shipService";
import { toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes"; import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRooms } from "@/src/types/personalRoomsTypes"; import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { getLoadout } from "@/src/services/loadoutService"; import { getLoadout } from "@/src/services/loadoutService";
export const getShipController: RequestHandler = async (req, res) => { export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const personalRoomsDb = await getPersonalRooms(accountId); const personalRoomsDb = await getPersonalRooms(accountId);
const personalRooms = personalRoomsDb.toJSON<IPersonalRooms>();
// 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<IPersonalRoomsClient>();
const loadout = await getLoadout(accountId); const loadout = await getLoadout(accountId);
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem"); const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");

View File

@ -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 { colorSchema } from "@/src/models/inventoryModels/inventoryModel";
import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes"; import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes";
import { import {
IFavouriteLoadoutDatabase, IFavouriteLoadoutDatabase,
IGardening, IGardeningDatabase,
IPlacedDecosDatabase, IPlacedDecosDatabase,
IPictureFrameInfo, IPictureFrameInfo,
IRoom, IRoom,
ITailorShopDatabase, ITailorShopDatabase,
IApartmentDatabase IApartmentDatabase,
IPlanterDatabase,
IPlantDatabase,
IPlantClient
} from "@/src/types/shipTypes"; } from "@/src/types/shipTypes";
import { Schema, model } from "mongoose"; import { Schema, model } from "mongoose";
@ -77,15 +80,45 @@ favouriteLoadoutSchema.set("toJSON", {
} }
}); });
const gardeningSchema = new Schema<IGardening>({ const plantSchema = new Schema<IPlantDatabase>(
Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening {
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<IPlanterDatabase>(
{
Name: { type: String, required: true },
Plants: { type: [plantSchema], default: [] }
},
{ _id: false }
);
const gardeningSchema = new Schema<IGardeningDatabase>(
{
Planters: { type: [planterSchema], default: [] }
},
{ _id: false }
);
const apartmentSchema = new Schema<IApartmentDatabase>( const apartmentSchema = new Schema<IApartmentDatabase>(
{ {
Rooms: [roomSchema], Rooms: [roomSchema],
FavouriteLoadouts: [favouriteLoadoutSchema], FavouriteLoadouts: [favouriteLoadoutSchema],
Gardening: gardeningSchema // TODO: ensure this is correct Gardening: gardeningSchema
}, },
{ _id: false } { _id: false }
); );
@ -98,7 +131,9 @@ const apartmentDefault: IApartmentDatabase = {
{ Name: "DuviriHallway", MaxCapacity: 1600 } { Name: "DuviriHallway", MaxCapacity: 1600 }
], ],
FavouriteLoadouts: [], FavouriteLoadouts: [],
Gardening: {} Gardening: {
Planters: []
}
}; };
const orbiterSchema = new Schema<IOrbiter>( const orbiterSchema = new Schema<IOrbiter>(

View File

@ -48,6 +48,7 @@ import { findSessionsController } from "@/src/controllers/api/findSessionsContro
import { fishmongerController } from "@/src/controllers/api/fishmongerController"; import { fishmongerController } from "@/src/controllers/api/fishmongerController";
import { focusController } from "@/src/controllers/api/focusController"; import { focusController } from "@/src/controllers/api/focusController";
import { fusionTreasuresController } from "@/src/controllers/api/fusionTreasuresController"; import { fusionTreasuresController } from "@/src/controllers/api/fusionTreasuresController";
import { gardeningController } from "@/src/controllers/api/gardeningController";
import { genericUpdateController } from "@/src/controllers/api/genericUpdateController"; import { genericUpdateController } from "@/src/controllers/api/genericUpdateController";
import { getAllianceController } from "@/src/controllers/api/getAllianceController"; import { getAllianceController } from "@/src/controllers/api/getAllianceController";
import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController"; import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController";
@ -240,6 +241,7 @@ apiRouter.post("/findSessions.php", findSessionsController);
apiRouter.post("/fishmonger.php", fishmongerController); apiRouter.post("/fishmonger.php", fishmongerController);
apiRouter.post("/focus.php", focusController); apiRouter.post("/focus.php", focusController);
apiRouter.post("/fusionTreasures.php", fusionTreasuresController); apiRouter.post("/fusionTreasures.php", fusionTreasuresController);
apiRouter.post("/gardening.php", gardeningController);
apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/genericUpdate.php", genericUpdateController);
apiRouter.post("/getAlliance.php", getAllianceController); apiRouter.post("/getAlliance.php", getAllianceController);
apiRouter.post("/getFriends.php", getFriendsController); apiRouter.post("/getFriends.php", getFriendsController);

View File

@ -213,6 +213,15 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del
for (const key in delta) { for (const key in delta) {
if (!(key in InventoryChanges)) { if (!(key in InventoryChanges)) {
InventoryChanges[key] = delta[key]; 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])) { } else if (Array.isArray(delta[key])) {
const left = InventoryChanges[key] as object[]; const left = InventoryChanges[key] as object[];
const right: object[] = delta[key]; 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 => { export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => {
const { MiscItems } = inventory; const { MiscItems } = inventory;

View File

@ -1,9 +1,14 @@
import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { PersonalRooms } from "@/src/models/personalRoomsModel";
import { addItem, getInventory } from "@/src/services/inventoryService"; import { addItem, getInventory } from "@/src/services/inventoryService";
import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes"; import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes";
import { IGardeningDatabase } from "../types/shipTypes";
import { getRandomElement } from "./rngService";
export const getPersonalRooms = async (accountId: string): Promise<TPersonalRoomsDatabaseDocument> => { export const getPersonalRooms = async (
const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }); accountId: string,
projection?: string
): Promise<TPersonalRoomsDatabaseDocument> => {
const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }, projection);
if (!personalRooms) { if (!personalRooms) {
throw new Error(`personal rooms not found for account ${accountId}`); 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 addItem(inventory, shipFeature, -1);
await inventory.save(); 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
}
]
}
]
};
};

View File

@ -8,6 +8,8 @@ export interface IMissionReward {
TypeName?: string; TypeName?: string;
UpgradeLevel?: number; UpgradeLevel?: number;
ItemCount: number; ItemCount: number;
DailyCooldown?: boolean;
Rarity?: number;
TweetText?: string; TweetText?: string;
ProductCategory?: string; ProductCategory?: string;
FromEnemyCache?: boolean; FromEnemyCache?: boolean;

View File

@ -1,12 +1,12 @@
import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { import {
IApartment,
IRoom, IRoom,
IPlacedDecosDatabase, IPlacedDecosDatabase,
ITailorShop, ITailorShop,
ITailorShopDatabase, ITailorShopDatabase,
TBootLocation, TBootLocation,
IApartmentDatabase IApartmentDatabase,
IApartmentClient
} from "@/src/types/shipTypes"; } from "@/src/types/shipTypes";
import { Document, Model, Types } from "mongoose"; import { Document, Model, Types } from "mongoose";
@ -21,10 +21,10 @@ export interface IOrbiter {
BootLocation?: TBootLocation; BootLocation?: TBootLocation;
} }
export interface IPersonalRooms { export interface IPersonalRoomsClient {
ShipInteriorColors: IColor; ShipInteriorColors: IColor;
Ship: IOrbiter; Ship: IOrbiter;
Apartment: IApartment; Apartment: IApartmentClient;
TailorShop: ITailorShop; TailorShop: ITailorShop;
} }

View File

@ -1,12 +1,12 @@
import { Types } from "mongoose"; 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 { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ILoadoutClient } from "./saveLoadoutTypes"; import { ILoadoutClient } from "./saveLoadoutTypes";
export interface IGetShipResponse { export interface IGetShipResponse {
ShipOwnerId: string; ShipOwnerId: string;
Ship: IShip; Ship: IShip;
Apartment: IApartment; Apartment: IApartmentClient;
TailorShop: ITailorShop; TailorShop: ITailorShop;
LoadOutInventory: { LoadOutPresets: ILoadoutClient }; LoadOutInventory: { LoadOutPresets: ILoadoutClient };
} }
@ -51,28 +51,42 @@ export interface IRoom {
PlacedDecos?: IPlacedDecosDatabase[]; PlacedDecos?: IPlacedDecosDatabase[];
} }
export interface IPlants { export interface IPlantClient {
PlantType: string; PlantType: string;
EndTime: IOid; EndTime: IMongoDate;
PlotIndex: number; PlotIndex: number;
} }
export interface IPlanters {
export interface IPlantDatabase extends Omit<IPlantClient, "EndTime"> {
EndTime: Date;
}
export interface IPlanterClient {
Name: string; Name: string;
Plants: IPlants[]; Plants: IPlantClient[];
} }
export interface IGardening { export interface IPlanterDatabase {
Planters?: IPlanters[]; Name: string;
Plants: IPlantDatabase[];
} }
export interface IApartment { export interface IGardeningClient {
Gardening: IGardening; Planters: IPlanterClient[];
}
export interface IGardeningDatabase {
Planters: IPlanterDatabase[];
}
export interface IApartmentClient {
Gardening: IGardeningClient;
Rooms: IRoom[]; Rooms: IRoom[];
FavouriteLoadouts: IFavouriteLoadout[]; FavouriteLoadouts: IFavouriteLoadout[];
} }
export interface IApartmentDatabase { export interface IApartmentDatabase {
Gardening: IGardening; Gardening: IGardeningDatabase;
Rooms: IRoom[]; Rooms: IRoom[];
FavouriteLoadouts: IFavouriteLoadoutDatabase[]; FavouriteLoadouts: IFavouriteLoadoutDatabase[];
} }