From a75e0c59afb1dea4a63fc2e215bd497d4a440765 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 13 Apr 2025 05:51:15 -0700 Subject: [PATCH] feat: personal research (#1602) This should be good enough for the railjack quest at least Closes #1599 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1602 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/guildTechController.ts | 239 ++++++++++++------- src/models/inventoryModels/inventoryModel.ts | 36 ++- src/types/inventoryTypes/inventoryTypes.ts | 14 +- 3 files changed, 194 insertions(+), 95 deletions(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 5b0b5374..55083d5a 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -32,11 +32,11 @@ import { logger } from "@/src/utils/logger"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); - const guild = await getGuildForRequestEx(req, inventory); const data = JSON.parse(String(req.body)) as TGuildTechRequest; if (data.Action == "Sync") { let needSave = false; const techProjects: ITechProjectClient[] = []; + const guild = await getGuildForRequestEx(req, inventory); if (guild.TechProjects) { for (const project of guild.TechProjects) { const techProject: ITechProjectClient = { @@ -59,110 +59,170 @@ export const guildTechController: RequestHandler = async (req, res) => { } res.json({ TechProjects: techProjects }); } else if (data.Action == "Start") { - if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { - res.status(400).send("-1").end(); - return; - } - const recipe = ExportDojoRecipes.research[data.RecipeType]; - guild.TechProjects ??= []; - if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { - const techProject = - guild.TechProjects[ - guild.TechProjects.push({ - ItemType: data.RecipeType, - ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), - ReqItems: recipe.ingredients.map(x => ({ - ItemType: x.ItemType, - ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) - })), - State: 0 - }) - 1 - ]; - setGuildTechLogState(guild, techProject.ItemType, 5); - if (config.noDojoResearchCosts) { - processFundedGuildTechProject(guild, techProject, recipe); - } else { - if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { - guild.ActiveDojoColorResearch = data.RecipeType; + if (data.Mode == "Guild") { + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { + res.status(400).send("-1").end(); + return; + } + const recipe = ExportDojoRecipes.research[data.RecipeType]; + guild.TechProjects ??= []; + if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { + const techProject = + guild.TechProjects[ + guild.TechProjects.push({ + ItemType: data.RecipeType, + ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), + ReqItems: recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) + })), + State: 0 + }) - 1 + ]; + setGuildTechLogState(guild, techProject.ItemType, 5); + if (config.noDojoResearchCosts) { + processFundedGuildTechProject(guild, techProject, recipe); + } else { + if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { + guild.ActiveDojoColorResearch = data.RecipeType; + } } } + await guild.save(); + res.end(); + } else { + const recipe = ExportDojoRecipes.research[data.RecipeType]; + const techProject = + inventory.PersonalTechProjects[ + inventory.PersonalTechProjects.push({ + State: 0, + ReqCredits: recipe.price, + ItemType: data.RecipeType, + ReqItems: recipe.ingredients + }) - 1 + ]; + await inventory.save(); + res.json({ + isPersonal: true, + action: "Start", + personalTech: techProject.toJSON() + }); } - await guild.save(); - res.end(); } else if (data.Action == "Contribute") { - if (!hasAccessToDojo(inventory)) { - res.status(400).send("-1").end(); - return; - } + if ((req.query.guildId as string) == "000000000000000000000000") { + const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!; - const guildMember = (await GuildMember.findOne( - { accountId, guildId: guild._id }, - "RegularCreditsContributed MiscItemsContributed" - ))!; + techProject.ReqCredits -= data.RegularCredits; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); - const contributions = data; - const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; - - if (contributions.VaultCredits) { - if (contributions.VaultCredits > techProject.ReqCredits) { - contributions.VaultCredits = techProject.ReqCredits; - } - techProject.ReqCredits -= contributions.VaultCredits; - guild.VaultRegularCredits! -= contributions.VaultCredits; - } - - if (contributions.RegularCredits > techProject.ReqCredits) { - contributions.RegularCredits = techProject.ReqCredits; - } - techProject.ReqCredits -= contributions.RegularCredits; - - guildMember.RegularCreditsContributed ??= 0; - guildMember.RegularCreditsContributed += contributions.RegularCredits; - - if (contributions.VaultMiscItems.length) { - for (const miscItem of contributions.VaultMiscItems) { + const miscItemChanges = []; + for (const miscItem of data.MiscItems) { const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); if (reqItem) { if (miscItem.ItemCount > reqItem.ItemCount) { miscItem.ItemCount = reqItem.ItemCount; } reqItem.ItemCount -= miscItem.ItemCount; - - const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; - vaultMiscItem.ItemCount -= miscItem.ItemCount; + miscItemChanges.push({ + ItemType: miscItem.ItemType, + ItemCount: miscItem.ItemCount * -1 + }); } } - } + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; - const miscItemChanges = []; - for (const miscItem of contributions.MiscItems) { - const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); - if (reqItem) { - if (miscItem.ItemCount > reqItem.ItemCount) { - miscItem.ItemCount = reqItem.ItemCount; - } - reqItem.ItemCount -= miscItem.ItemCount; - miscItemChanges.push({ - ItemType: miscItem.ItemType, - ItemCount: miscItem.ItemCount * -1 - }); + techProject.HasContributions = true; - addGuildMemberMiscItemContribution(guildMember, miscItem); + if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { + techProject.State = 1; + const recipe = ExportDojoRecipes.research[techProject.ItemType]; + techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000); } + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + PersonalResearch: { $oid: data.ResearchId }, + PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined + }); + } else { + if (!hasAccessToDojo(inventory)) { + res.status(400).send("-1").end(); + return; + } + + const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed" + ))!; + + const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; + + if (data.VaultCredits) { + if (data.VaultCredits > techProject.ReqCredits) { + data.VaultCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= data.VaultCredits; + guild.VaultRegularCredits! -= data.VaultCredits; + } + + if (data.RegularCredits > techProject.ReqCredits) { + data.RegularCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= data.RegularCredits; + + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += data.RegularCredits; + + if (data.VaultMiscItems.length) { + for (const miscItem of data.VaultMiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + + const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; + vaultMiscItem.ItemCount -= miscItem.ItemCount; + } + } + } + + const miscItemChanges = []; + for (const miscItem of data.MiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + miscItemChanges.push({ + ItemType: miscItem.ItemType, + ItemCount: miscItem.ItemCount * -1 + }); + + addGuildMemberMiscItemContribution(guildMember, miscItem); + } + } + addMiscItems(inventory, miscItemChanges); + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); + inventoryChanges.MiscItems = miscItemChanges; + + // Check if research is fully funded now. + await processGuildTechProjectContributionsUpdate(guild, techProject); + + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); + res.json({ + InventoryChanges: inventoryChanges, + Vault: getGuildVault(guild) + }); } - addMiscItems(inventory, miscItemChanges); - const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); - inventoryChanges.MiscItems = miscItemChanges; - - // Check if research is fully funded now. - await processGuildTechProjectContributionsUpdate(guild, techProject); - - await Promise.all([guild.save(), inventory.save(), guildMember.save()]); - res.json({ - InventoryChanges: inventoryChanges, - Vault: getGuildVault(guild) - }); } else if (data.Action.split(",")[0] == "Buy") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { res.status(400).send("-1").end(); return; @@ -190,6 +250,7 @@ export const guildTechController: RequestHandler = async (req, res) => { } }); } else if (data.Action == "Fabricate") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { res.status(400).send("-1").end(); return; @@ -206,6 +267,7 @@ export const guildTechController: RequestHandler = async (req, res) => { // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. res.json({ inventoryChanges: inventoryChanges }); } else if (data.Action == "Pause") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { res.status(400).send("-1").end(); return; @@ -217,6 +279,7 @@ export const guildTechController: RequestHandler = async (req, res) => { await removePigmentsFromGuildMembers(guild._id); res.end(); } else if (data.Action == "Unpause") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { res.status(400).send("-1").end(); return; @@ -239,7 +302,7 @@ type TGuildTechRequest = interface IGuildTechBasicRequest { Action: "Start" | "Fabricate" | "Pause" | "Unpause"; - Mode: "Guild"; + Mode: "Guild" | "Personal"; RecipeType: string; } @@ -251,7 +314,7 @@ interface IGuildTechBuyRequest { interface IGuildTechContributeRequest { Action: "Contribute"; - ResearchId: ""; + ResearchId: string; RecipeType: string; RegularCredits: number; MiscItems: IMiscItem[]; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 32ce6dac..a0eddb10 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -84,7 +84,9 @@ import { IInfNode, IDiscoveredMarker, IWeeklyMission, - ILockedWeaponGroupDatabase + ILockedWeaponGroupDatabase, + IPersonalTechProjectDatabase, + IPersonalTechProjectClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -498,7 +500,34 @@ const seasonChallengeHistorySchema = new Schema( { _id: false } ); -//TODO: check whether this is complete +const personalTechProjectSchema = new Schema({ + State: Number, + ReqCredits: Number, + ItemType: String, + ReqItems: { type: [typeCountSchema], default: undefined }, + HasContributions: Boolean, + CompletionDate: Date +}); + +personalTechProjectSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() }; +}); + +personalTechProjectSchema.set("toJSON", { + virtuals: true, + transform(_doc, ret, _options) { + delete ret._id; + delete ret.__v; + + const db = ret as IPersonalTechProjectDatabase; + const client = ret as IPersonalTechProjectClient; + + if (db.CompletionDate) { + client.CompletionDate = toMongoDate(db.CompletionDate); + } + } +}); + const playerSkillsSchema = new Schema( { LPP_SPACE: { type: Number, default: 0 }, @@ -1442,7 +1471,7 @@ const inventorySchema = new Schema( //Railjack craft //https://warframe.fandom.com/wiki/Rising_Tide - PersonalTechProjects: [Schema.Types.Mixed], + PersonalTechProjects: { type: [personalTechProjectSchema], default: [] }, //Modulars lvl and exp(Railjack|Duviri) //https://warframe.fandom.com/wiki/Intrinsics @@ -1585,6 +1614,7 @@ export type InventoryDocumentProps = { Drones: Types.DocumentArray; CrewShipWeaponSkins: Types.DocumentArray; CrewShipSalvagedWeaponsSkins: Types.DocumentArray; + PersonalTechProjects: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 39894564..1146bef6 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -46,6 +46,7 @@ export interface IInventoryDatabase | "EntratiVaultCountResetDate" | "BrandedSuits" | "LockedWeaponGroup" + | "PersonalTechProjects" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -77,6 +78,7 @@ export interface IInventoryDatabase EntratiVaultCountResetDate?: Date; BrandedSuits?: Types.ObjectId[]; LockedWeaponGroup?: ILockedWeaponGroupDatabase; + PersonalTechProjects: IPersonalTechProjectDatabase[]; } export interface IQuestKeyDatabase { @@ -306,7 +308,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu NemesisHistory: INemesisBaseClient[]; LastNemesisAllySpawnTime?: IMongoDate; Settings?: ISettings; - PersonalTechProjects: IPersonalTechProject[]; + PersonalTechProjects: IPersonalTechProjectClient[]; PlayerSkills: IPlayerSkills; CrewShipAmmo: ITypeCount[]; CrewShipWeaponSkins: IUpgradeClient[]; @@ -941,16 +943,20 @@ export interface IPersonalGoalProgress { ReceivedClanReward1?: boolean; } -export interface IPersonalTechProject { +export interface IPersonalTechProjectDatabase { State: number; ReqCredits: number; ItemType: string; ReqItems: ITypeCount[]; + HasContributions?: boolean; + CompletionDate?: Date; +} + +export interface IPersonalTechProjectClient extends Omit { CompletionDate?: IMongoDate; - ItemId: IOid; ProductCategory?: string; CategoryItemId?: IOid; - HasContributions?: boolean; + ItemId: IOid; } export interface IPlayerSkills {