feat: dojo component "collecting materials" stage (#1071)
Some checks failed
Build Docker image / docker (push) Waiting to run
Build / build (18) (push) Has been cancelled
Build / build (20) (push) Has been cancelled
Build / build (22) (push) Has been cancelled

Closes #1051

Reviewed-on: #1071
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
This commit is contained in:
Sainan 2025-03-03 12:48:39 -08:00 committed by OrdisPrime
parent 77cadc732c
commit 67a275a009
13 changed files with 164 additions and 33 deletions

View File

@ -32,6 +32,7 @@
"unlockArcanesEverywhere": true, "unlockArcanesEverywhere": true,
"noDailyStandingLimits": true, "noDailyStandingLimits": true,
"instantResourceExtractorDrones": false, "instantResourceExtractorDrones": false,
"noDojoRoomBuildStage": true,
"noDojoResearchCosts": true, "noDojoResearchCosts": true,
"noDojoResearchTime": true, "noDojoResearchTime": true,
"spoofMasteryRank": -1 "spoofMasteryRank": -1

View File

@ -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;
}

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express"; 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 { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService";
@ -130,8 +130,3 @@ interface IGuildTechContributeFields {
VaultCredits: number; VaultCredits: number;
VaultMiscItems: IMiscItem[]; 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));
};

View File

@ -12,7 +12,7 @@ export const setDojoComponentMessageController: RequestHandler = async (req, res
component.Message = payload.Message; component.Message = payload.Message;
} }
await guild.save(); await guild.save();
res.json(getDojoClient(guild, 1)); res.json(getDojoClient(guild, 0, component._id));
}; };
type SetDojoComponentMessageRequest = { Name: string } | { Message: string }; type SetDojoComponentMessageRequest = { Name: string } | { Message: string };

View File

@ -3,6 +3,7 @@ import { IDojoComponentClient } from "@/src/types/guildTypes";
import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
interface IStartDojoRecipeRequest { interface IStartDojoRecipeRequest {
PlacedComponent: IDojoComponentClient; PlacedComponent: IDojoComponentClient;
@ -20,15 +21,20 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => {
guild.DojoEnergy += room.energy; guild.DojoEnergy += room.energy;
} }
guild.DojoComponents.push({ const component =
_id: new Types.ObjectId(), guild.DojoComponents[
pf: request.PlacedComponent.pf, guild.DojoComponents.push({
ppf: request.PlacedComponent.ppf, _id: new Types.ObjectId(),
pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), pf: request.PlacedComponent.pf,
op: request.PlacedComponent.op, ppf: request.PlacedComponent.ppf,
pp: request.PlacedComponent.pp, pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid),
CompletionTime: new Date(Date.now()) // TOOD: Omit this field & handle the "Collecting Materials" state. op: request.PlacedComponent.op,
}); pp: request.PlacedComponent.pp
}) - 1
];
if (config.noDojoRoomBuildStage) {
component.CompletionTime = new Date(Date.now());
}
await guild.save(); await guild.save();
res.json(getDojoClient(guild, 0)); res.json(getDojoClient(guild, 0));
}; };

View File

@ -16,6 +16,8 @@ const dojoComponentSchema = new Schema<IDojoComponentDatabase>({
pp: String, pp: String,
Name: String, Name: String,
Message: String, Message: String,
RegularCredits: Number,
MiscItems: { type: [typeCountSchema], default: undefined },
CompletionTime: Date CompletionTime: Date
}); });

View File

@ -1,5 +1,6 @@
import express from "express"; import express from "express";
import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandonLibraryDailyTaskController"; import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandonLibraryDailyTaskController";
import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController";
import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController";
import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController";
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; 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 { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController";
import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController";
import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController";
import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController";
import { createGuildController } from "@/src/controllers/api/createGuildController"; import { createGuildController } from "@/src/controllers/api/createGuildController";
import { creditsController } from "@/src/controllers/api/creditsController"; import { creditsController } from "@/src/controllers/api/creditsController";
import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController";
@ -135,6 +137,7 @@ apiRouter.get("/surveys.php", surveysController);
apiRouter.get("/updateSession.php", updateSessionGetController); apiRouter.get("/updateSession.php", updateSessionGetController);
// post // post
apiRouter.post("/abortDojoComponent.php", abortDojoComponentController);
apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/activateRandomMod.php", activateRandomModController);
apiRouter.post("/addFriendImage.php", addFriendImageController); apiRouter.post("/addFriendImage.php", addFriendImageController);
apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/arcaneCommon.php", arcaneCommonController);
@ -144,6 +147,7 @@ apiRouter.post("/changeDojoRoot.php", changeDojoRootController);
apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController);
apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController);
apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController);
apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController);
apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/createGuild.php", createGuildController);
apiRouter.post("/drones.php", dronesController); apiRouter.post("/drones.php", dronesController);
apiRouter.post("/endlessXp.php", endlessXpController); apiRouter.post("/endlessXp.php", endlessXpController);

View File

@ -58,6 +58,7 @@ interface IConfig {
unlockArcanesEverywhere?: boolean; unlockArcanesEverywhere?: boolean;
noDailyStandingLimits?: boolean; noDailyStandingLimits?: boolean;
instantResourceExtractorDrones?: boolean; instantResourceExtractorDrones?: boolean;
noDojoRoomBuildStage?: boolean;
noDojoResearchCosts?: boolean; noDojoResearchCosts?: boolean;
noDojoResearchTime?: boolean; noDojoResearchTime?: boolean;
spoofMasteryRank?: number; spoofMasteryRank?: number;

View File

@ -5,6 +5,7 @@ import { Guild, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes"; import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { Types } from "mongoose";
export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => { export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -27,7 +28,11 @@ export const getGuildForRequestEx = async (
return guild; 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 = { const dojo: IDojoClient = {
_id: { $oid: guild._id.toString() }, _id: { $oid: guild._id.toString() },
Name: guild.Name, Name: guild.Name,
@ -41,23 +46,33 @@ export const getDojoClient = (guild: TGuildDatabaseDocument, status: number): ID
DojoComponents: [] DojoComponents: []
}; };
guild.DojoComponents.forEach(dojoComponent => { guild.DojoComponents.forEach(dojoComponent => {
const clientComponent: IDojoComponentClient = { if (!componentId || componentId == dojoComponent._id) {
id: toOid(dojoComponent._id), const clientComponent: IDojoComponentClient = {
pf: dojoComponent.pf, id: toOid(dojoComponent._id),
ppf: dojoComponent.ppf, pf: dojoComponent.pf,
Name: dojoComponent.Name, ppf: dojoComponent.ppf,
Message: dojoComponent.Message, Name: dojoComponent.Name,
DecoCapacity: 600 Message: dojoComponent.Message,
}; DecoCapacity: 600
if (dojoComponent.pi) { };
clientComponent.pi = toOid(dojoComponent.pi); if (dojoComponent.pi) {
clientComponent.op = dojoComponent.op!; clientComponent.pi = toOid(dojoComponent.pi);
clientComponent.pp = dojoComponent.pp!; 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; 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));
};

View File

@ -521,6 +521,10 @@
<input class="form-check-input" type="checkbox" id="instantResourceExtractorDrones" /> <input class="form-check-input" type="checkbox" id="instantResourceExtractorDrones" />
<label class="form-check-label" for="instantResourceExtractorDrones" data-loc="cheats_instantResourceExtractorDrones"></label> <label class="form-check-label" for="instantResourceExtractorDrones" data-loc="cheats_instantResourceExtractorDrones"></label>
</div> </div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="noDojoRoomBuildStage" />
<label class="form-check-label" for="noDojoRoomBuildStage" data-loc="cheats_noDojoRoomBuildStage"></label>
</div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="noDojoResearchCosts" /> <input class="form-check-input" type="checkbox" id="noDojoResearchCosts" />
<label class="form-check-label" for="noDojoResearchCosts" data-loc="cheats_noDojoResearchCosts"></label> <label class="form-check-label" for="noDojoResearchCosts" data-loc="cheats_noDojoResearchCosts"></label>

View File

@ -112,6 +112,7 @@ dict = {
cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`,
cheats_noDailyStandingLimits: `No Daily Standing Limits`, cheats_noDailyStandingLimits: `No Daily Standing Limits`,
cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`,
cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`,
cheats_noDojoResearchCosts: `No Dojo Research Costs`, cheats_noDojoResearchCosts: `No Dojo Research Costs`,
cheats_noDojoResearchTime: `No Dojo Research Time`, cheats_noDojoResearchTime: `No Dojo Research Time`,
cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`,

View File

@ -113,6 +113,7 @@ dict = {
cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`,
cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`,
cheats_instantResourceExtractorDrones: `[UNTRANSLATED] Instant Resource Extractor Drones`, cheats_instantResourceExtractorDrones: `[UNTRANSLATED] Instant Resource Extractor Drones`,
cheats_noDojoRoomBuildStage: `[UNTRANSLATED] No Dojo Room Build Stage`,
cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`, cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`,
cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`, cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`,
cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`,