From 8b50189fcff70c150da33c9e932c4dcda3289798 Mon Sep 17 00:00:00 2001 From: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:49:45 +0100 Subject: [PATCH] Foundry 1 - Preliminary (#127) --- src/constants/timeConstants.ts | 2 + .../api/claimCompletedRecipeController.ts | 64 ++++++++++ src/controllers/api/inventoryController.ts | 1 + src/controllers/api/startRecipeController.ts | 21 ++++ src/controllers/custom/addItemController.ts | 2 +- src/helpers/customHelpers/addItemHelpers.ts | 14 +-- src/helpers/purchaseHelpers.ts | 19 +-- src/helpers/stringHelpers.ts | 10 +- src/models/inventoryModels/inventoryModel.ts | 71 ++++++----- src/routes/api.ts | 5 + src/services/inventoryService.ts | 3 +- src/services/itemDataService.ts | 117 ++++++++++++++++++ .../missionInventoryUpdateService .ts | 9 +- src/services/purchaseService.ts | 3 +- src/services/recipeService.ts | 74 +++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 13 +- static/data/items.ts | 60 --------- 17 files changed, 363 insertions(+), 125 deletions(-) create mode 100644 src/controllers/api/claimCompletedRecipeController.ts create mode 100644 src/controllers/api/startRecipeController.ts create mode 100644 src/services/itemDataService.ts create mode 100644 src/services/recipeService.ts diff --git a/src/constants/timeConstants.ts b/src/constants/timeConstants.ts index 4a5a374f3..32a037427 100644 --- a/src/constants/timeConstants.ts +++ b/src/constants/timeConstants.ts @@ -3,11 +3,13 @@ const secondsPerMinute = 60; const minutesPerHour = 60; const hoursPerDay = 24; +const unixSecond = millisecondsPerSecond; const unixMinute = secondsPerMinute * millisecondsPerSecond; const unixHour = unixMinute * minutesPerHour; const unixDay = hoursPerDay * unixHour; export const unixTimesInMs = { + second: unixSecond, minute: unixMinute, hour: unixHour, day: unixDay diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts new file mode 100644 index 000000000..02120194c --- /dev/null +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -0,0 +1,64 @@ +//this is a controller for the claimCompletedRecipe route +//it will claim a recipe for the user + +import { Request, RequestHandler, Response } from "express"; +import { logger } from "@/src/utils/logger"; +import { getItemByBlueprint, getItemCategoryByUniqueName } from "@/src/services/itemDataService"; +import { IOid } from "@/src/types/commonTypes"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { IInventoryDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; + +export interface IClaimCompletedRecipeRequest { + RecipeIds: IOid[]; +} + +// eslint-disable-next-line @typescript-eslint/no-misused-promises +export const claimCompletedRecipeController: RequestHandler = async (req, res) => { + const claimCompletedRecipeRequest = getJSONfromString(req.body.toString()) as IClaimCompletedRecipeRequest; + const accountId = req.query.accountId as string; + if (!accountId) throw new Error("no account id"); + + console.log(claimCompletedRecipeRequest); + const inventory = await getInventory(accountId); + const pendingRecipe = inventory.PendingRecipes.find( + recipe => recipe._id?.toString() === claimCompletedRecipeRequest.RecipeIds[0].$oid + ); + console.log(pendingRecipe); + if (!pendingRecipe) { + logger.error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`); + throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`); + } + + //check recipe is indeed ready to be completed + // if (pendingRecipe.CompletionDate > new Date()) { + // logger.error(`recipe ${pendingRecipe._id} is not ready to be completed`); + // throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`); + // } + + //get completed Items + const completedItemName = getItemByBlueprint(pendingRecipe.ItemType)?.uniqueName; + + if (!completedItemName) { + logger.error(`no completed item found for recipe ${pendingRecipe._id}`); + throw new Error(`no completed item found for recipe ${pendingRecipe._id}`); + } + const itemCategory = getItemCategoryByUniqueName(completedItemName) as keyof typeof inventory; + console.log(itemCategory); + //TODO: remove all Schema.Mixed for inventory[itemCategory] not to be any + //add item + //inventory[itemCategory]. + + //add additional item components like mods or weapons for a sentinel. + //const additionalItemComponents = itemComponents[uniqueName] + //add these items to inventory + //return changes as InventoryChanges + + //remove pending recipe + inventory.PendingRecipes.pull(pendingRecipe._id); + // await inventory.save(); + + logger.debug("Claiming Completed Recipe", { completedItemName }); + + res.json({ InventoryChanges: {} }); +}; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index c12387a96..bfdad3548 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -24,6 +24,7 @@ const inventoryController: RequestHandler = async (request: Request, response: R return; } + //TODO: make a function that converts from database representation to client const inventoryJSON = inventory.toJSON(); const inventoryResponse = toInventoryResponse(inventoryJSON); diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts new file mode 100644 index 000000000..1ce3da22a --- /dev/null +++ b/src/controllers/api/startRecipeController.ts @@ -0,0 +1,21 @@ +import { parseString } from "@/src/helpers/general"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { startRecipe } from "@/src/services/recipeService"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +interface IStartRecipeRequest { + RecipeName: string; + Ids: string[]; +} + +// eslint-disable-next-line @typescript-eslint/no-misused-promises +export const startRecipeController: RequestHandler = async (req, res) => { + const startRecipeRequest = getJSONfromString(req.body.toString()) as IStartRecipeRequest; + logger.debug("StartRecipe Request", { startRecipeRequest }); + + const accountId = parseString(req.query.accountId); + + const newRecipeId = await startRecipe(startRecipeRequest.RecipeName, accountId); + res.json(newRecipeId); +}; diff --git a/src/controllers/custom/addItemController.ts b/src/controllers/custom/addItemController.ts index 504d539e4..c81a861e8 100644 --- a/src/controllers/custom/addItemController.ts +++ b/src/controllers/custom/addItemController.ts @@ -1,5 +1,5 @@ import { ItemType, toAddItemRequest } from "@/src/helpers/customHelpers/addItemHelpers"; -import { getWeaponType } from "@/src/helpers/purchaseHelpers"; +import { getWeaponType } from "@/src/services/itemDataService"; import { addPowerSuit, addWeapon } from "@/src/services/inventoryService"; import { RequestHandler } from "express"; diff --git a/src/helpers/customHelpers/addItemHelpers.ts b/src/helpers/customHelpers/addItemHelpers.ts index efc517c31..d26d571f6 100644 --- a/src/helpers/customHelpers/addItemHelpers.ts +++ b/src/helpers/customHelpers/addItemHelpers.ts @@ -1,5 +1,5 @@ import { isString, parseString } from "@/src/helpers/general"; -import { items } from "@/static/data/items"; +import { items } from "@/src/services/itemDataService"; export enum ItemType { Powersuit = "Powersuit", @@ -23,20 +23,20 @@ interface IAddItemRequest { InternalName: string; accountId: string; } -export const isInternalName = (internalName: string): boolean => { +export const isInternalItemName = (internalName: string): boolean => { const item = items.find(i => i.uniqueName === internalName); return Boolean(item); }; -const parseInternalName = (internalName: unknown): string => { - if (!isString(internalName) || !isInternalName(internalName)) { +const parseInternalItemName = (internalName: unknown): string => { + if (!isString(internalName) || !isInternalItemName(internalName)) { throw new Error("incorrect internal name"); } return internalName; }; -const toAddItemRequest = (body: unknown): IAddItemRequest => { +export const toAddItemRequest = (body: unknown): IAddItemRequest => { if (!body || typeof body !== "object") { throw new Error("incorrect or missing add item request data"); } @@ -44,12 +44,10 @@ const toAddItemRequest = (body: unknown): IAddItemRequest => { if ("type" in body && "internalName" in body && "accountId" in body) { return { type: parseItemType(body.type), - InternalName: parseInternalName(body.internalName), + InternalName: parseInternalItemName(body.internalName), accountId: parseString(body.accountId) }; } throw new Error("malformed add item request"); }; - -export { toAddItemRequest }; diff --git a/src/helpers/purchaseHelpers.ts b/src/helpers/purchaseHelpers.ts index 47016abe5..8eb82c17e 100644 --- a/src/helpers/purchaseHelpers.ts +++ b/src/helpers/purchaseHelpers.ts @@ -1,8 +1,7 @@ import { parseBoolean, parseNumber, parseString } from "@/src/helpers/general"; -import { WeaponTypeInternal } from "@/src/services/inventoryService"; +import { weapons } from "@/src/services/itemDataService"; import { slotPurchaseNameToSlotName } from "@/src/services/purchaseService"; import { IPurchaseRequest, SlotPurchaseName } from "@/src/types/purchaseTypes"; -import { weapons } from "@/static/data/items"; export const toPurchaseRequest = (purchaseRequest: unknown): IPurchaseRequest => { if (!purchaseRequest || typeof purchaseRequest !== "object") { @@ -41,22 +40,6 @@ export const toPurchaseRequest = (purchaseRequest: unknown): IPurchaseRequest => throw new Error("invalid purchaseRequest"); }; -export const getWeaponType = (weaponName: string) => { - const weaponInfo = weapons.find(i => i.uniqueName === weaponName); - - if (!weaponInfo) { - throw new Error(`unknown weapon ${weaponName}`); - } - - const weaponType = weaponInfo.productCategory as WeaponTypeInternal; - - if (!weaponType) { - throw new Error(`unknown weapon category for item ${weaponName}`); - } - - return weaponType; -}; - export const isSlotPurchaseName = (slotPurchaseName: string): slotPurchaseName is SlotPurchaseName => { return slotPurchaseName in slotPurchaseNameToSlotName; }; diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts index 8cf21e808..75b421368 100644 --- a/src/helpers/stringHelpers.ts +++ b/src/helpers/stringHelpers.ts @@ -1,4 +1,4 @@ -export const getJSONfromString = (str: string): any => { +export const getJSONfromString = (str: string) => { const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1); return JSON.parse(jsonSubstring); }; @@ -16,3 +16,11 @@ export const getSubstringFromKeywordToKeyword = (str: string, keywordBegin: stri const endIndex = str.indexOf(keywordEnd); return str.substring(beginIndex, endIndex + 1); }; + +export const getIndexAfter = (str: string, searchWord: string) => { + const index = str.indexOf(searchWord); + if (index === -1) { + return -1; + } + return index + searchWord.length; +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 1e30e4fc4..91386abea 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1,4 +1,4 @@ -import { Model, Schema, Types, model } from "mongoose"; +import { HydratedDocument, Model, Schema, Types, model } from "mongoose"; import { IFlavourItem, IRawUpgrade, @@ -10,7 +10,9 @@ import { ISlots, IGenericItem, IMailbox, - IDuviriInfo + IDuviriInfo, + IPendingRecipe as IPendingRecipeDatabase, + IPendingRecipeResponse } from "../../types/inventoryTypes/inventoryTypes"; import { IMongoDate, IOid } from "../../types/commonTypes"; import { ISuitDatabase } from "@/src/types/inventoryTypes/SuitTypes"; @@ -25,6 +27,29 @@ import { } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { toOid } from "@/src/helpers/inventoryHelpers"; +const pendingRecipeSchema = new Schema( + { + ItemType: String, + CompletionDate: Date + }, + { id: false } +); + +pendingRecipeSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() }; +}); + +pendingRecipeSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject) { + delete returnedObject._id; + delete returnedObject.__v; + (returnedObject as IPendingRecipeResponse).CompletionDate = { + $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } + }; + } +}); + const polaritySchema = new Schema({ Slot: Number, Value: String @@ -296,7 +321,6 @@ DuviriInfoSchema.set("toJSON", { }); const inventorySchema = new Schema({ - accountOwnerId: Schema.Types.ObjectId, SubscribedToEmails: Number, Created: Schema.Types.Mixed, @@ -325,7 +349,6 @@ const inventorySchema = new Schema({ MechBin: slotsBinSchema, CrewMemberBin: slotsBinSchema, - //How many trades do you have left TradesRemaining: Number, //How many Gift do you have left*(gift spends the trade) @@ -351,10 +374,9 @@ const inventorySchema = new Schema({ DailyAffiliationZariman: Number, DailyAffiliationKahl: Number, - //Daily Focus limit DailyFocus: Number, - //you not used Focus + //you not used Focus FocusXP: Schema.Types.Mixed, //Curent active like Active school focuses is = "Zenurik" FocusAbility: String, @@ -441,24 +463,21 @@ const inventorySchema = new Schema({ //Railjack/Components(https://warframe.fandom.com/wiki/Railjack/Components) CrewShipRawSalvage: [Schema.Types.Mixed], - //Default RailJack CrewShips: [Schema.Types.Mixed], CrewShipAmmo: [Schema.Types.Mixed], CrewShipWeapons: [Schema.Types.Mixed], CrewShipWeaponSkins: [Schema.Types.Mixed], - //NPC Crew and weapon CrewMembers: [Schema.Types.Mixed], CrewShipSalvagedWeaponSkins: [Schema.Types.Mixed], CrewShipSalvagedWeapons: [Schema.Types.Mixed], - //Complete Mission\Quests Missions: [Schema.Types.Mixed], QuestKeys: [Schema.Types.Mixed], - //item like DojoKey or Boss missions key + //item like DojoKey or Boss missions key LevelKeys: [Schema.Types.Mixed], //Active quests Quests: [Schema.Types.Mixed], @@ -478,25 +497,22 @@ const inventorySchema = new Schema({ //Retries rank up(3 time) TrainingRetriesLeft: Number, - //you saw last played Region when you opened the star map LastRegionPlayed: String, //Blueprint Recipes: [Schema.Types.Mixed], //Crafting Blueprint(Item Name + CompletionDate) - PendingRecipes: [Schema.Types.Mixed], + PendingRecipes: [pendingRecipeSchema], //warframe\Weapon skins WeaponSkins: [Schema.Types.Mixed], - //Ayatan Item FusionTreasures: [Schema.Types.Mixed], //"node": "TreasureTutorial", "state": "TS_COMPLETED" TauntHistory: [Schema.Types.Mixed], - //noShow2FA,VisitPrimeVault etc WebFlags: Schema.Types.Mixed, //Id CompletedAlerts @@ -508,7 +524,6 @@ const inventorySchema = new Schema({ //Alert->Kuva Siphon PeriodicMissionCompletions: [Schema.Types.Mixed], - //Codex->LoreFragment LoreFragmentScans: [Schema.Types.Mixed], @@ -520,7 +535,7 @@ const inventorySchema = new Schema({ ActiveDojoColorResearch: String, SentientSpawnChanceBoosters: Schema.Types.Mixed, - + QualifyingInvasions: [Schema.Types.Mixed], FactionScores: [Number], @@ -530,11 +545,9 @@ const inventorySchema = new Schema({ //If you want change Spectre Gear id PendingSpectreLoadouts: [Schema.Types.Mixed], - //New quest Email spam //example:"ItemType": "/Lotus/Types/Keys/RailJackBuildQuest/RailjackBuildQuestEmailItem", EmailItems: [Schema.Types.Mixed], - //Profile->Wishlist Wishlist: [String], @@ -561,7 +574,7 @@ const inventorySchema = new Schema({ //Game mission\ivent score example "Tag": "WaterFight", "Best": 170, "Count": 1258, PersonalGoalProgress: [Schema.Types.Mixed], - + //Setting interface Style ThemeStyle: String, ThemeBackground: String, @@ -579,7 +592,6 @@ const inventorySchema = new Schema({ //Night Wave Challenge SeasonChallengeHistory: [Schema.Types.Mixed], - //Cephalon Simaris Entries Example:"TargetType"+"Scans"(1-10)+"Completed": true|false LibraryPersonalProgress: [Schema.Types.Mixed], //Cephalon Simaris Daily Task @@ -587,23 +599,23 @@ const inventorySchema = new Schema({ //https://warframe.fandom.com/wiki/Invasion InvasionChainProgress: [Schema.Types.Mixed], - + //https://warframe.fandom.com/wiki/Parazon DataKnives: [GenericItemSchema], - + //CorpusLich or GrineerLich NemesisAbandonedRewards: [String], - //CorpusLich\KuvaLich + //CorpusLich\KuvaLich NemesisHistory: [Schema.Types.Mixed], LastNemesisAllySpawnTime: Schema.Types.Mixed, - + //TradingRulesConfirmed,ShowFriendInvNotifications(Option->Social) Settings: Schema.Types.Mixed, - //Railjack craft + //Railjack craft //https://warframe.fandom.com/wiki/Rising_Tide PersonalTechProjects: [Schema.Types.Mixed], - + //Modulars lvl and exp(Railjack|Duviri) //https://warframe.fandom.com/wiki/Intrinsics PlayerSkills: Schema.Types.Mixed, @@ -611,7 +623,6 @@ const inventorySchema = new Schema({ //TradeBannedUntil data TradeBannedUntil: Schema.Types.Mixed, - //https://warframe.fandom.com/wiki/Helminth InfestedFoundry: Schema.Types.Mixed, NextRefill: Schema.Types.Mixed, @@ -624,7 +635,6 @@ const inventorySchema = new Schema({ //https://warframe.fandom.com/wiki/Incarnon EvolutionProgress: [Schema.Types.Mixed], - //Unknown and system DuviriInfo: DuviriInfoSchema, Mailbox: MailboxSchema, @@ -650,7 +660,7 @@ const inventorySchema = new Schema({ CollectibleSeries: [Schema.Types.Mixed], HasResetAccount: Boolean, - //Discount Coupon + //Discount Coupon PendingCoupon: Schema.Types.Mixed, //Like BossAladV,BossCaptainVor come for you on missions % chance DeathMarks: [String], @@ -685,13 +695,14 @@ type InventoryDocumentProps = { MiscItems: Types.DocumentArray; Boosters: Types.DocumentArray; OperatorLoadOuts: Types.DocumentArray; - AdultOperatorLoadOuts: Types.DocumentArray; + AdultOperatorLoadOuts: Types.DocumentArray; //TODO: this should still contain _id MechSuits: Types.DocumentArray; Scoops: Types.DocumentArray; DataKnives: Types.DocumentArray; DrifterMelee: Types.DocumentArray; Sentinels: Types.DocumentArray; Horses: Types.DocumentArray; + PendingRecipes: Types.DocumentArray; }; type InventoryModelType = Model; diff --git a/src/routes/api.ts b/src/routes/api.ts index 7ffd3bc54..1c9270bdd 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -35,6 +35,8 @@ import express from "express"; import { setBootLocationController } from "@/src/controllers/api/setBootLocationController"; import { focusController } from "@/src/controllers/api/focusController"; import { inventorySlotsController } from "@/src/controllers/api/inventorySlotsController"; +import { startRecipeController } from "@/src/controllers/api/startRecipeController"; +import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController"; const apiRouter = express.Router(); @@ -62,6 +64,9 @@ apiRouter.get("/logout.php", logoutController); apiRouter.get("/setBootLocation.php", setBootLocationController); // post +// eslint-disable-next-line @typescript-eslint/no-misused-promises +apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); +apiRouter.post("/startRecipe.php", startRecipeController); apiRouter.post("/inventorySlots.php", inventorySlotsController); apiRouter.post("/focus.php", focusController); apiRouter.post("/artifacts.php", artifactsController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index f51063a19..9b74dad60 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -17,6 +17,7 @@ import { import { IGenericUpdate } from "../types/genericUpdate"; import { IArtifactsRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; +import { WeaponTypeInternal } from "@/src/services/itemDataService"; export const createInventory = async (accountOwnerId: Types.ObjectId, loadOutPresetId: Types.ObjectId) => { try { @@ -145,8 +146,6 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string) => return data; }; -export type WeaponTypeInternal = "LongGuns" | "Pistols" | "Melee"; - export const addWeapon = async ( weaponType: WeaponTypeInternal, weaponName: string, diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts new file mode 100644 index 000000000..a251b9ae8 --- /dev/null +++ b/src/services/itemDataService.ts @@ -0,0 +1,117 @@ +import { getIndexAfter } from "@/src/helpers/stringHelpers"; +import { logger } from "@/src/utils/logger"; +import Items, { Buildable, Category, Item, Warframe, Weapon } from "warframe-items"; + +type MinWeapon = Omit; +type MinItem = Omit; + +export const weapons: MinWeapon[] = (new Items({ category: ["Primary", "Secondary", "Melee"] }) as Weapon[]).map( + item => { + const next = { ...item }; + delete next.patchlogs; + return next; + } +); + +export type WeaponTypeInternal = "LongGuns" | "Pistols" | "Melee"; + +export const items: MinItem[] = new Items({ category: ["All"] }).map(item => { + const next = { ...item }; + delete next.patchlogs; + return next; +}); + +export const getWeaponType = (weaponName: string) => { + const weaponInfo = weapons.find(i => i.uniqueName === weaponName); + + if (!weaponInfo) { + throw new Error(`unknown weapon ${weaponName}`); + } + + const weaponType = weaponInfo.productCategory as WeaponTypeInternal; + + if (!weaponType) { + logger.error(`unknown weapon category for item ${weaponName}`); + throw new Error(`unknown weapon category for item ${weaponName}`); + } + + return weaponType; +}; + +const getNamesObj = (category: Category) => + new Items({ category: [category] }).reduce<{ [index: string]: string }>((acc, item) => { + acc[item.name!.replace("'S", "'s")] = item.uniqueName!; + return acc; + }, {}); + +export const modNames = getNamesObj("Mods"); +export const resourceNames = getNamesObj("Resources"); +export const miscNames = getNamesObj("Misc"); +export const relicNames = getNamesObj("Relics"); +export const skinNames = getNamesObj("Skins"); +export const arcaneNames = getNamesObj("Arcanes"); +export const gearNames = getNamesObj("Gear"); +//logger.debug(`gear names`, { gearNames }); + +export const craftNames = Object.fromEntries( + ( + new Items({ + category: [ + "Warframes", + "Gear", + "Melee", + "Primary", + "Secondary", + "Sentinels", + "Misc", + "Arch-Gun", + "Arch-Melee" + ] + }) as Warframe[] + ) + .flatMap(item => item.components || []) + .filter(item => item.drops && item.drops[0]) + .map(item => [item.drops![0].type, item.uniqueName]) +); + +export const blueprintNames = Object.fromEntries( + Object.keys(craftNames) + .filter(name => name.includes("Blueprint")) + .map(name => [name, craftNames[name]]) +); + +const buildables = items.filter(item => !!(item as Buildable).components); + +export const getItemByBlueprint = (uniqueName: string): (MinItem & Buildable) | undefined => { + const item = buildables.find( + item => (item as Buildable).components?.find(component => component.uniqueName === uniqueName) + ); + return item; +}; + +export const getItemCategoryByUniqueName = (uniqueName: string) => { + //Lotus/Types/Items/MiscItems/PolymerBundle + + let splitWord = "Items/"; + if (!uniqueName.includes("/Items/")) { + splitWord = "/Types/"; + } + + const index = getIndexAfter(uniqueName, splitWord); + if (index === -1) { + logger.error(`error parsing item category ${uniqueName}`); + throw new Error(`error parsing item category ${uniqueName}`); + } + const category = uniqueName.substring(index).split("/")[0]; + return category; +}; + +export const getItemByUniqueName = (uniqueName: string) => { + const item = items.find(item => item.uniqueName === uniqueName); + return item; +}; + +export const getItemByName = (name: string) => { + const item = items.find(item => item.name === name); + return item; +}; diff --git a/src/services/missionInventoryUpdateService .ts b/src/services/missionInventoryUpdateService .ts index f815627c9..5936adbe8 100644 --- a/src/services/missionInventoryUpdateService .ts +++ b/src/services/missionInventoryUpdateService .ts @@ -1,7 +1,14 @@ import { IMissionRewardResponse, IReward, IInventoryFieldType, inventoryFields } from "@/src/types/missionTypes"; import missionsDropTable from "@/static/json/missions-drop-table.json"; -import { modNames, relicNames, miscNames, resourceNames, gearNames, blueprintNames } from "@/static/data/items"; +import { + modNames, + relicNames, + miscNames, + resourceNames, + gearNames, + blueprintNames +} from "@/src/services/itemDataService"; import { IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 567239a45..7865ee00d 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -1,4 +1,5 @@ -import { getWeaponType, parseSlotPurchaseName } from "@/src/helpers/purchaseHelpers"; +import { parseSlotPurchaseName } from "@/src/helpers/purchaseHelpers"; +import { getWeaponType } from "@/src/services/itemDataService"; import { getSubstringFromKeyword } from "@/src/helpers/stringHelpers"; import { addBooster, diff --git a/src/services/recipeService.ts b/src/services/recipeService.ts new file mode 100644 index 000000000..8bfdcc930 --- /dev/null +++ b/src/services/recipeService.ts @@ -0,0 +1,74 @@ +import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { getInventory } from "@/src/services/inventoryService"; +import { getItemByBlueprint, getItemCategoryByUniqueName } from "@/src/services/itemDataService"; +import { logger } from "@/src/utils/logger"; +import { Types } from "mongoose"; + +export interface IResource { + uniqueName: string; + count: number; +} + +// export const updateResources = async (accountId: string, components: IResource[]) => { +// const inventory = await getInventory(accountId); + +// for (const component of components) { +// const category = getItemCategoryByUniqueName(component.uniqueName) as keyof typeof inventory; +// //validate category + +// console.log(component.uniqueName); +// console.log("cate", category); + +// const invItem = inventory[category]; +// console.log("invItem", invItem); + +// inventory["MiscItems"]; +// } +// }; + +export const startRecipe = async (recipeName: string, accountId: string) => { + const recipe = getItemByBlueprint(recipeName); + + if (!recipe) { + logger.error(`unknown recipe ${recipeName}`); + throw new Error(`unknown recipe ${recipeName}`); + } + + const componentsNeeded = recipe.components?.map(component => ({ + uniqueName: component.uniqueName, + count: component.itemCount + })); + + if (!componentsNeeded) { + logger.error(`recipe ${recipeName} has no components`); + throw new Error(`recipe ${recipeName} has no components`); + } + + //TODO: consume components used + //await updateResources(accountId, componentsNeeded); + + //might be redundant + if (recipe.consumeOnBuild) { + //consume + } + + if (!recipe.buildTime) { + logger.error(`recipe ${recipeName} has no build time`); + throw new Error(`recipe ${recipeName} has no build time`); + } + //buildtime is in seconds + const completionDate = new Date(Date.now() + recipe.buildTime * unixTimesInMs.second); + + const inventory = await getInventory(accountId); + inventory.PendingRecipes.push({ + ItemType: recipeName, + CompletionDate: completionDate, + _id: new Types.ObjectId() + }); + + const newInventory = await inventory.save(); + + return { + RecipeId: { $oid: newInventory.PendingRecipes[newInventory.PendingRecipes.length - 1]._id?.toString() } + }; +}; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 114a42110..924c373cd 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -14,11 +14,13 @@ import { IOperatorLoadOutSigcol, IWeaponDatabase } from "@/src/types/inventoryTy //Document extends will be deleted soon. TODO: delete and migrate uses to ... export interface IInventoryDatabaseDocument extends IInventoryDatabase, Document {} -export interface IInventoryDatabase extends Omit { +export interface IInventoryDatabase + extends Omit { accountOwnerId: Types.ObjectId; TrainingDate: Date; // TrainingDate changed from IMongoDate to Date LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population Mailbox: Types.ObjectId; // Mailbox changed from IMailbox to Types.ObjectId + PendingRecipes: IPendingRecipe[]; } export interface IInventoryResponseDocument extends IInventoryResponse, Document {} @@ -41,6 +43,11 @@ export interface IMailbox { LastInboxId: IOid; } +//TODO: perhaps split response and database into their own files + +export interface IPendingRecipeResponse extends Omit { + CompletionDate: IMongoDate; +} export interface IInventoryResponse { Horses: IGenericItem[]; DrifterMelee: IGenericItem[]; @@ -96,7 +103,7 @@ export interface IInventoryResponse { XPInfo: IEmailItem[]; Recipes: IConsumable[]; WeaponSkins: IWeaponSkin[]; - PendingRecipes: IPendingRecipe[]; + PendingRecipes: IPendingRecipeResponse[]; TrainingDate: IMongoDate; PlayerLevel: number; Upgrades: ICrewShipSalvagedWeaponSkin[]; @@ -816,7 +823,7 @@ export interface IPendingCoupon { export interface IPendingRecipe { ItemType: string; - CompletionDate: IMongoDate; + CompletionDate: Date; ItemId: IOid; } diff --git a/static/data/items.ts b/static/data/items.ts index f2718a2fe..e69de29bb 100644 --- a/static/data/items.ts +++ b/static/data/items.ts @@ -1,60 +0,0 @@ -import Items, { Category, Item, Warframe, Weapon } from "warframe-items"; - -type MinWeapon = Omit; -type MinItem = Omit; - -export const weapons: MinWeapon[] = (new Items({ category: ["Primary", "Secondary", "Melee"] }) as Weapon[]).map( - item => { - const next = { ...item }; - delete next.patchlogs; - return next; - } -); - -export const items: MinItem[] = new Items({ category: ["All"] }).map(item => { - const next = { ...item }; - delete next.patchlogs; - return next; -}); - -const getNamesObj = (category: Category) => - new Items({ category: [category] }).reduce((acc, item) => { - acc[item.name!.replace("'S", "'s")] = item.uniqueName!; - return acc; - }, {} as ImportAssertions); - -export const modNames = getNamesObj("Mods"); -export const resourceNames = getNamesObj("Resources"); -export const miscNames = getNamesObj("Misc"); -export const relicNames = getNamesObj("Relics"); -export const skinNames = getNamesObj("Skins"); -export const arcaneNames = getNamesObj("Arcanes"); -export const gearNames = getNamesObj("Gear"); - -export const craftNames: ImportAssertions = Object.fromEntries( - ( - new Items({ - category: [ - "Warframes", - "Gear", - "Melee", - "Primary", - "Secondary", - "Sentinels", - "Misc", - "Arch-Gun", - "Arch-Melee" - ] - }) as Warframe[] - ) - .flatMap(item => item.components || []) - .filter(item => item.drops && item.drops[0]) - .map(item => [item.drops![0].type, item.uniqueName]) -); -craftNames["Forma Blueprint"] = "/Lotus/Types/Recipes/Components/FormaBlueprint"; - -export const blueprintNames: ImportAssertions = Object.fromEntries( - Object.keys(craftNames) - .filter(name => name.includes("Blueprint")) - .map(name => [name, craftNames[name]]) -);