From 67a275a0099e32025f9f6666743465cdf8300cb6 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 3 Mar 2025 12:48:39 -0800 Subject: [PATCH] feat: dojo component "collecting materials" stage (#1071) Closes #1051 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1071 Co-authored-by: Sainan Co-committed-by: Sainan --- config.json.example | 1 + .../api/abortDojoComponentController.ts | 19 +++++ .../contributeToDojoComponentController.ts | 82 +++++++++++++++++++ src/controllers/api/guildTechController.ts | 7 +- .../api/setDojoComponentMessageController.ts | 2 +- .../api/startDojoRecipeController.ts | 24 ++++-- src/models/guildModel.ts | 2 + src/routes/api.ts | 4 + src/services/configService.ts | 1 + src/services/guildService.ts | 49 +++++++---- static/webui/index.html | 4 + static/webui/translations/en.js | 1 + static/webui/translations/ru.js | 1 + 13 files changed, 164 insertions(+), 33 deletions(-) create mode 100644 src/controllers/api/abortDojoComponentController.ts create mode 100644 src/controllers/api/contributeToDojoComponentController.ts diff --git a/config.json.example b/config.json.example index c6b12971..af2e9846 100644 --- a/config.json.example +++ b/config.json.example @@ -32,6 +32,7 @@ "unlockArcanesEverywhere": true, "noDailyStandingLimits": true, "instantResourceExtractorDrones": false, + "noDojoRoomBuildStage": true, "noDojoResearchCosts": true, "noDojoResearchTime": true, "spoofMasteryRank": -1 diff --git a/src/controllers/api/abortDojoComponentController.ts b/src/controllers/api/abortDojoComponentController.ts new file mode 100644 index 00000000..da10a839 --- /dev/null +++ b/src/controllers/api/abortDojoComponentController.ts @@ -0,0 +1,19 @@ +import { getDojoClient, getGuildForRequestEx } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const abortDojoComponentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest; + // TODO: Move already-contributed credits & items to the clan vault + guild.DojoComponents.pull({ _id: request.ComponentId }); + await guild.save(); + res.json(getDojoClient(guild, 0)); +}; + +export interface IAbortDojoComponentRequest { + ComponentId: string; +} diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts new file mode 100644 index 00000000..72e20ef1 --- /dev/null +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -0,0 +1,82 @@ +import { getDojoClient, getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; +import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; + +export const contributeToDojoComponentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest; + const component = guild.DojoComponents.id(request.ComponentId)!; + const componentMeta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; + + component.RegularCredits ??= 0; + if (component.RegularCredits + request.RegularCredits > scaleRequiredCount(componentMeta.price)) { + request.RegularCredits = scaleRequiredCount(componentMeta.price) - component.RegularCredits; + } + component.RegularCredits += request.RegularCredits; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, request.RegularCredits, false); + + component.MiscItems ??= []; + const miscItemChanges: IMiscItem[] = []; + for (const ingredientContribution of request.IngredientContributions) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType); + if (componentMiscItem) { + const ingredientMeta = componentMeta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; + if ( + componentMiscItem.ItemCount + ingredientContribution.ItemCount > + scaleRequiredCount(ingredientMeta.ItemCount) + ) { + ingredientContribution.ItemCount = + scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + } + componentMiscItem.ItemCount += ingredientContribution.ItemCount; + } else { + component.MiscItems.push(ingredientContribution); + } + miscItemChanges.push({ + ItemType: ingredientContribution.ItemType, + ItemCount: ingredientContribution.ItemCount * -1 + }); + } + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + + if (component.RegularCredits >= scaleRequiredCount(componentMeta.price)) { + let fullyFunded = true; + for (const ingredient of componentMeta.ingredients) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType); + if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) { + fullyFunded = false; + break; + } + } + if (fullyFunded) { + component.RegularCredits = undefined; + component.MiscItems = undefined; + component.CompletionTime = new Date(Date.now() + componentMeta.time * 1000); + } + } + + await guild.save(); + await inventory.save(); + res.json({ + ...getDojoClient(guild, 0, component._id), + InventoryChanges: inventoryChanges + }); +}; + +export interface IContributeToDojoComponentRequest { + ComponentId: string; + IngredientContributions: { + ItemType: string; + ItemCount: number; + }[]; + RegularCredits: number; + VaultIngredientContributions: []; + VaultCredits: number; +} diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 129131d6..d7cb99ad 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getGuildForRequestEx } from "@/src/services/guildService"; +import { getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; @@ -130,8 +130,3 @@ interface IGuildTechContributeFields { VaultCredits: number; VaultMiscItems: IMiscItem[]; } - -const scaleRequiredCount = (count: number): number => { - // The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans. - return Math.max(1, Math.trunc(count / 100)); -}; diff --git a/src/controllers/api/setDojoComponentMessageController.ts b/src/controllers/api/setDojoComponentMessageController.ts index 0b75838b..255c4d2e 100644 --- a/src/controllers/api/setDojoComponentMessageController.ts +++ b/src/controllers/api/setDojoComponentMessageController.ts @@ -12,7 +12,7 @@ export const setDojoComponentMessageController: RequestHandler = async (req, res component.Message = payload.Message; } await guild.save(); - res.json(getDojoClient(guild, 1)); + res.json(getDojoClient(guild, 0, component._id)); }; type SetDojoComponentMessageRequest = { Name: string } | { Message: string }; diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index b449b2ed..96a1e99c 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -3,6 +3,7 @@ import { IDojoComponentClient } from "@/src/types/guildTypes"; import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { config } from "@/src/services/configService"; interface IStartDojoRecipeRequest { PlacedComponent: IDojoComponentClient; @@ -20,15 +21,20 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { guild.DojoEnergy += room.energy; } - guild.DojoComponents.push({ - _id: new Types.ObjectId(), - pf: request.PlacedComponent.pf, - ppf: request.PlacedComponent.ppf, - pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), - op: request.PlacedComponent.op, - pp: request.PlacedComponent.pp, - CompletionTime: new Date(Date.now()) // TOOD: Omit this field & handle the "Collecting Materials" state. - }); + const component = + guild.DojoComponents[ + guild.DojoComponents.push({ + _id: new Types.ObjectId(), + pf: request.PlacedComponent.pf, + ppf: request.PlacedComponent.ppf, + pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), + op: request.PlacedComponent.op, + pp: request.PlacedComponent.pp + }) - 1 + ]; + if (config.noDojoRoomBuildStage) { + component.CompletionTime = new Date(Date.now()); + } await guild.save(); res.json(getDojoClient(guild, 0)); }; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 781fcd24..417eeb7e 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -16,6 +16,8 @@ const dojoComponentSchema = new Schema({ pp: String, Name: String, Message: String, + RegularCredits: Number, + MiscItems: { type: [typeCountSchema], default: undefined }, CompletionTime: Date }); diff --git a/src/routes/api.ts b/src/routes/api.ts index c303059d..988d5207 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,5 +1,6 @@ import express from "express"; import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandonLibraryDailyTaskController"; +import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; @@ -11,6 +12,7 @@ import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompl import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; +import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; @@ -135,6 +137,7 @@ apiRouter.get("/surveys.php", surveysController); apiRouter.get("/updateSession.php", updateSessionGetController); // post +apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriendImage.php", addFriendImageController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); @@ -144,6 +147,7 @@ apiRouter.post("/changeDojoRoot.php", changeDojoRootController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); +apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); diff --git a/src/services/configService.ts b/src/services/configService.ts index 83571774..788ee4bf 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -58,6 +58,7 @@ interface IConfig { unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; instantResourceExtractorDrones?: boolean; + noDojoRoomBuildStage?: boolean; noDojoResearchCosts?: boolean; noDojoResearchTime?: boolean; spoofMasteryRank?: number; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 6c556ff4..5a4658e1 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -5,6 +5,7 @@ import { Guild, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { Types } from "mongoose"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -27,7 +28,11 @@ export const getGuildForRequestEx = async ( return guild; }; -export const getDojoClient = (guild: TGuildDatabaseDocument, status: number): IDojoClient => { +export const getDojoClient = ( + guild: TGuildDatabaseDocument, + status: number, + componentId: Types.ObjectId | undefined = undefined +): IDojoClient => { const dojo: IDojoClient = { _id: { $oid: guild._id.toString() }, Name: guild.Name, @@ -41,23 +46,33 @@ export const getDojoClient = (guild: TGuildDatabaseDocument, status: number): ID DojoComponents: [] }; guild.DojoComponents.forEach(dojoComponent => { - const clientComponent: IDojoComponentClient = { - id: toOid(dojoComponent._id), - pf: dojoComponent.pf, - ppf: dojoComponent.ppf, - Name: dojoComponent.Name, - Message: dojoComponent.Message, - DecoCapacity: 600 - }; - if (dojoComponent.pi) { - clientComponent.pi = toOid(dojoComponent.pi); - clientComponent.op = dojoComponent.op!; - clientComponent.pp = dojoComponent.pp!; + if (!componentId || componentId == dojoComponent._id) { + const clientComponent: IDojoComponentClient = { + id: toOid(dojoComponent._id), + pf: dojoComponent.pf, + ppf: dojoComponent.ppf, + Name: dojoComponent.Name, + Message: dojoComponent.Message, + DecoCapacity: 600 + }; + if (dojoComponent.pi) { + clientComponent.pi = toOid(dojoComponent.pi); + clientComponent.op = dojoComponent.op!; + clientComponent.pp = dojoComponent.pp!; + } + if (dojoComponent.CompletionTime) { + clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); + } else { + clientComponent.RegularCredits = dojoComponent.RegularCredits; + clientComponent.MiscItems = dojoComponent.MiscItems; + } + dojo.DojoComponents.push(clientComponent); } - if (dojoComponent.CompletionTime) { - clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); - } - dojo.DojoComponents.push(clientComponent); }); return dojo; }; + +export const scaleRequiredCount = (count: number): number => { + // The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans. + return Math.max(1, Math.trunc(count / 100)); +}; diff --git a/static/webui/index.html b/static/webui/index.html index 72dd4de9..a84fa719 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -521,6 +521,10 @@ +
+ + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 406a61dd..9294bd29 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -112,6 +112,7 @@ dict = { cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, + cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, 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 a5bc1a4c..6f3179c6 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -113,6 +113,7 @@ dict = { cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, cheats_instantResourceExtractorDrones: `[UNTRANSLATED] Instant Resource Extractor Drones`, + cheats_noDojoRoomBuildStage: `[UNTRANSLATED] No Dojo Room Build Stage`, cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`, cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`,