diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 9e4243da..951291c0 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,5 +1,92 @@ import { RequestHandler } from "express"; +import { getGuildForRequestEx } from "@/src/services/guildService"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; -export const guildTechController: RequestHandler = (_req, res) => { - res.status(500).end(); // This is what I got for a fresh clan. +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") { + res.json({ + TechProjects: guild.toJSON().TechProjects + }); + } else if (data.Action == "Start") { + const recipe = ExportDojoRecipes.research[data.RecipeType!]; + guild.TechProjects ??= []; + guild.TechProjects.push({ + ItemType: data.RecipeType!, + ReqCredits: scaleRequiredCount(recipe.price), + ReqItems: recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: scaleRequiredCount(x.ItemCount) + })), + State: 0 + }); + await guild.save(); + res.end(); + } else if (data.Action == "Contribute") { + const contributions = data as IGuildTechContributeFields; + const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; + techProject.ReqCredits -= contributions.RegularCredits; + const miscItemChanges = []; + for (const miscItem of contributions.MiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + reqItem.ItemCount -= miscItem.ItemCount; + miscItemChanges.push({ + ItemType: miscItem.ItemType, + ItemCount: miscItem.ItemCount * -1 + }); + } + } + addMiscItems(inventory, miscItemChanges); + const inventoryChanges: IInventoryChanges = { + ...updateCurrency(inventory, contributions.RegularCredits, false), + MiscItems: miscItemChanges + }; + + if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { + // This research is now fully funded. + techProject.State = 1; + const recipe = ExportDojoRecipes.research[data.RecipeType!]; + techProject.CompletionDate = new Date(new Date().getTime() + recipe.time * 1000); + } + + await guild.save(); + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); + } else { + throw new Error(`unknown guildTech action: ${data.Action}`); + } +}; + +type TGuildTechRequest = { + Action: string; +} & Partial & + Partial; + +interface IGuildTechStartFields { + Mode: "Guild"; + RecipeType: string; +} + +interface IGuildTechContributeFields { + ResearchId: ""; + RecipeType: string; + RegularCredits: number; + MiscItems: IMiscItem[]; + 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/models/guildModel.ts b/src/models/guildModel.ts index 22269ec8..ab87e719 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -1,5 +1,12 @@ -import { IGuildDatabase, IDojoComponentDatabase } from "@/src/types/guildTypes"; +import { + IGuildDatabase, + IDojoComponentDatabase, + ITechProjectDatabase, + ITechProjectClient +} from "@/src/types/guildTypes"; import { model, Schema } from "mongoose"; +import { typeCountSchema } from "./inventoryModels/inventoryModel"; +import { toMongoDate } from "../helpers/inventoryHelpers"; const dojoComponentSchema = new Schema({ pf: { type: String, required: true }, @@ -10,12 +17,35 @@ const dojoComponentSchema = new Schema({ CompletionTime: Date }); +const techProjectSchema = new Schema( + { + ItemType: String, + ReqCredits: Number, + ReqItems: [typeCountSchema], + State: Number, + CompletionDate: Date + }, + { _id: false } +); + +techProjectSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as ITechProjectDatabase; + const client = obj as ITechProjectClient; + if (db.CompletionDate) { + client.CompletionDate = toMongoDate(db.CompletionDate); + } + } +}); + const guildSchema = new Schema( { Name: { type: String, required: true }, DojoComponents: [dojoComponentSchema], DojoCapacity: { type: Number, default: 100 }, - DojoEnergy: { type: Number, default: 5 } + DojoEnergy: { type: Number, default: 5 }, + TechProjects: { type: [techProjectSchema], default: undefined } }, { id: false } ); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 5e5a1680..868a2491 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -61,7 +61,7 @@ import { import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { EquipmentSelectionSchema } from "./loadoutModel"; -const typeCountSchema = new Schema({ ItemType: String, ItemCount: Number }, { _id: false }); +export const typeCountSchema = new Schema({ ItemType: String, ItemCount: Number }, { _id: false }); const focusXPSchema = new Schema( { diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 35b697fc..250408b9 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -2,10 +2,15 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory } from "@/src/services/inventoryService"; import { Guild } from "@/src/models/guildModel"; +import { IInventoryDatabaseDocument } from "../types/inventoryTypes/inventoryTypes"; export const getGuildForRequest = async (req: Request) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); + return await getGuildForRequestEx(req, inventory); +}; + +export const getGuildForRequestEx = async (req: Request, inventory: IInventoryDatabaseDocument) => { const guildId = req.query.guildId as string; if (!inventory.GuildId || inventory.GuildId.toString() != guildId) { throw new Error("Account is not in the guild that it has sent a request for"); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index edd7aa0c..1778582f 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -11,6 +11,7 @@ export interface IGuildDatabase extends IGuild { DojoComponents?: IDojoComponentDatabase[]; DojoCapacity: number; DojoEnergy: number; + TechProjects?: ITechProjectDatabase[]; } export interface IDojoClient { @@ -45,3 +46,15 @@ export interface IDojoComponentDatabase pi?: Types.ObjectId; CompletionTime?: Date; } + +export interface ITechProjectClient { + ItemType: string; + ReqCredits: number; + ReqItems: IMiscItem[]; + State: number; // 0 = pending, 1 = complete + CompletionDate?: IMongoDate; +} + +export interface ITechProjectDatabase extends Omit { + CompletionDate?: Date; +}