diff --git a/src/controllers/api/abortDojoComponentController.ts b/src/controllers/api/abortDojoComponentController.ts index 0ad1f074..3fcf770c 100644 --- a/src/controllers/api/abortDojoComponentController.ts +++ b/src/controllers/api/abortDojoComponentController.ts @@ -32,7 +32,7 @@ export const abortDojoComponentController: RequestHandler = async (req, res) => if (request.DecoId) { removeDojoDeco(guild, request.ComponentId, request.DecoId); } else { - removeDojoRoom(guild, request.ComponentId); + await removeDojoRoom(guild, request.ComponentId); } await guild.save(); diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 05859ad4..3177d5b6 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -94,10 +94,10 @@ const processContribution = ( component.RegularCredits += request.VaultCredits; guild.VaultRegularCredits! -= request.VaultCredits; } - if (component.RegularCredits > scaleRequiredCount(meta.price)) { + if (component.RegularCredits > scaleRequiredCount(guild.Tier, meta.price)) { guild.VaultRegularCredits ??= 0; - guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(meta.price); - component.RegularCredits = scaleRequiredCount(meta.price); + guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price); + component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price); } component.MiscItems ??= []; @@ -108,10 +108,10 @@ const processContribution = ( const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; if ( componentMiscItem.ItemCount + ingredientContribution.ItemCount > - scaleRequiredCount(ingredientMeta.ItemCount) + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) ) { ingredientContribution.ItemCount = - scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount; } componentMiscItem.ItemCount += ingredientContribution.ItemCount; } else { @@ -129,10 +129,10 @@ const processContribution = ( const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; if ( componentMiscItem.ItemCount + ingredientContribution.ItemCount > - scaleRequiredCount(ingredientMeta.ItemCount) + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) ) { ingredientContribution.ItemCount = - scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount; } componentMiscItem.ItemCount += ingredientContribution.ItemCount; } else { @@ -150,11 +150,14 @@ const processContribution = ( inventoryChanges.MiscItems = miscItemChanges; } - if (component.RegularCredits >= scaleRequiredCount(meta.price)) { + if (component.RegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { let fullyFunded = true; for (const ingredient of meta.ingredients) { const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType); - if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) { + if ( + !componentMiscItem || + componentMiscItem.ItemCount < scaleRequiredCount(guild.Tier, ingredient.ItemCount) + ) { fullyFunded = false; break; } diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index b1ac10d8..58079e02 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -1,5 +1,5 @@ import { GuildMember } from "@/src/models/guildModel"; -import { getGuildForRequestEx } from "@/src/services/guildService"; +import { addVaultMiscItems, getGuildForRequestEx } from "@/src/services/guildService"; import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -23,11 +23,17 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { guildMember.RegularCreditsContributed += request.RegularCredits; } if (request.MiscItems.length) { - guild.VaultMiscItems ??= []; + addVaultMiscItems(guild, request.MiscItems); + guildMember.MiscItemsContributed ??= []; for (const item of request.MiscItems) { - guild.VaultMiscItems.push(item); - guildMember.MiscItemsContributed.push(item); + const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType); + if (miscItemContribution) { + miscItemContribution.ItemCount += item.ItemCount; + } else { + guildMember.MiscItemsContributed.push(item); + } + addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index 5f334356..33aec126 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -1,4 +1,4 @@ -import { GuildMember } from "@/src/models/guildModel"; +import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; @@ -36,10 +36,10 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { if (request.DecoId) { const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; - processContribution(deco, meta, platinumDonated); + processContribution(guild, deco, meta, platinumDonated); } else { const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; - processContribution(component, meta, platinumDonated); + processContribution(guild, component, meta, platinumDonated); const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id)); if (entry) { @@ -61,8 +61,13 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { }); }; -const processContribution = (component: IDojoContributable, meta: IDojoBuild, platinumDonated: number): void => { - const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice); +const processContribution = ( + guild: TGuildDatabaseDocument, + component: IDojoContributable, + meta: IDojoBuild, + platinumDonated: number +): void => { + const fullPlatinumCost = scaleRequiredCount(guild.Tier, meta.skipTimePrice); const fullDurationSeconds = meta.time; const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; component.CompletionTime = new Date( diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 58c505b0..35b4d626 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -4,10 +4,13 @@ import { getGuildVault, hasAccessToDojo, hasGuildPermission, + processFundedGuildTechProject, + processGuildTechProjectContributionsUpdate, removePigmentsFromGuildMembers, - scaleRequiredCount + scaleRequiredCount, + setGuildTechLogState } from "@/src/services/guildService"; -import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addItem, @@ -20,8 +23,8 @@ import { import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; -import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; -import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes"; +import { GuildMember } from "@/src/models/guildModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; @@ -44,7 +47,7 @@ export const guildTechController: RequestHandler = async (req, res) => { if (project.CompletionDate) { techProject.CompletionDate = toMongoDate(project.CompletionDate); if (Date.now() >= project.CompletionDate.getTime()) { - needSave ||= setTechLogState(guild, project.ItemType, 4, project.CompletionDate); + needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate); } } techProjects.push(techProject); @@ -66,17 +69,17 @@ export const guildTechController: RequestHandler = async (req, res) => { guild.TechProjects[ guild.TechProjects.push({ ItemType: data.RecipeType, - ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(recipe.price), + ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), ReqItems: recipe.ingredients.map(x => ({ ItemType: x.ItemType, - ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(x.ItemCount) + ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) })), State: 0 }) - 1 ]; - setTechLogState(guild, techProject.ItemType, 5); + setGuildTechLogState(guild, techProject.ItemType, 5); if (config.noDojoResearchCosts) { - processFundedProject(guild, techProject, recipe); + processFundedGuildTechProject(guild, techProject, recipe); } else { if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { guild.ActiveDojoColorResearch = data.RecipeType; @@ -151,15 +154,8 @@ export const guildTechController: RequestHandler = async (req, res) => { const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); inventoryChanges.MiscItems = miscItemChanges; - if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { - // This research is now fully funded. - const recipe = ExportDojoRecipes.research[data.RecipeType]; - processFundedProject(guild, techProject, recipe); - if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { - guild.ActiveDojoColorResearch = ""; - await removePigmentsFromGuildMembers(guild._id); - } - } + // Check if research is fully funded now. + await processGuildTechProjectContributionsUpdate(guild, techProject); await guild.save(); await inventory.save(); @@ -238,43 +234,6 @@ export const guildTechController: RequestHandler = async (req, res) => { } }; -const processFundedProject = ( - guild: TGuildDatabaseDocument, - techProject: ITechProjectDatabase, - recipe: IDojoResearch -): void => { - techProject.State = 1; - techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); - if (recipe.guildXpValue) { - guild.XP += recipe.guildXpValue; - } - setTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate); -}; - -const setTechLogState = ( - guild: TGuildDatabaseDocument, - type: string, - state: number, - dateTime: Date | undefined = undefined -): boolean => { - guild.TechChanges ??= []; - const entry = guild.TechChanges.find(x => x.details == type); - if (entry) { - if (entry.entryType == state) { - return false; - } - entry.dateTime = dateTime; - entry.entryType = state; - } else { - guild.TechChanges.push({ - dateTime: dateTime, - entryType: state, - details: type - }); - } - return true; -}; - type TGuildTechRequest = | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" } | IGuildTechBasicRequest diff --git a/src/services/guildService.ts b/src/services/guildService.ts index cdaf52fd..19bbf803 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -13,16 +13,18 @@ import { IGuildClient, IGuildMemberClient, IGuildMemberDatabase, - IGuildVault + IGuildVault, + ITechProjectDatabase } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { Types } from "mongoose"; -import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; +import { ExportDojoRecipes, IDojoBuild, IDojoResearch } from "warframe-public-export-plus"; import { logger } from "../utils/logger"; import { config } from "./configService"; import { Account } from "../models/loginModel"; import { getRandomInt } from "./rngService"; import { Inbox } from "../models/inboxModel"; +import { ITypeCount } from "../types/inventoryTypes/inventoryTypes"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -152,6 +154,27 @@ export const getDojoClient = async ( entry.entryType = 1; needSave = true; } + + let newTier: number | undefined; + switch (dojoComponent.pf) { + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksShadow.level": + newTier = 2; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksStorm.level": + newTier = 3; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMountain.level": + newTier = 4; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMoon.level": + newTier = 5; + break; + } + if (newTier) { + logger.debug(`clan finished building barracks, updating to tier ${newTier}`); + await setGuildTier(guild, newTier); + needSave = true; + } } if (dojoComponent.DestructionTime) { if (Date.now() >= dojoComponent.DestructionTime.getTime()) { @@ -189,22 +212,26 @@ export const getDojoClient = async ( if (roomsToRemove.length) { logger.debug(`removing now-destroyed rooms`, roomsToRemove); for (const id of roomsToRemove) { - removeDojoRoom(guild, id); + await removeDojoRoom(guild, id); } needSave = true; } if (needSave) { await guild.save(); } + dojo.Tier = guild.Tier; 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)); +const guildTierScalingFactors = [0.01, 0.03, 0.1, 0.3, 1]; +export const scaleRequiredCount = (tier: number, count: number): number => { + return Math.max(1, Math.trunc(count * guildTierScalingFactors[tier - 1])); }; -export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: Types.ObjectId | string): void => { +export const removeDojoRoom = async ( + guild: TGuildDatabaseDocument, + componentId: Types.ObjectId | string +): Promise => { const component = guild.DojoComponents.splice( guild.DojoComponents.findIndex(x => x._id.equals(componentId)), 1 @@ -223,6 +250,21 @@ export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: Types guild.RoomChanges.splice(index, 1); } } + + switch (component.pf) { + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksShadow.level": + await setGuildTier(guild, 1); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksStorm.level": + await setGuildTier(guild, 2); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMountain.level": + await setGuildTier(guild, 3); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMoon.level": + await setGuildTier(guild, 4); + break; + } }; export const removeDojoDeco = ( @@ -248,15 +290,7 @@ const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoCon guild.VaultRegularCredits += component.RegularCredits; } if (component.MiscItems) { - guild.VaultMiscItems ??= []; - for (const componentMiscItem of component.MiscItems) { - const vaultMiscItem = guild.VaultMiscItems.find(x => x.ItemType == componentMiscItem.ItemType); - if (vaultMiscItem) { - vaultMiscItem.ItemCount += componentMiscItem.ItemCount; - } else { - guild.VaultMiscItems.push(componentMiscItem); - } - } + addVaultMiscItems(guild, component.MiscItems); } if (component.RushPlatinum) { guild.VaultPremiumCredits ??= 0; @@ -264,6 +298,18 @@ const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoCon } }; +export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITypeCount[]): void => { + guild.VaultMiscItems ??= []; + for (const miscItem of miscItems) { + const vaultMiscItem = guild.VaultMiscItems.find(x => x.ItemType == miscItem.ItemType); + if (vaultMiscItem) { + vaultMiscItem.ItemCount += miscItem.ItemCount; + } else { + guild.VaultMiscItems.push(miscItem); + } + } +}; + export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { if (build.guildXpValue) { guild.ClaimedXP ??= []; @@ -362,6 +408,102 @@ export const removePigmentsFromGuildMembers = async (guildId: string | Types.Obj } }; +export const processGuildTechProjectContributionsUpdate = async ( + guild: TGuildDatabaseDocument, + techProject: ITechProjectDatabase +): Promise => { + if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { + // This research is now fully funded. + + if ( + techProject.State == 0 && + techProject.ItemType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/" + ) { + guild.ActiveDojoColorResearch = ""; + await removePigmentsFromGuildMembers(guild._id); + } + + const recipe = ExportDojoRecipes.research[techProject.ItemType]; + processFundedGuildTechProject(guild, techProject, recipe); + } +}; + +export const processFundedGuildTechProject = ( + guild: TGuildDatabaseDocument, + techProject: ITechProjectDatabase, + recipe: IDojoResearch +): void => { + techProject.State = 1; + techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); + if (recipe.guildXpValue) { + guild.XP += recipe.guildXpValue; + } + setGuildTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate); +}; + +export const setGuildTechLogState = ( + guild: TGuildDatabaseDocument, + type: string, + state: number, + dateTime: Date | undefined = undefined +): boolean => { + guild.TechChanges ??= []; + const entry = guild.TechChanges.find(x => x.details == type); + if (entry) { + if (entry.entryType == state) { + return false; + } + entry.dateTime = dateTime; + entry.entryType = state; + } else { + guild.TechChanges.push({ + dateTime: dateTime, + entryType: state, + details: type + }); + } + return true; +}; + +const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Promise => { + const oldTier = guild.Tier; + guild.Tier = newTier; + if (guild.TechProjects) { + for (const project of guild.TechProjects) { + if (project.State == 1) { + continue; + } + + const meta = ExportDojoRecipes.research[project.ItemType]; + + { + const numContributed = scaleRequiredCount(oldTier, meta.price) - project.ReqCredits; + project.ReqCredits = scaleRequiredCount(newTier, meta.price) - numContributed; + if (project.ReqCredits < 0) { + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += project.ReqCredits * -1; + project.ReqCredits = 0; + } + } + + for (let i = 0; i != project.ReqItems.length; ++i) { + const numContributed = + scaleRequiredCount(oldTier, meta.ingredients[i].ItemCount) - project.ReqItems[i].ItemCount; + project.ReqItems[i].ItemCount = + scaleRequiredCount(newTier, meta.ingredients[i].ItemCount) - numContributed; + if (project.ReqItems[i].ItemCount < 0) { + project.ReqItems[i].ItemCount *= -1; + addVaultMiscItems(guild, [project.ReqItems[i]]); + project.ReqItems[i].ItemCount = 0; + } + } + + // Check if research is fully funded now due to lowered requirements. + await processGuildTechProjectContributionsUpdate(guild, project); + } + } +}; + export const deleteGuild = async (guildId: Types.ObjectId): Promise => { await Guild.deleteOne({ _id: guildId }); await GuildMember.deleteMany({ guildId });