From aca258e32c77046b0f9a78a66630f864909548d5 Mon Sep 17 00:00:00 2001 From: Ordis <134585663+OrdisPrime@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:47:16 +0100 Subject: [PATCH] quests 1 --- .../giveKeyChainTriggeredItemsController.ts | 57 +++ src/controllers/api/inventoryController.ts | 2 + .../api/missionInventoryUpdateController.ts | 83 ++-- .../api/unlockShipFeatureController.ts | 13 + src/controllers/api/updateQuestController.ts | 41 ++ src/models/inventoryModels/inventoryModel.ts | 12 +- src/models/personalRoomsModel.ts | 4 +- src/routes/api.ts | 6 + src/services/inventoryService.ts | 200 ++++---- src/services/itemDataService.ts | 61 ++- src/services/missionInventoryUpdateService.ts | 454 +++++++++++------- src/services/personalRoomsService.ts | 10 + src/services/questService.ts | 35 ++ src/types/inventoryTypes/inventoryTypes.ts | 15 +- src/types/missionTypes.ts | 10 +- src/types/requestTypes.ts | 60 ++- .../questCompletionRewards.json | 13 + 17 files changed, 748 insertions(+), 328 deletions(-) create mode 100644 src/controllers/api/giveKeyChainTriggeredItemsController.ts create mode 100644 src/controllers/api/unlockShipFeatureController.ts create mode 100644 src/controllers/api/updateQuestController.ts create mode 100644 src/services/questService.ts create mode 100644 static/fixed_responses/questCompletionRewards.json diff --git a/src/controllers/api/giveKeyChainTriggeredItemsController.ts b/src/controllers/api/giveKeyChainTriggeredItemsController.ts new file mode 100644 index 00000000..827bb79c --- /dev/null +++ b/src/controllers/api/giveKeyChainTriggeredItemsController.ts @@ -0,0 +1,57 @@ +import { RequestHandler } from "express"; +import { isEmptyObject, parseString } from "@/src/helpers/general"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addKeyChainItems, getInventory } from "@/src/services/inventoryService"; + +export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => { + const accountId = parseString(req.query.accountId); + const keyChainTriggeredItemsRequest = getJSONfromString( + (req.body as string).toString() + ) as IGiveKeyChainTriggeredItemsRequest; + + const inventory = await getInventory(accountId); + const inventoryChanges = await addKeyChainItems(inventory, keyChainTriggeredItemsRequest); + + if (isEmptyObject(inventoryChanges)) { + throw new Error("inventory changes was empty after getting keychain items: should not happen"); + } + // items were added: update quest stage's i (item was given) + const quest = inventory.QuestKeys.find(quest => quest.ItemType === keyChainTriggeredItemsRequest.KeyChain); + console.log(`quest: ${JSON.stringify(quest)}`); + + if (!quest) { + throw new Error(`Quest ${keyChainTriggeredItemsRequest.KeyChain} not found in QuestKeys`); + } + if (!quest.Progress) { + throw new Error(`Progress should always exist when giving keychain triggered items`); + } + + const questStage = quest.Progress[keyChainTriggeredItemsRequest.ChainStage]; + if (questStage) { + questStage.i = true; + } else { + const questStageIndex = quest.Progress.push({ i: true }) - 1; + if (questStageIndex !== keyChainTriggeredItemsRequest.ChainStage) { + throw new Error( + `Quest stage index mismatch: ${questStageIndex} !== ${keyChainTriggeredItemsRequest.ChainStage}` + ); + } + } + + await inventory.save(); + res.send(inventoryChanges); + + //TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory + /* + some items are added or removed (not sure) to the wishlist, in that case a + WishlistChanges: ["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"], + is added to the response, need to determine for which items this is the case and what purpose this has. + */ + //{"KeyChain":"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain","ChainStage":0} + //{"WishlistChanges":["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],"MiscItems":[{"ItemType":"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem","ItemCount":1}]} +}; + +export interface IGiveKeyChainTriggeredItemsRequest { + KeyChain: string; + ChainStage: number; +} diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index b4e0035d..5eb55435 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -247,6 +247,8 @@ export const inventoryController: RequestHandler = async (request, response) => // This determines if the "void fissures" tab is shown in navigation. inventoryResponse.HasOwnedVoidProjectionsPreviously = true; + inventoryResponse.QuestKeys[0].Completed = true; + response.json(inventoryResponse); }; diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 8e801efe..b02f62ca 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -1,10 +1,13 @@ import { RequestHandler } from "express"; -import { missionInventoryUpdate } from "@/src/services/inventoryService"; -import { combineRewardAndLootInventory, getRewards } from "@/src/services/missionInventoryUpdateService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; -import { logger } from "@/src/utils/logger"; +import { + addMissionInventoryUpdates, + addMissionRewards, + calculateFinalCredits +} from "@/src/services/missionInventoryUpdateService"; +import { getInventory } from "@/src/services/inventoryService"; /* **** INPUT **** - [ ] crossPlaySetting @@ -30,13 +33,13 @@ import { logger } from "@/src/utils/logger"; - [ ] hosts - [x] ChallengeProgress - [ ] SeasonChallengeHistory -- [ ] PS (Passive anti-cheat data which includes your username, module list, process list, and system name.) +- [ ] PS (anticheat data) - [ ] ActiveDojoColorResearch - [x] RewardInfo - [ ] ReceivedCeremonyMsg - [ ] LastCeremonyResetDate - [ ] MissionPTS (Used to validate the mission/alive time above.) -- [ ] RepHash (A hash from the replication manager/RepMgr Unknown what it does.) +- [ ] RepHash - [ ] EndOfMatchUpload - [ ] ObjectiveReached - [ ] FpsAvg @@ -45,34 +48,54 @@ import { logger } from "@/src/utils/logger"; - [ ] FpsSamples */ -const missionInventoryUpdateController: RequestHandler = async (req, res): Promise => { +// eslint-disable-next-line @typescript-eslint/no-misused-promises +export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise => { const accountId = await getAccountIdForRequest(req); - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call - const lootInventory = getJSONfromString(req.body.toString()) as IMissionInventoryUpdateRequest; + const missionReport = getJSONfromString((req.body as string).toString()) as IMissionInventoryUpdateRequest; - logger.debug("missionInventoryUpdate with lootInventory =", lootInventory); - - const { InventoryChanges, MissionRewards } = getRewards(lootInventory); - - const { combinedInventoryChanges, TotalCredits, CreditsBonus, MissionCredits, FusionPoints } = - combineRewardAndLootInventory(InventoryChanges, lootInventory); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const InventoryJson = JSON.stringify(await missionInventoryUpdate(combinedInventoryChanges, accountId)); - res.json({ - // InventoryJson, // this part will reset game data and missions will be locked - MissionRewards, - InventoryChanges, - TotalCredits, - CreditsBonus, - MissionCredits, - FusionPoints - }); - } catch (err) { - console.error("Error parsing JSON data:", err); + if (missionReport.MissionStatus !== "GS_SUCCESS") { + console.log(`Mission failed: ${missionReport.RewardInfo?.node}`); + //todo: return expected response for failed mission + //duvirisadjob does not provide missionStatus } + + const inventory = await getInventory(accountId); + + const missionRewardsResults = await addMissionRewards(inventory, missionReport); + + if (!missionRewardsResults) { + console.error("Failed to add mission rewards"); + res.status(500).json({ error: "Failed to add mission rewards" }); + return; + } + + const { MissionRewards, inventoryChanges, missionCompletionCredits } = missionRewardsResults; + + const inventoryUpdates = addMissionInventoryUpdates(inventory, missionReport); + + //todo ? can go after not awaiting + //creditBonus is not correct for mirage mission 3 + const credits = calculateFinalCredits(inventory, { + missionCompletionCredits, + missionDropCredits: missionReport.RegularCredits ?? 0, + rngRewardCredits: inventoryChanges.RegularCredits as number + }); + + const InventoryJson = JSON.stringify((await inventory.save()).toJSON()); + inventoryChanges.WishlistChanges = [ + "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem" + ] as unknown as object[]; + + //TODO: figure out when to send inventory. it is needed for many cases. + res.json({ + InventoryJson, + InventoryChanges: inventoryChanges, + MissionRewards, + ...credits, + ...inventoryUpdates, + FusionPoints: inventoryChanges.FusionPoints + }); }; /* @@ -85,5 +108,3 @@ const missionInventoryUpdateController: RequestHandler = async (req, res): Promi - [x] InventoryChanges - [x] FusionPoints */ - -export { missionInventoryUpdateController }; diff --git a/src/controllers/api/unlockShipFeatureController.ts b/src/controllers/api/unlockShipFeatureController.ts new file mode 100644 index 00000000..d5ca6d74 --- /dev/null +++ b/src/controllers/api/unlockShipFeatureController.ts @@ -0,0 +1,13 @@ +import { RequestHandler } from "express"; +import { updateShipFeature } from "@/src/services/personalRoomsService"; +import { IUnlockShipFeatureRequest } from "@/src/types/requestTypes"; +import { parseString } from "@/src/helpers/general"; + +// eslint-disable-next-line @typescript-eslint/no-misused-promises +export const unlockShipFeatureController: RequestHandler = async (req, res) => { + const accountId = parseString(req.query.accountId); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call + const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest; + await updateShipFeature(accountId, shipFeatureRequest.Feature); + res.send([]); +}; diff --git a/src/controllers/api/updateQuestController.ts b/src/controllers/api/updateQuestController.ts new file mode 100644 index 00000000..042e1d23 --- /dev/null +++ b/src/controllers/api/updateQuestController.ts @@ -0,0 +1,41 @@ +import { RequestHandler } from "express"; +import { parseString } from "@/src/helpers/general"; +import { logger } from "@/src/utils/logger"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { updateQuestKey, IUpdateQuestRequest } from "@/src/services/questService"; +import { getQuestCompletionItems } from "@/src/services/itemDataService"; +import { addItem, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; + +// eslint-disable-next-line @typescript-eslint/no-misused-promises +export const updateQuestController: RequestHandler = async (req, res) => { + const accountId = parseString(req.query.accountId); + const updateQuestRequest = getJSONfromString((req.body as string).toString()) as IUpdateQuestRequest; + + // updates should be made only to one quest key per request + if (updateQuestRequest.QuestKeys.length > 1) { + throw new Error(`quest keys array should only have 1 item, but has ${updateQuestRequest.QuestKeys.length}`); + } + + const inventory = await getInventory(accountId); + + updateQuestKey(inventory, updateQuestRequest.QuestKeys); + + if (updateQuestRequest.QuestKeys[0].Completed) { + logger.debug(`completed quest ${updateQuestRequest.QuestKeys[0].ItemType} `); + const questKeyName = updateQuestRequest.QuestKeys[0].ItemType; + const questCompletionItems = getQuestCompletionItems(questKeyName); + + console.log(questCompletionItems, "quest completion items"); + + const inventoryChanges = {}; + for (const item of questCompletionItems) { + console.log(item, "item"); + const inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount); + combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges); + } + res.json({ MissionRewards: [], inventoryChanges }); + return; + } + + res.send({ MissionRewards: [] }); +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 276e2321..eead6914 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -26,9 +26,9 @@ import { IInfestedFoundryDatabase, IHelminthResource, IConsumedSuit, - IQuestProgress, + IQuestStage, IQuestKeyDatabase, - IQuestKeyResponse, + IQuestKeyClient, IFusionTreasure, ISpectreLoadout, IWeaponSkinDatabase, @@ -518,7 +518,7 @@ infestedFoundrySchema.set("toJSON", { } }); -const questProgressSchema = new Schema({ +const questProgressSchema = new Schema({ c: Number, i: Boolean, m: Boolean, @@ -527,7 +527,7 @@ const questProgressSchema = new Schema({ const questKeysSchema = new Schema( { - Progress: [questProgressSchema], + Progress: { type: [questProgressSchema], default: undefined }, unlock: Boolean, Completed: Boolean, //CustomData: Schema.Types.Mixed, @@ -544,7 +544,7 @@ questKeysSchema.set("toJSON", { const questKeysDatabase = ret as IQuestKeyDatabase; if (questKeysDatabase.CompletionDate) { - (questKeysDatabase as IQuestKeyResponse).CompletionDate = toMongoDate(questKeysDatabase.CompletionDate); + (questKeysDatabase as IQuestKeyClient).CompletionDate = toMongoDate(questKeysDatabase.CompletionDate); } } }); @@ -1164,7 +1164,7 @@ inventorySchema.set("toJSON", { }); // type overwrites for subdocuments/subdocument arrays -type InventoryDocumentProps = { +export type InventoryDocumentProps = { Suits: Types.DocumentArray; LongGuns: Types.DocumentArray; Pistols: Types.DocumentArray; diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index 448cd52f..76c9e315 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -70,7 +70,7 @@ const apartmentSchema = new Schema( { Rooms: [roomSchema], FavouriteLoadouts: [Schema.Types.Mixed], - Gardening: gardeningSchema + Gardening: gardeningSchema // TODO: ensure this is correct }, { _id: false } ); @@ -88,7 +88,7 @@ const apartmentDefault: IApartment = { const orbiterSchema = new Schema( { - Features: [String], + Features: { type: [String], default: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"] }, Rooms: [roomSchema], ContentUrlSignature: { type: String, required: false }, BootLocation: String diff --git a/src/routes/api.ts b/src/routes/api.ts index a164b455..a81ec037 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -78,6 +78,9 @@ import { updateChallengeProgressController } from "@/src/controllers/api/updateC import { updateSessionGetController, updateSessionPostController } from "@/src/controllers/api/updateSessionController"; import { updateThemeController } from "../controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; +import { updateQuestController } from "@/src/controllers/api/updateQuestController"; +import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; +import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController"; const apiRouter = express.Router(); @@ -132,6 +135,7 @@ apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/getAlliance.php", getAllianceController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); apiRouter.post("/gildWeapon.php", gildWeaponController); +apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); apiRouter.post("/guildTech.php", guildTechController); apiRouter.post("/hostSession.php", hostSessionController); apiRouter.post("/infestedFoundry.php", infestedFoundryController); @@ -161,10 +165,12 @@ apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController); apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController); apiRouter.post("/tauntHistory.php", tauntHistoryController); apiRouter.post("/trainingResult.php", trainingResultController); +apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController); apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController); apiRouter.post("/updateNodeIntros.php", genericUpdateController); apiRouter.post("/updateSession.php", updateSessionPostController); apiRouter.post("/updateTheme.php", updateThemeController); +apiRouter.post("/updateQuest.php", updateQuestController); apiRouter.post("/upgrades.php", upgradesController); export { apiRouter }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 9b436c14..6ccbca2a 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1,6 +1,10 @@ -import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { + Inventory, + InventoryDocumentProps, + TInventoryDatabaseDocument +} from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; -import { Types } from "mongoose"; +import { HydratedDocument, Types } from "mongoose"; import { SlotNames, IInventoryChanges, IBinChanges, ICurrencyChanges } from "@/src/types/purchaseTypes"; import { IChallengeProgress, @@ -16,7 +20,8 @@ import { TEquipmentKey, equipmentKeys, IFusionTreasure, - IDailyAffiliations + IDailyAffiliations, + IInventoryDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate } from "../types/genericUpdate"; import { @@ -25,7 +30,7 @@ import { IUpdateChallengeProgressRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { getWeaponType, getExalted } from "@/src/services/itemDataService"; +import { getWeaponType, getExalted, getKeyChainItems } from "@/src/services/itemDataService"; import { IEquipmentClient, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; import { ExportArcanes, @@ -39,6 +44,8 @@ import { TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; +import { creditBundles, fusionBundles } from "@/src/services/missionInventoryUpdateService"; +import { IGiveKeyChainTriggeredItemsRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -109,7 +116,6 @@ export const createInventory = async ( }); inventory.QuestKeys.push({ - Completed: true, ItemType: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" }); @@ -132,13 +138,19 @@ export const createInventory = async ( } }; +/** + * Combines two inventory changes objects into one. + * + * @param InventoryChanges - will hold the combined changes + * @param delta - inventory changes to be added + */ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => { for (const key in delta) { if (!(key in InventoryChanges)) { InventoryChanges[key] = delta[key]; } else if (Array.isArray(delta[key])) { const left = InventoryChanges[key] as object[]; - const right: object[] = delta[key]; + const right: object[] | string[] = delta[key]; for (const item of right) { left.push(item); } @@ -154,8 +166,10 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del left.Extra ??= 0; left.Extra += right.Extra; } + } else if (typeof delta[key] === "number") { + (InventoryChanges[key] as number) += delta[key]; } else { - logger.warn(`inventory change not merged: ${key}`); + throw new Error(`inventory change not merged: unhandled type for inventory key ${key}`); } } }; @@ -274,6 +288,24 @@ export const addItem = async ( } }; } + if (typeName in creditBundles) { + const creditsTotal = creditBundles[typeName] * quantity; + inventory.RegularCredits += creditsTotal; + return { + InventoryChanges: { + RegularCredits: creditsTotal + } + }; + } + if (typeName in fusionBundles) { + const fusionPointsTotal = fusionBundles[typeName] * quantity; + inventory.FusionPoints += fusionPointsTotal; + return { + InventoryChanges: { + FusionPoints: fusionPointsTotal + } + }; + } // Path-based duck typing switch (typeName.substr(1).split("/")[1]) { @@ -364,7 +396,7 @@ export const addItem = async ( } } } - case "Game": + case "Game": { if (typeName.substr(1).split("/")[3] == "Projections") { // Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze const miscItemChanges = [ @@ -381,6 +413,27 @@ export const addItem = async ( }; } break; + } + case "Keys": { + inventory.QuestKeys.push({ ItemType: typeName }); + return { + InventoryChanges: { + QuestKeys: [ + { + ItemType: typeName + } + ] + } + }; + } + case "NeutralCreatures": { + const horseIndex = inventory.Horses.push({ ItemType: typeName }); + return { + InventoryChanges: { + Horses: inventory.Horses[horseIndex - 1].toJSON() + } + }; + } } break; } @@ -684,7 +737,8 @@ const addCrewShip = ( return inventoryChanges; }; -const addGearExpByCategory = ( +//TODO: wrong id is not erroring +export const addGearExpByCategory = ( inventory: TInventoryDatabaseDocument, gearArray: IEquipmentClient[] | undefined, categoryName: TEquipmentKey @@ -870,7 +924,7 @@ export const addChallenges = ( }); }; -const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes }: IMission): void => { +export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes }: IMission): void => { const { Missions } = inventory; const itemIndex = Missions.findIndex(item => item.Tag === Tag); @@ -882,83 +936,6 @@ const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Comple } }; -export const missionInventoryUpdate = async (data: IMissionInventoryUpdateRequest, accountId: string) => { - const { - RawUpgrades, - MiscItems, - RegularCredits, - ChallengeProgress, - FusionPoints, - Consumables, - Recipes, - Missions, - FusionTreasures - } = data; - const inventory = await getInventory(accountId); - - // credits - inventory.RegularCredits += RegularCredits || 0; - - // endo - inventory.FusionPoints += FusionPoints || 0; - - // syndicate - data.AffiliationChanges?.forEach(affiliation => { - const syndicate = inventory.Affiliations.find(x => x.Tag == affiliation.Tag); - if (syndicate !== undefined) { - syndicate.Standing = - syndicate.Standing === undefined ? affiliation.Standing : syndicate.Standing + affiliation.Standing; - syndicate.Title = syndicate.Title === undefined ? affiliation.Title : syndicate.Title + affiliation.Title; - } else { - inventory.Affiliations.push({ - Standing: affiliation.Standing, - Title: affiliation.Title, - Tag: affiliation.Tag, - FreeFavorsEarned: [], - FreeFavorsUsed: [] - }); - } - }); - - // Gear XP - equipmentKeys.forEach(key => addGearExpByCategory(inventory, data[key], key)); - - // Incarnon Challenges - if (data.EvolutionProgress) { - for (const evoProgress of data.EvolutionProgress) { - const entry = inventory.EvolutionProgress - ? inventory.EvolutionProgress.find(entry => entry.ItemType == evoProgress.ItemType) - : undefined; - if (entry) { - entry.Progress = evoProgress.Progress; - entry.Rank = evoProgress.Rank; - } else { - inventory.EvolutionProgress ??= []; - inventory.EvolutionProgress.push(evoProgress); - } - } - } - - // LastRegionPlayed - if (data.LastRegionPlayed) { - inventory.LastRegionPlayed = data.LastRegionPlayed; - } - - // other - addMods(inventory, RawUpgrades); - addMiscItems(inventory, MiscItems); - addConsumables(inventory, Consumables); - addRecipes(inventory, Recipes); - addChallenges(inventory, ChallengeProgress); - addFusionTreasures(inventory, FusionTreasures); - if (Missions) { - addMissionComplete(inventory, Missions); - } - - const changedInventory = await inventory.save(); - return changedInventory.toJSON(); -}; - export const addBooster = async (ItemType: string, time: number, accountId: string): Promise => { const currentTime = Math.floor(Date.now() / 1000) - 129600; // Value is wrong without 129600. Figure out why, please. :) @@ -977,3 +954,52 @@ export const addBooster = async (ItemType: string, time: number, accountId: stri await inventory.save(); }; + +export const updateSyndicate = ( + inventory: HydratedDocument, + syndicateUpdate: IMissionInventoryUpdateRequest["AffiliationChanges"] +) => { + syndicateUpdate?.forEach(affiliation => { + const syndicate = inventory.Affiliations.find(x => x.Tag == affiliation.Tag); + if (syndicate !== undefined) { + syndicate.Standing = + syndicate.Standing === undefined ? affiliation.Standing : syndicate.Standing + affiliation.Standing; + syndicate.Title = syndicate.Title === undefined ? affiliation.Title : syndicate.Title + affiliation.Title; + } else { + inventory.Affiliations.push({ + Standing: affiliation.Standing, + Title: affiliation.Title, + Tag: affiliation.Tag, + FreeFavorsEarned: [], + FreeFavorsUsed: [] + }); + } + }); + return { AffiliationMods: [] }; +}; + +/** + * @returns object with inventory keys of changes or empty object when no items were added + */ +export const addKeyChainItems = async ( + inventory: TInventoryDatabaseDocument, + keyChainData: IGiveKeyChainTriggeredItemsRequest +): Promise => { + const keyChainItems = getKeyChainItems(keyChainData); + + logger.debug( + `adding key chain items ${keyChainItems.join()} for ${keyChainData.KeyChain} at stage ${keyChainData.ChainStage}` + ); + + const nonStoreItems = keyChainItems.map(item => item.replace("StoreItems/", "")); + + //TODO: inventoryChanges is not typed correctly + const inventoryChanges = {}; + + for (const item of nonStoreItems) { + const inventoryChangesDelta = await addItem(inventory, item); + combineInventoryChanges(inventoryChanges, inventoryChangesDelta.InventoryChanges); + } + + return inventoryChanges; +}; diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index b9ee0bb7..a9bddd1d 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -1,4 +1,7 @@ +import { IGiveKeyChainTriggeredItemsRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { getIndexAfter } from "@/src/helpers/stringHelpers"; +import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; import { dict_de, dict_en, @@ -20,13 +23,16 @@ import { ExportGear, ExportKeys, ExportRecipes, + ExportRegions, ExportResources, ExportSentinels, ExportWarframes, ExportWeapons, IPowersuit, - IRecipe + IRecipe, + IRegion } from "warframe-public-export-plus"; +import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json"; export type WeaponTypeInternal = | "LongGuns" @@ -150,3 +156,56 @@ export const getDict = (lang: string): Record => { export const getString = (key: string, dict: Record): string => { return dict[key] ?? key; }; + +export const getKeyChainItems = ({ KeyChain, ChainStage }: IGiveKeyChainTriggeredItemsRequest): string[] => { + const chainStages = ExportKeys[KeyChain].chainStages; + if (!chainStages) { + throw new Error(`KeyChain ${KeyChain} does not contain chain stages`); + } + + const keyChainStage = chainStages[ChainStage]; + if (!keyChainStage) { + throw new Error(`KeyChainStage ${ChainStage} not found`); + } + + if (keyChainStage.itemsToGiveWhenTriggered.length === 0) { + throw new Error(`No items to give for KeyChain ${KeyChain} at stage ${ChainStage}`); + } + + return keyChainStage.itemsToGiveWhenTriggered; +}; + +export const getLevelKeyRewards = (levelKey: string) => { + const levelKeyData = ExportKeys[levelKey]; + if (!levelKeyData) { + const error = `LevelKey ${levelKey} not found`; + logger.error(error); + throw new Error(error); + } + + if (!levelKeyData.rewards) { + const error = `LevelKey ${levelKey} does not contain rewards`; + logger.error(error); + throw new Error(error); + } + + return levelKeyData.rewards; +}; + +export const getNode = (nodeName: string): IRegion => { + const node = ExportRegions[nodeName]; + if (!node) { + throw new Error(`Node ${nodeName} not found`); + } + + return node; +}; + +export const getQuestCompletionItems = (questKey: string) => { + const items = (questCompletionItems as unknown as Record)[questKey]; + if (!items) { + throw new Error(`Quest ${questKey} not found in questCompletionItems`); + } + + return items; +}; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index ad633117..94eb134f 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1,30 +1,289 @@ -import { IMissionRewardResponse, IInventoryFieldType, inventoryFields } from "@/src/types/missionTypes"; - -import { - ExportRegions, - ExportRewards, - ExportUpgrades, - ExportGear, - ExportRecipes, - ExportRelics, - ExportResources, - IReward -} from "warframe-public-export-plus"; -import { IMissionInventoryUpdateRequest } from "../types/requestTypes"; +import { ExportRegions, ExportRewards, IReward } from "warframe-public-export-plus"; +import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { IRngResult, getRandomReward } from "@/src/services/rngService"; +import { IInventoryDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; +import { + addChallenges, + addConsumables, + addFusionTreasures, + addGearExpByCategory, + addItem, + addMiscItems, + addMissionComplete, + addMods, + addRecipes, + combineInventoryChanges, + updateSyndicate +} from "@/src/services/inventoryService"; +import { updateQuestKey } from "@/src/services/questService"; +import { HydratedDocument } from "mongoose"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { getLevelKeyRewards, getNode } from "@/src/services/itemDataService"; +import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; -// need reverse engineer rewardSeed, otherwise ingame displayed rotation reward will be different than added to db or displayed on mission end -const getRewards = ({ - RewardInfo -}: IMissionInventoryUpdateRequest): { - InventoryChanges: IMissionInventoryUpdateRequest; - MissionRewards: IMissionRewardResponse[]; -} => { - if (!RewardInfo) { - return { InventoryChanges: {}, MissionRewards: [] }; +const getRotations = (rotationCount: number): number[] => { + if (rotationCount === 0) return [0]; + + const rotationPattern = [0, 0, 1, 2]; // A, A, B, C + const rotatedValues = []; + + for (let i = 0; i < rotationCount; i++) { + rotatedValues.push(rotationPattern[i % rotationPattern.length]); } + return rotatedValues; +}; + +const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { + return getRandomReward(pool as IRngResult[]); +}; + +export const creditBundles: Record = { + "/Lotus/Types/PickUps/Credits/1500Credits": 1500, + "/Lotus/Types/PickUps/Credits/2000Credits": 2000, + "/Lotus/Types/PickUps/Credits/2500Credits": 2500, + "/Lotus/Types/PickUps/Credits/3000Credits": 3000, + "/Lotus/Types/PickUps/Credits/4000Credits": 4000, + "/Lotus/Types/PickUps/Credits/5000Credits": 5000, + "/Lotus/Types/PickUps/Credits/7500Credits": 7500, + "/Lotus/Types/PickUps/Credits/10000Credits": 10000, + "/Lotus/Types/PickUps/Credits/5000Hollars": 5000, + "/Lotus/Types/PickUps/Credits/7500Hollars": 7500, + "/Lotus/Types/PickUps/Credits/10000Hollars": 10000, + "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardOneHard": 105000, + "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardTwoHard": 175000, + "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardThreeHard": 250000, + "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsCommon": 15000, + "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000 +}; + +export const fusionBundles: Record = { + "/Lotus/Upgrades/Mods/FusionBundles/CommonFusionBundle": 15, + "/Lotus/Upgrades/Mods/FusionBundles/UncommonFusionBundle": 50, + "/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle": 80 +}; + +type Entries = (K extends unknown ? [K, T[K]] : never)[]; + +function getEntriesUnsafe(object: T): Entries { + return Object.entries(object) as Entries; +} + +type TMissionInventoryUpdateKeys = keyof IMissionInventoryUpdateRequest; +const ignoredInventoryUpdateKeys = ["FpsAvg", "FpsMax", "FpsMin", "FpsSamples"] satisfies TMissionInventoryUpdateKeys[]; // for keys with no meaning for this server +type TignoredInventoryUpdateKeys = (typeof ignoredInventoryUpdateKeys)[number]; +const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys + +export const addMissionInventoryUpdates = ( + inventory: HydratedDocument, + inventoryUpdates: IMissionInventoryUpdateRequest +) => { + //TODO: type this properly + const inventoryChanges: Partial = {}; + if (inventoryUpdates.MissionFailed === true) { + return; + } + for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) { + if (value === undefined) { + logger.error(`Inventory update key ${key} has no value `); + continue; + } + switch (key) { + case "RegularCredits": + inventory.RegularCredits += value; + break; + case "QuestKeys": + updateQuestKey(inventory, value); + break; + case "AffiliationChanges": + updateSyndicate(inventory, value); + break; + // Incarnon Challenges + case "EvolutionProgress": { + for (const evoProgress of value) { + const entry = inventory.EvolutionProgress + ? inventory.EvolutionProgress.find(entry => entry.ItemType == evoProgress.ItemType) + : undefined; + if (entry) { + entry.Progress = evoProgress.Progress; + entry.Rank = evoProgress.Rank; + } else { + inventory.EvolutionProgress ??= []; + inventory.EvolutionProgress.push(evoProgress); + } + } + break; + } + case "Missions": + addMissionComplete(inventory, value); + break; + case "LastRegionPlayed": + inventory.LastRegionPlayed = value; + break; + case "RawUpgrades": + addMods(inventory, value); + break; + case "MiscItems": + addMiscItems(inventory, value); + break; + case "Consumables": + addConsumables(inventory, value); + break; + case "Recipes": + addRecipes(inventory, value); + break; + case "ChallengeProgress": + addChallenges(inventory, value); + break; + case "FusionTreasures": + addFusionTreasures(inventory, value); + break; + case "FusionBundles": { + let fusionPoints = 0; + for (const fusionBundle of value) { + const fusionPointsTotal = fusionBundles[fusionBundle.ItemType] * fusionBundle.ItemCount; + inventory.FusionPoints += fusionPointsTotal; + fusionPoints += fusionPointsTotal; + } + inventoryChanges.FusionPoints = fusionPoints; + break; + } + // Equipment XP updates + case "Suits": + case "LongGuns": + case "Pistols": + case "Melee": + case "SpecialItems": + case "Sentinels": + case "SentinelWeapons": + case "SpaceSuits": + case "SpaceGuns": + case "SpaceMelee": + case "Hoverboards": + case "OperatorAmps": + case "MoaPets": + addGearExpByCategory(inventory, value, key); + break; + default: + // if ( + // (ignoredInventoryUpdateKeys as readonly string[]).includes(key) || + // knownUnhandledKeys.includes(key) + // ) { + // continue; + // } + // logger.error(`Unhandled inventory update key: ${key}`); + } + } + + return inventoryChanges; +}; + +//return partial missioninventoryupdate response +export const addMissionRewards = async ( + inventory: TInventoryDatabaseDocument, + { RewardInfo: rewardInfo, LevelKeyName: levelKeyName, Missions: missions }: IMissionInventoryUpdateRequest +) => { + if (!rewardInfo) { + logger.error("no reward info provided"); + return; + } + + //TODO: check double reward merging + const MissionRewards = getRandomMissionDrops(rewardInfo).map(drop => { + return { StoreItem: drop.type, ItemCount: drop.itemCount }; + }); + console.log("random mission drops:", MissionRewards); + const inventoryChanges: IInventoryChanges = {}; + + let missionCompletionCredits = 0; + //inventory change is what the client does not know the server updated, although credit updates seem to be taken from totalCredits + if (levelKeyName) { + const fixedLevelRewards = getLevelKeyRewards(levelKeyName); + console.log("fixedLevelRewards", fixedLevelRewards); + for (const reward of fixedLevelRewards) { + if (reward.rewardType == "RT_CREDITS") { + inventory.RegularCredits += reward.amount; + missionCompletionCredits += reward.amount; + continue; + } + MissionRewards.push({ StoreItem: reward.itemType, ItemCount: 1 }); + } + } + + if (missions) { + const levelCreditReward = getLevelCreditRewards(missions?.Tag); + missionCompletionCredits += levelCreditReward; + inventory.RegularCredits += levelCreditReward; + console.log("levelCreditReward", levelCreditReward); + } + + //TODO: resolve issue with creditbundles + for (const reward of MissionRewards) { + //TODO: additem should take in storeItems + console.log("adding item", reward.StoreItem); + const inventoryChange = await addItem(inventory, reward.StoreItem.replace("StoreItems/", ""), reward.ItemCount); + //TODO: combineInventoryChanges improve type safety, merging 2 of the same item? + //TODO: check for the case when two of the same item are added, combineInventoryChanges should merge them + //TODO: some conditional types to rule out binchanges? + combineInventoryChanges(inventoryChanges, inventoryChange.InventoryChanges); + } + + console.log(inventory.RegularCredits); + console.log("inventoryChanges", inventoryChanges); + return { inventoryChanges, MissionRewards, missionCompletionCredits }; +}; + +//might not be faithful to original +//TODO: consider ActiveBoosters +export const calculateFinalCredits = ( + inventory: HydratedDocument, + { + missionDropCredits, + missionCompletionCredits, + rngRewardCredits = 0 + }: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number } +) => { + const hasDailyCreditBonus = true; + const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits; + console.log( + "missionDropCredits", + missionDropCredits, + "missionCompletionCredits", + missionCompletionCredits, + "rngRewardCredits", + rngRewardCredits + ); + console.log("totalCredits", totalCredits); + + const finalCredits = { + MissionCredits: [missionDropCredits, missionDropCredits], + CreditBonus: [missionCompletionCredits, missionCompletionCredits], + TotalCredits: [totalCredits, totalCredits] + }; + + if (hasDailyCreditBonus) { + inventory.RegularCredits += totalCredits; + finalCredits.CreditBonus[1] *= 2; + finalCredits.MissionCredits[1] *= 2; + finalCredits.TotalCredits[1] *= 2; + } + + if (!hasDailyCreditBonus) { + return finalCredits; + } + return { ...finalCredits, DailyMissionBonus: true }; +}; + +function getLevelCreditRewards(nodeName: string): number { + const minEnemyLevel = getNode(nodeName).minEnemyLevel; + + return 1000 + (minEnemyLevel - 1) * 100; + + //TODO: get dark sektor fixed credit rewards and railjack bonus +} + +function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { const drops: IRngResult[] = []; if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; @@ -62,152 +321,5 @@ const getRewards = ({ } } } - - logger.debug("Mission rewards:", drops); - return formatRewardsToInventoryType(drops); -}; - -const combineRewardAndLootInventory = ( - rewardInventory: IMissionInventoryUpdateRequest, - lootInventory: IMissionInventoryUpdateRequest -) => { - const missionCredits = lootInventory.RegularCredits || 0; - const creditsBonus = rewardInventory.RegularCredits || 0; - const totalCredits = missionCredits + creditsBonus; - let FusionPoints = rewardInventory.FusionPoints || 0; - - // Discharge Endo picked up during the mission - if (lootInventory.FusionBundles) { - for (const fusionBundle of lootInventory.FusionBundles) { - if (fusionBundle.ItemType in fusionBundles) { - FusionPoints += fusionBundles[fusionBundle.ItemType] * fusionBundle.ItemCount; - } else { - logger.error(`unknown fusion bundle: ${fusionBundle.ItemType}`); - } - } - lootInventory.FusionBundles = undefined; - } - - lootInventory.RegularCredits = totalCredits; - lootInventory.FusionPoints = FusionPoints; - inventoryFields.forEach((field: IInventoryFieldType) => { - if (rewardInventory[field] && !lootInventory[field]) { - lootInventory[field] = []; - } - rewardInventory[field]?.forEach(item => lootInventory[field]!.push(item)); - }); - - return { - combinedInventoryChanges: lootInventory, - TotalCredits: [totalCredits, totalCredits], - CreditsBonus: [creditsBonus, creditsBonus], - MissionCredits: [missionCredits, missionCredits], - FusionPoints: FusionPoints - }; -}; - -const getRotations = (rotationCount: number): number[] => { - if (rotationCount === 0) return [0]; - - const rotationPattern = [0, 0, 1, 2]; // A, A, B, C - const rotatedValues = []; - - for (let i = 0; i < rotationCount; i++) { - rotatedValues.push(rotationPattern[i % rotationPattern.length]); - } - - return rotatedValues; -}; - -const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { - return getRandomReward(pool as IRngResult[]); -}; - -const creditBundles: Record = { - "/Lotus/StoreItems/Types/PickUps/Credits/1500Credits": 1500, - "/Lotus/StoreItems/Types/PickUps/Credits/2000Credits": 2000, - "/Lotus/StoreItems/Types/PickUps/Credits/2500Credits": 2500, - "/Lotus/StoreItems/Types/PickUps/Credits/3000Credits": 3000, - "/Lotus/StoreItems/Types/PickUps/Credits/4000Credits": 4000, - "/Lotus/StoreItems/Types/PickUps/Credits/5000Credits": 5000, - "/Lotus/StoreItems/Types/PickUps/Credits/7500Credits": 7500, - "/Lotus/StoreItems/Types/PickUps/Credits/10000Credits": 10000, - "/Lotus/StoreItems/Types/StoreItems/CreditBundles/Zariman/TableACreditsCommon": 15000, - "/Lotus/StoreItems/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000, - "/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardOneHard": 105000, - "/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardTwoHard": 175000, - "/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardThreeHard": 250000 -}; - -const fusionBundles: Record = { - "/Lotus/Upgrades/Mods/FusionBundles/CommonFusionBundle": 15, - "/Lotus/Upgrades/Mods/FusionBundles/UncommonFusionBundle": 50, - "/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle": 80 -}; - -const formatRewardsToInventoryType = ( - rewards: IRngResult[] -): { InventoryChanges: IMissionInventoryUpdateRequest; MissionRewards: IMissionRewardResponse[] } => { - const InventoryChanges: IMissionInventoryUpdateRequest = {}; - const MissionRewards: IMissionRewardResponse[] = []; - for (const reward of rewards) { - if (reward.type in creditBundles) { - InventoryChanges.RegularCredits ??= 0; - InventoryChanges.RegularCredits += creditBundles[reward.type] * reward.itemCount; - } else { - const type = reward.type.replace("/Lotus/StoreItems/", "/Lotus/"); - if (type in fusionBundles) { - InventoryChanges.FusionPoints ??= 0; - InventoryChanges.FusionPoints += fusionBundles[type] * reward.itemCount; - } else if (type in ExportUpgrades) { - addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "RawUpgrades"); - } else if (type in ExportGear) { - addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "Consumables"); - } else if (type in ExportRecipes) { - addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "Recipes"); - } else if ( - type in ExportRelics || - (type in ExportResources && ExportResources[type].productCategory == "MiscItems") - ) { - addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "MiscItems"); - } else { - logger.error(`rolled reward ${reward.itemCount}X ${reward.type} but unsure how to give it`); - } - } - } - return { InventoryChanges, MissionRewards }; -}; - -const addRewardResponse = ( - InventoryChanges: IMissionInventoryUpdateRequest, - MissionRewards: IMissionRewardResponse[], - ItemType: string, - ItemCount: number, - InventoryCategory: IInventoryFieldType -) => { - if (!ItemType) return; - - if (!InventoryChanges[InventoryCategory]) { - InventoryChanges[InventoryCategory] = []; - } - - const existReward = InventoryChanges[InventoryCategory].find(item => item.ItemType === ItemType); - if (existReward) { - existReward.ItemCount += ItemCount; - const missionReward = MissionRewards.find(missionReward => missionReward.TypeName === ItemType); - if (missionReward) { - missionReward.ItemCount += ItemCount; - } - } else { - InventoryChanges[InventoryCategory].push({ ItemType, ItemCount }); - MissionRewards.push({ - ItemCount, - TweetText: ItemType, // ensure if/how this even still used, or if it's needed at all - ProductCategory: InventoryCategory, - StoreItem: ItemType.replace("/Lotus/", "/Lotus/StoreItems/"), - TypeName: ItemType - }); - } -}; - -export { getRewards, combineRewardAndLootInventory }; + return drops; +} diff --git a/src/services/personalRoomsService.ts b/src/services/personalRoomsService.ts index 1392ce46..c9604c7a 100644 --- a/src/services/personalRoomsService.ts +++ b/src/services/personalRoomsService.ts @@ -8,3 +8,13 @@ export const getPersonalRooms = async (accountId: string) => { } return personalRooms; }; + +export const updateShipFeature = async (accountId: string, shipFeature: string) => { + const personalRooms = await getPersonalRooms(accountId); + personalRooms.Ship.Features.push(shipFeature); + + //push if not already present + + //remove ship feature item from misc items + await personalRooms.save(); +}; diff --git a/src/services/questService.ts b/src/services/questService.ts new file mode 100644 index 00000000..22f53d80 --- /dev/null +++ b/src/services/questService.ts @@ -0,0 +1,35 @@ +import { IInventoryDatabase, IQuestKeyDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; +import { HydratedDocument } from "mongoose"; + +export const updateQuestKey = ( + inventory: HydratedDocument, + questKeyUpdate: IUpdateQuestRequest["QuestKeys"] +): void => { + if (questKeyUpdate.length > 1) { + logger.error(`more than 1 quest key not supported`); + throw new Error("more than 1 quest key not supported"); + } + + const questKeyIndex = inventory.QuestKeys.findIndex(questKey => questKey.ItemType === questKeyUpdate[0].ItemType); + console.log("quest key index", questKeyIndex); + + if (questKeyIndex === -1) { + throw new Error(`quest key ${questKeyUpdate[0].ItemType} not found`); + } + + inventory.QuestKeys[questKeyIndex] = questKeyUpdate[0]; + + if (questKeyUpdate[0].Completed) { + inventory.QuestKeys[questKeyIndex].CompletionDate = new Date(); + } +}; + +export interface IUpdateQuestRequest { + QuestKeys: Omit[]; + PS: string; + questCompletion: boolean; + PlayerShipEvents: unknown[]; + crossPlaySetting: string; + DoQuestReward: boolean; +} diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 3c2cf582..3a225fc3 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -43,6 +43,7 @@ export interface IInventoryDatabase GuildId?: Types.ObjectId; // GuildId changed from ?IOid to ?Types.ObjectId PendingRecipes: IPendingRecipe[]; QuestKeys: IQuestKeyDatabase[]; + ActiveQuest: string; BlessingCooldown: Date; Ships: Types.ObjectId[]; WeaponSkins: IWeaponSkinDatabase[]; @@ -71,7 +72,7 @@ export interface IInventoryDatabase } export interface IQuestKeyDatabase { - Progress?: IQuestProgress[]; + Progress?: IQuestStage[]; unlock?: boolean; Completed?: boolean; CustomData?: string; //TODO: check whether this actually exists @@ -205,7 +206,7 @@ export interface IInventoryClient extends IDailyAffiliations { RawUpgrades: IRawUpgrade[]; ReceivedStartingGear: boolean; Ships: IShipInventory[]; - QuestKeys: IQuestKeyResponse[]; + QuestKeys: IQuestKeyClient[]; FlavourItems: IFlavourItem[]; Scoops: IEquipmentDatabase[]; TrainingRetriesLeft: number; @@ -889,14 +890,14 @@ export interface IPlayerSkills { LPS_DRIFT_ENDURANCE: number; } -export interface IQuestKeyResponse extends Omit { +export interface IQuestKeyClient extends Omit { CompletionDate?: IMongoDate; } -export interface IQuestProgress { - c: number; - i: boolean; - m: boolean; +export interface IQuestStage { + c?: number; + i?: boolean; + m?: boolean; b?: any[]; } diff --git a/src/types/missionTypes.ts b/src/types/missionTypes.ts index fab170a9..8e79e8d8 100644 --- a/src/types/missionTypes.ts +++ b/src/types/missionTypes.ts @@ -1,11 +1,11 @@ export const inventoryFields = ["RawUpgrades", "MiscItems", "Consumables", "Recipes"] as const; export type IInventoryFieldType = (typeof inventoryFields)[number]; -export interface IMissionRewardResponse { - StoreItem?: string; - TypeName: string; +export interface IMissionReward { + StoreItem: string; + TypeName?: string; UpgradeLevel?: number; ItemCount: number; - TweetText: string; - ProductCategory: string; + TweetText?: string; + ProductCategory?: string; } diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 3154d428..678f4450 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -3,9 +3,7 @@ import { ArtifactPolarity, IPolarity, IEquipmentClient } from "@/src/types/inven import { IBooster, IChallengeProgress, - IConsumable, IEvolutionProgress, - IMiscItem, ITypeCount, IMission, IRawUpgrade, @@ -33,10 +31,13 @@ export interface IUpdateChallengeProgressRequest { SeasonChallengeCompletions: ISeasonChallenge[]; } -export interface IMissionInventoryUpdateRequest { - rewardsMultiplier?: number; - ActiveBoosters?: IBooster[]; +export type IMissionInventoryUpdateRequest = { AffiliationChanges?: IAffiliationChange[]; + crossPlaySetting?: string; + rewardsMultiplier?: number; + GoalTag: string; + LevelKeyName: string; + ActiveBoosters?: IBooster[]; Suits?: IEquipmentClient[]; LongGuns?: IEquipmentClient[]; Pistols?: IEquipmentClient[]; @@ -52,21 +53,40 @@ export interface IMissionInventoryUpdateRequest { MoaPets?: IEquipmentClient[]; FusionBundles?: ITypeCount[]; RawUpgrades?: IRawUpgrade[]; - MiscItems?: IMiscItem[]; - Consumables?: IConsumable[]; + MiscItems?: ITypeCount[]; + Consumables?: ITypeCount[]; FusionTreasures?: IFusionTreasure[]; - Recipes?: IConsumable[]; + Recipes?: ITypeCount[]; + QuestKeys?: IQuestKeyDatabase[]; //completionDate is not sent RegularCredits?: number; - ChallengeProgress?: IChallengeProgress[]; - RewardInfo?: IMissionInventoryUpdateRequestRewardInfo; + MissionFailed: boolean; + MissionStatus: IMissionStatus; + AliveTime: number; + MissionTime: number; Missions?: IMission; - EvolutionProgress?: IEvolutionProgress[]; LastRegionPlayed?: TSolarMapRegion; + GameModeId: number; + hosts: string[]; + currentClients: unknown[]; + ChallengeProgress: IChallengeProgress[]; + PS: string; + ActiveDojoColorResearch: string; + RewardInfo?: IRewardInfo; + ReceivedCeremonyMsg: boolean; + LastCeremonyResetDate: number; + MissionPTS: number; + RepHash: string; + EndOfMatchUpload: boolean; + ObjectiveReached: boolean; + sharedSessionId: string; + FpsAvg: number; + FpsMin: number; + FpsMax: number; + FpsSamples: number; + EvolutionProgress?: IEvolutionProgress[]; +}; - FusionPoints?: number; // Not a part of the request, but we put it in this struct as an intermediate storage. -} - -export interface IMissionInventoryUpdateRequestRewardInfo { +export interface IRewardInfo { node: string; VaultsCracked?: number; // for Spy missions rewardTier?: number; @@ -82,15 +102,15 @@ export interface IMissionInventoryUpdateRequestRewardInfo { rewardSeed?: number; } +export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED"; + export interface IInventorySlotsRequest { Bin: "PveBonusLoadoutBin"; } - export interface IUpdateGlyphRequest { AvatarImageType: string; AvatarImage: string; } - export interface IUpgradesRequest { ItemCategory: TEquipmentKey; ItemId: IOid; @@ -98,7 +118,6 @@ export interface IUpgradesRequest { UpgradeVersion: number; Operations: IUpgradeOperation[]; } - export interface IUpgradeOperation { OperationType: string; UpgradeRequirement: string; // uniqueName of item being consumed @@ -106,3 +125,8 @@ export interface IUpgradeOperation { PolarizeValue: ArtifactPolarity; PolarityRemap: IPolarity[]; } +export interface IUnlockShipFeatureRequest { + Feature: string; + KeyChain: string; + ChainStage: number; +} diff --git a/static/fixed_responses/questCompletionRewards.json b/static/fixed_responses/questCompletionRewards.json new file mode 100644 index 00000000..9d727f64 --- /dev/null +++ b/static/fixed_responses/questCompletionRewards.json @@ -0,0 +1,13 @@ +{ + "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain": [ + { + "ItemType": "/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit", + "ItemCount": 1 + } + ], + "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain": [{ "ItemType": "/Lotus/Types/Recipes/WarframeRecipes/BrokenFrameBlueprint", "ItemCount": 1 }] +}