diff --git a/config.json.example b/config.json.example index 04add3df..c6b12971 100644 --- a/config.json.example +++ b/config.json.example @@ -31,6 +31,7 @@ "unlockExilusEverywhere": true, "unlockArcanesEverywhere": true, "noDailyStandingLimits": true, + "instantResourceExtractorDrones": false, "noDojoResearchCosts": true, "noDojoResearchTime": true, "spoofMasteryRank": -1 diff --git a/package-lock.json b/package-lock.json index 8f67ed87..ad2dcbc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.38", + "warframe-public-export-plus": "^0.5.39", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4083,9 +4083,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.38", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.38.tgz", - "integrity": "sha512-yvc86eOmYPSnnU8LzLBhg/lR1AS1RHID24TqFHVcZuOzMYc934NL8Cv7rtllyefWAMyl7iA5x9tyXSuJWbi6CA==" + "version": "0.5.39", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.39.tgz", + "integrity": "sha512-sEGZedtW4I/M2ceoDs6MQ5eHD7sJgv1KRNLt8BWByXLuDa7qTR3Y9px5TGxqt/rBHKGUyPO1LUxu4bDGZi6yXw==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index f7a70d38..aae92ef4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.38", + "warframe-public-export-plus": "^0.5.39", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/dronesController.ts b/src/controllers/api/dronesController.ts index bff5086c..eef59c68 100644 --- a/src/controllers/api/dronesController.ts +++ b/src/controllers/api/dronesController.ts @@ -1,7 +1,140 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { config } from "@/src/services/configService"; +import { addMiscItems, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getRandomInt, getRandomWeightedReward3 } from "@/src/services/rngService"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IDroneClient } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; +import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus"; -const dronesController: RequestHandler = (_req, res) => { - res.json({}); +export const dronesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + if ("GetActive" in req.query) { + const activeDrones: IActiveDrone[] = []; + for (const drone of inventory.Drones) { + if (drone.DeployTime) { + activeDrones.push({ + DeployTime: toMongoDate(drone.DeployTime), + System: drone.System!, + ItemId: toOid(drone._id), + ItemType: drone.ItemType, + CurrentHP: drone.CurrentHP, + DamageTime: toMongoDate(drone.DamageTime!), + PendingDamage: drone.PendingDamage!, + Resources: [ + { + ItemType: drone.ResourceType!, + BinTotal: drone.ResourceCount!, + StartTime: toMongoDate(drone.DeployTime) + } + ] + }); + } + } + res.json({ + ActiveDrones: activeDrones + }); + } else if ("droneId" in req.query && "systemIndex" in req.query) { + const drone = inventory.Drones.id(req.query.droneId as string)!; + const droneMeta = ExportDrones[drone.ItemType]; + drone.DeployTime = config.instantResourceExtractorDrones ? new Date(0) : new Date(); + if (drone.RepairStart) { + const repairMinutes = (Date.now() - drone.RepairStart.getTime()) / 60_000; + const hpPerMinute = droneMeta.repairRate / 60; + drone.CurrentHP = Math.min(drone.CurrentHP + Math.round(repairMinutes * hpPerMinute), droneMeta.durability); + drone.RepairStart = undefined; + } + drone.System = parseInt(req.query.systemIndex as string); + const system = ExportSystems[drone.System - 1]; + drone.DamageTime = config.instantResourceExtractorDrones + ? new Date() + : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000)); + drone.PendingDamage = + Math.random() < system.damageChance + ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue) + : 0; + const resource = getRandomWeightedReward3(system.resources, droneMeta.probabilities)!; + //logger.debug(`drone rolled`, resource); + drone.ResourceType = "/Lotus/" + resource.StoreItem.substring(18); + const resourceMeta = ExportResources[drone.ResourceType]; + if (resourceMeta.pickupQuantity) { + const pickupsToCollect = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity]; + drone.ResourceCount = 0; + for (let i = 0; i != pickupsToCollect; ++i) { + drone.ResourceCount += getRandomInt( + resourceMeta.pickupQuantity.minValue, + resourceMeta.pickupQuantity.maxValue + ); + } + } else { + drone.ResourceCount = 1; + } + await inventory.save(); + res.json({}); + } else if ("collectDroneId" in req.query) { + const drone = inventory.Drones.id(req.query.collectDroneId as string)!; + + if (new Date() >= drone.DamageTime!) { + drone.CurrentHP -= drone.PendingDamage!; + drone.RepairStart = new Date(); + } + + const inventoryChanges: IInventoryChanges = {}; + if (drone.CurrentHP <= 0) { + inventory.RegularCredits += 100; + inventoryChanges.RegularCredits = 100; + inventory.Drones.pull({ _id: req.query.collectDroneId as string }); + inventoryChanges.RemovedIdItems = [ + { + ItemId: { $oid: req.query.collectDroneId } + } + ]; + } else { + const completionTime = drone.DeployTime!.getTime() + ExportDrones[drone.ItemType].fillRate * 3600_000; + if (Date.now() >= completionTime) { + const miscItemChanges = [ + { + ItemType: drone.ResourceType!, + ItemCount: drone.ResourceCount! + } + ]; + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } + + drone.DeployTime = undefined; + drone.System = undefined; + drone.DamageTime = undefined; + drone.PendingDamage = undefined; + drone.ResourceType = undefined; + drone.ResourceCount = undefined; + + inventoryChanges.Drones = [drone.toJSON()]; + } + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); + } else { + throw new Error(`drones.php query not handled`); + } }; -export { dronesController }; +interface IActiveDrone { + DeployTime: IMongoDate; + System: number; + ItemId: IOid; + ItemType: string; + CurrentHP: number; + DamageTime: IMongoDate; + PendingDamage: number; + Resources: { + ItemType: string; + BinTotal: number; + StartTime: IMongoDate; + }[]; +} diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index bf3a346f..466bd197 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -86,6 +86,11 @@ export const sellController: RequestHandler = async (req, res) => { inventory.Hoverboards.pull({ _id: sellItem.String }); }); } + if (payload.Items.Drones) { + payload.Items.Drones.forEach(sellItem => { + inventory.Drones.pull({ _id: sellItem.String }); + }); + } if (payload.Items.Consumables) { const consumablesChanges = []; for (const sellItem of payload.Items.Consumables) { @@ -152,6 +157,7 @@ interface ISellRequest { SentinelWeapons?: ISellItem[]; OperatorAmps?: ISellItem[]; Hoverboards?: ISellItem[]; + Drones?: ISellItem[]; }; SellPrice: number; SellCurrency: diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index b21eee02..2aa16bb9 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -336,7 +336,14 @@ const droneSchema = new Schema( { ItemType: String, CurrentHP: Number, - RepairStart: { type: Date, default: undefined } + RepairStart: { type: Date, default: undefined }, + + DeployTime: { type: Date, default: undefined }, + System: Number, + DamageTime: { type: Date, default: undefined }, + PendingDamage: Number, + ResourceType: String, + ResourceCount: Number }, { id: false } ); @@ -347,6 +354,16 @@ droneSchema.set("toJSON", { const db = obj as IDroneDatabase; client.ItemId = toOid(db._id); + if (db.RepairStart) { + client.RepairStart = toMongoDate(db.RepairStart); + } + + delete db.DeployTime; + delete db.System; + delete db.DamageTime; + delete db.PendingDamage; + delete db.ResourceType; + delete db.ResourceCount; delete obj._id; delete obj.__v; diff --git a/src/routes/api.ts b/src/routes/api.ts index af8de31a..c303059d 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -145,6 +145,7 @@ apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); apiRouter.post("/evolveWeapon.php", evolveWeaponController); apiRouter.post("/findSessions.php", findSessionsController); diff --git a/src/services/configService.ts b/src/services/configService.ts index 466b062c..83571774 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -57,6 +57,7 @@ interface IConfig { unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + instantResourceExtractorDrones?: boolean; noDojoResearchCosts?: boolean; noDojoResearchTime?: boolean; spoofMasteryRank?: number; diff --git a/src/services/rngService.ts b/src/services/rngService.ts index f519c0d8..a7ce5ce4 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -18,7 +18,7 @@ export const getRandomInt = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1)) + min; }; -export const getRandomReward = (pool: IRngResult[]): IRngResult | undefined => { +export const getRandomReward = (pool: T[]): T | undefined => { if (pool.length == 0) return; const totalChance = pool.reduce((accum, item) => accum + item.probability, 0); @@ -71,3 +71,21 @@ export const getRandomWeightedReward2 = ( } return getRandomReward(resultPool); }; + +export const getRandomWeightedReward3 = ( + pool: T[], + weights: Record +): (T & { probability: number }) | undefined => { + const resultPool: (T & { probability: number })[] = []; + const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; + for (const entry of pool) { + ++rarityCounts[entry.Rarity]; + } + for (const entry of pool) { + resultPool.push({ + ...entry, + probability: weights[entry.Rarity] / rarityCounts[entry.Rarity] + }); + } + return getRandomReward(resultPool); +}; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d81491c1..0496a050 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -520,6 +520,13 @@ export interface IDroneDatabase { CurrentHP: number; _id: Types.ObjectId; RepairStart?: Date; + + DeployTime?: Date; + System?: number; + DamageTime?: Date; + PendingDamage?: number; + ResourceType?: string; + ResourceCount?: number; } export interface ITypeXPItem { diff --git a/static/webui/index.html b/static/webui/index.html index 57e35b47..72dd4de9 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -517,6 +517,10 @@ +
+ + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index dd8f5d0b..406a61dd 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -111,6 +111,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, + cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoResearchCosts: `No Dojo Research Costs`, cheats_noDojoResearchTime: `No Dojo Research Time`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index a8a2e3ac..a5bc1a4c 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -112,6 +112,7 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, + cheats_instantResourceExtractorDrones: `[UNTRANSLATED] Instant Resource Extractor Drones`, cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`, cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`,