From bf04755c36ef20e2e4a7c2123df34aa0dc7a71db Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 15 Aug 2025 08:14:36 -0700 Subject: [PATCH] feat: belly of the beast / eight claw (#2621) Re #1103 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2621 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- config-vanilla.json | 4 + src/controllers/api/getGuildEventScore.ts | 25 +++++ src/models/guildModel.ts | 27 +++++- src/models/inventoryModels/inventoryModel.ts | 10 +- src/routes/api.ts | 2 + src/services/configService.ts | 4 + src/services/guildService.ts | 91 ++++++++++++++++++- src/services/missionInventoryUpdateService.ts | 14 ++- src/services/purchaseService.ts | 19 ++++ src/services/worldStateService.ts | 43 +++++++++ src/types/guildTypes.ts | 11 ++- src/types/inventoryTypes/inventoryTypes.ts | 10 +- src/types/worldStateTypes.ts | 26 ++++-- static/webui/index.html | 26 ++++++ static/webui/translations/de.js | 4 + static/webui/translations/en.js | 4 + static/webui/translations/es.js | 4 + static/webui/translations/fr.js | 4 + static/webui/translations/ru.js | 4 + static/webui/translations/uk.js | 4 + static/webui/translations/zh.js | 4 + 21 files changed, 320 insertions(+), 20 deletions(-) create mode 100644 src/controllers/api/getGuildEventScore.ts diff --git a/config-vanilla.json b/config-vanilla.json index 4d18edcc..595bce17 100644 --- a/config-vanilla.json +++ b/config-vanilla.json @@ -79,6 +79,10 @@ "starDaysOverride": null, "dogDaysOverride": null, "dogDaysRewardsOverride": null, + "bellyOfTheBeast": false, + "bellyOfTheBeastProgressOverride": 0, + "eightClaw": false, + "eightClawProgressOverride": 0, "eidolonOverride": "", "vallisOverride": "", "duviriOverride": "", diff --git a/src/controllers/api/getGuildEventScore.ts b/src/controllers/api/getGuildEventScore.ts new file mode 100644 index 00000000..7e488bd4 --- /dev/null +++ b/src/controllers/api/getGuildEventScore.ts @@ -0,0 +1,25 @@ +import { RequestHandler } from "express"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { getInventory } from "@/src/services/inventoryService"; +import { Guild } from "@/src/models/guildModel"; + +export const getGuildEventScoreController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString(), "GuildId"); + const guild = await Guild.findById(inventory.GuildId); + const goalId = req.query.goalId as string; + if (guild && guild.GoalProgress && goalId) { + const goal = guild.GoalProgress.find(x => x.goalId.toString() == goalId); + if (goal) { + return res.json({ + Tier: guild.Tier, + GoalProgress: { + Count: goal.Count, + Tag: goal.Tag, + _id: { $oid: goal.goalId } + } + }); + } + } + return res.json({}); +}; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 75492bc2..eb005144 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -19,6 +19,8 @@ import { import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "@/src/models/inventoryModels/inventoryModel"; import { pictureFrameInfoSchema } from "@/src/models/personalRoomsModel"; +import { IGoalProgressClient, IGoalProgressDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; +import { toOid } from "@/src/helpers/inventoryHelpers"; const dojoDecoSchema = new Schema({ Type: String, @@ -174,6 +176,28 @@ const guildLogEntryNumberSchema = new Schema( { _id: false } ); +const goalProgressSchema = new Schema( + { + Count: Number, + Tag: String, + goalId: Types.ObjectId + }, + { _id: false } +); + +goalProgressSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj: Record) { + const db = obj as IGoalProgressDatabase; + const client = obj as IGoalProgressClient; + + client._id = toOid(db.goalId); + + delete obj.goalId; + delete obj.__v; + } +}); + const guildSchema = new Schema( { Name: { type: String, required: true, unique: true }, @@ -206,7 +230,8 @@ const guildSchema = new Schema( RoomChanges: { type: [guildLogRoomChangeSchema], default: undefined }, TechChanges: { type: [guildLogEntryContributableSchema], default: undefined }, RosterActivity: { type: [guildLogEntryRosterSchema], default: undefined }, - ClassChanges: { type: [guildLogEntryNumberSchema], default: undefined } + ClassChanges: { type: [guildLogEntryNumberSchema], default: undefined }, + GoalProgress: { type: [goalProgressSchema], default: undefined } }, { id: false } ); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index b11135a0..5d4bf32e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -85,8 +85,8 @@ import { IAccolades, IHubNpcCustomization, IEndlessXpReward, - IPersonalGoalProgressDatabase, - IPersonalGoalProgressClient, + IGoalProgressDatabase, + IGoalProgressClient, IKubrowPetPrintClient, IKubrowPetPrintDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -445,7 +445,7 @@ const discoveredMarkerSchema = new Schema( { _id: false } ); -const personalGoalProgressSchema = new Schema( +const personalGoalProgressSchema = new Schema( { Best: Number, Count: Number, @@ -458,8 +458,8 @@ const personalGoalProgressSchema = new Schema( personalGoalProgressSchema.set("toJSON", { virtuals: true, transform(_doc, obj: Record) { - const db = obj as IPersonalGoalProgressDatabase; - const client = obj as IPersonalGoalProgressClient; + const db = obj as IGoalProgressDatabase; + const client = obj as IGoalProgressClient; client._id = toOid(db.goalId); diff --git a/src/routes/api.ts b/src/routes/api.ts index e0d3b712..f2c2da95 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -62,6 +62,7 @@ import { getFriendsController } from "@/src/controllers/api/getFriendsController import { getGuildContributionsController } from "@/src/controllers/api/getGuildContributionsController"; import { getGuildController } from "@/src/controllers/api/getGuildController"; import { getGuildDojoController } from "@/src/controllers/api/getGuildDojoController"; +import { getGuildEventScoreController } from "@/src/controllers/api/getGuildEventScore"; import { getGuildLogController } from "@/src/controllers/api/getGuildLogController"; import { getIgnoredUsersController } from "@/src/controllers/api/getIgnoredUsersController"; import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSeedController"; @@ -192,6 +193,7 @@ apiRouter.get("/getFriends.php", getFriendsController); apiRouter.get("/getGuild.php", getGuildController); apiRouter.get("/getGuildContributions.php", getGuildContributionsController); apiRouter.get("/getGuildDojo.php", getGuildDojoController); +apiRouter.get("/getGuildEventScore.php", getGuildEventScoreController); apiRouter.get("/getGuildLog.php", getGuildLogController); apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController); apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17 diff --git a/src/services/configService.ts b/src/services/configService.ts index b84975f3..997276e7 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -91,6 +91,10 @@ export interface IConfig { starDaysOverride?: boolean; dogDaysOverride?: boolean; dogDaysRewardsOverride?: number; + bellyOfTheBeast?: boolean; + bellyOfTheBeastProgressOverride?: number; + eightClaw?: boolean; + eightClawProgressOverride?: number; eidolonOverride?: string; vallisOverride?: string; duviriOverride?: string; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 6d60f0c2..da80d2aa 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -115,7 +115,14 @@ export const getGuildClient = async ( NumContributors: guild.CeremonyContributors?.length ?? 0, CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, AutoContributeFromVault: guild.AutoContributeFromVault, - AllianceId: guild.AllianceId ? toOid2(guild.AllianceId, account.BuildLabel) : undefined + AllianceId: guild.AllianceId ? toOid2(guild.AllianceId, account.BuildLabel) : undefined, + GoalProgress: guild.GoalProgress + ? guild.GoalProgress.map(gp => ({ + Count: gp.Count, + Tag: gp.Tag, + _id: { $oid: gp.goalId.toString() } + })) + : undefined }; }; @@ -809,3 +816,85 @@ export const getAllianceClient = async ( } }; }; + +export const handleGuildGoalProgress = async ( + guild: TGuildDatabaseDocument, + upload: { Count: number; Tag: string; goalId: Types.ObjectId } +): Promise => { + guild.GoalProgress ??= []; + const goalProgress = guild.GoalProgress.find(x => x.goalId.equals(upload.goalId)); + if (!goalProgress) { + guild.GoalProgress.push({ + Count: upload.Count, + Tag: upload.Tag, + goalId: upload.goalId + }); + } + const totalCount = (goalProgress?.Count ?? 0) + upload.Count; + const guildRewards = goalGuildRewardByTag[upload.Tag].rewards; + const tierGoals = goalGuildRewardByTag[upload.Tag].guildGoals[guild.Tier - 1]; + const rewards = []; + if (tierGoals.length && guildRewards.length) { + for (let i = 0; i < tierGoals.length; i++) { + if ( + tierGoals[i] && + tierGoals[i] <= totalCount && + (!goalProgress || goalProgress.Count < tierGoals[i]) && + guildRewards[i] + ) { + rewards.push(guildRewards[i]); + } + } + + if (rewards.length) { + logger.debug(`guild goal rewards`, rewards); + guild.VaultDecoRecipes ??= []; + rewards.forEach(type => { + guild.VaultDecoRecipes!.push({ + ItemType: type, + ItemCount: 1 + }); + }); + } + } + + if (goalProgress) { + goalProgress.Count += upload.Count; + } + await guild.save(); +}; + +export const goalGuildRewardByTag: Record = { + JadeShadowsEvent: { + guildGoals: [ + // I don't know what ClanGoal means + [15, 30, 45, 60], + [45, 90, 135, 180], + [150, 300, 450, 600], + [450, 900, 1350, 1800], + [1500, 3000, 4500, 6000] + ], + rewards: [ + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventPewterTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventGoldTrophyRecipe" + ] + }, + DuviriMurmurEvent: { + guildGoals: [ + // I don't know what ClanGoal means + [260, 519, 779, 1038], + [779, 1557, 2336, 3114], + [2595, 5190, 7785, 10380], + [7785, 15570, 23355, 31140], + [29950, 51900, 77850, 103800] + ], + rewards: [ + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventGoldTrophyRecipe" + ] + } +}; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index eea01155..8fb17b59 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -81,6 +81,8 @@ import { fromOid } from "@/src/helpers/inventoryHelpers"; import { TAccountDocument } from "@/src/services/loginService"; import { ITypeCount } from "@/src/types/commonTypes"; import { IEquipmentClient } from "@/src/types/equipmentTypes"; +import { Guild } from "@/src/models/guildModel"; +import { handleGuildGoalProgress } from "@/src/services/guildService"; const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { // Disruption missions just tell us (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2599) @@ -712,10 +714,20 @@ export const addMissionInventoryUpdates = async ( } if (goalProgress) { - goalProgress.Best = Math.max(goalProgress.Best, uploadProgress.Best); + goalProgress.Best = Math.max(goalProgress.Best!, uploadProgress.Best); goalProgress.Count += uploadProgress.Count; } } + if (goal && goal.ClanGoal && inventory.GuildId) { + const guild = await Guild.findById(inventory.GuildId, "GoalProgress Tier VaultDecoRecipes"); + if (guild) { + await handleGuildGoalProgress(guild, { + Count: uploadProgress.Count, + Tag: goal.Tag, + goalId: new Types.ObjectId(goal._id.$oid) + }); + } + } } break; } diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 3b4dd121..11d3de82 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -36,6 +36,9 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento import { fromStoreItem, toStoreItem } from "@/src/services/itemDataService"; import { DailyDeal } from "@/src/models/worldStateModel"; import { fromMongoDate, toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { handleGuildGoalProgress } from "@/src/services/guildService"; +import { Types } from "mongoose"; export const getStoreItemCategory = (storeItem: string): string => { const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/"); @@ -137,6 +140,22 @@ export const handlePurchase = async ( updateCurrency(inventory, offer.PremiumPrice[0], true, prePurchaseInventoryChanges); } } + if ( + inventory.GuildId && + offer.ItemPrices && + manifest.VendorInfo.TypeName == + "/Lotus/Types/Game/VendorManifests/Events/DuviriMurmurInvasionVendorManifest" + ) { + const guild = await Guild.findById(inventory.GuildId, "GoalProgress Tier VaultDecoRecipes"); + const goal = getWorldState().Goals.find(x => x.Tag == "DuviriMurmurEvent"); + if (guild && goal) { + await handleGuildGoalProgress(guild, { + Count: offer.ItemPrices[0].ItemCount * purchaseRequest.PurchaseParams.Quantity, + Tag: goal.Tag, + goalId: new Types.ObjectId(goal._id.$oid) + }); + } + } if (!config.dontSubtractPurchaseItemCost) { if (offer.ItemPrices) { handleItemPrices( diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 9520fe54..b929331d 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -2265,6 +2265,49 @@ export const getWorldState = (buildLabel?: string): IWorldState => { BonusReward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"] } }); } + if (config.worldState?.bellyOfTheBeast) { + worldState.Goals.push({ + _id: { $oid: "67a5035c2a198564d62e165e" }, + Activation: { $date: { $numberLong: "1738868400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: config.worldState.bellyOfTheBeastProgressOverride ?? 0, + HealthPct: (config.worldState.bellyOfTheBeastProgressOverride ?? 0) / 100, + Goal: 0, + Personal: true, + Community: true, + ClanGoal: [72, 216, 648, 1944, 5832], + Tag: "JadeShadowsEvent", + Faction: "FC_MITW", + Desc: "/Lotus/Language/JadeShadows/JadeShadowsEventName", + ToolTip: "/Lotus/Language/JadeShadows/JadeShadowsShortEventDesc", + Icon: "/Lotus/Interface/Icons/WorldStatePanel/JadeShadowsEventBadge.png", + ScoreLocTag: "/Lotus/Language/JadeShadows/JadeShadowsEventScore", + Node: "SolNode723", + MissionKeyName: "/Lotus/Types/Keys/JadeShadowsEventMission", + ItemType: "/Lotus/Types/Gameplay/JadeShadows/Resources/AscensionEventResourceItem" + }); + } + if (config.worldState?.eightClaw) { + worldState.Goals.push({ + _id: { $oid: "685c15f80000000000000000" }, + Activation: { $date: { $numberLong: "1750865400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: config.worldState.eightClawProgressOverride ?? 0, + HealthPct: (config.worldState.eightClawProgressOverride ?? 0) / 100, + Goal: 0, + Personal: true, + Community: true, + ClanGoal: [72, 216, 648, 1944, 5832], + Tag: "DuviriMurmurEvent", + Faction: "FC_MITW", + Desc: "/Lotus/Language/Isleweaver/DuviriMurmurEventTitle", + ToolTip: "/Lotus/Language/Isleweaver/DuviriMurmurEventDescription", + Icon: "/Lotus/Interface/Icons/WorldStatePanel/EightClawEventBadge.png", + ScoreLocTag: "/Lotus/Language/Isleweaver/DuviriMurmurEventScore", + Node: "SolNode236", + MissionKeyName: "/Lotus/Types/Keys/DuviriMITW/DuviriMITWEventKey" + }); + } // Nightwave Challenges const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index ec1b4899..791099c3 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -1,6 +1,11 @@ import { Types } from "mongoose"; import { IOid, IMongoDate, IOidWithLegacySupport, ITypeCount } from "@/src/types/commonTypes"; -import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { + IFusionTreasure, + IMiscItem, + IGoalProgressDatabase, + IGoalProgressClient +} from "@/src/types/inventoryTypes/inventoryTypes"; import { IPictureFrameInfo } from "@/src/types/personalRoomsTypes"; import { IFriendInfo } from "@/src/types/friendTypes"; @@ -23,6 +28,8 @@ export interface IGuildClient { CrossPlatformEnabled?: boolean; AutoContributeFromVault?: boolean; AllianceId?: IOidWithLegacySupport; + + GoalProgress?: IGoalProgressClient[]; } export interface IGuildDatabase { @@ -63,6 +70,8 @@ export interface IGuildDatabase { TechChanges?: IGuildLogEntryContributable[]; RosterActivity?: IGuildLogEntryRoster[]; ClassChanges?: IGuildLogEntryNumber[]; + + GoalProgress?: IGoalProgressDatabase[]; } export interface ILongMOTD { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index f08e0728..ebe999fc 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -109,7 +109,7 @@ export interface IInventoryDatabase QualifyingInvasions: IInvasionProgressDatabase[]; LastInventorySync?: Types.ObjectId; EndlessXP?: IEndlessXpProgressDatabase[]; - PersonalGoalProgress?: IPersonalGoalProgressDatabase[]; + PersonalGoalProgress?: IGoalProgressDatabase[]; } export interface IQuestKeyDatabase { @@ -318,7 +318,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu HWIDProtectEnabled?: boolean; KubrowPetPrints: IKubrowPetPrintClient[]; AlignmentReplay?: IAlignment; - PersonalGoalProgress?: IPersonalGoalProgressClient[]; + PersonalGoalProgress?: IGoalProgressClient[]; ThemeStyle: string; ThemeBackground: string; ThemeSounds: string; @@ -895,8 +895,8 @@ export interface IPeriodicMissionCompletionResponse extends Omit { +export interface IGoalProgressDatabase extends Omit { goalId: Types.ObjectId; } diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index b390f40d..84054bbb 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -39,32 +39,44 @@ export interface IGoal { _id: IOid; Activation: IMongoDate; Expiry: IMongoDate; + Count?: number; + HealthPct?: number; + + Icon: string; + Desc: string; + ToolTip?: string; + Faction?: string; + Goal?: number; InterimGoals?: number[]; BonusGoal?: number; - HealthPct?: number; + ClanGoal?: number[]; + Success?: number; Personal?: boolean; - Best?: boolean; + Community?: boolean; + Best?: boolean; // Fist one on Event Tab Bounty?: boolean; // Tactical Alert - Faction?: string; ClampNodeScores?: boolean; - Desc: string; - ToolTip?: string; + Transmission?: string; InstructionalItem?: string; - Icon: string; + ItemType?: string; + Tag: string; PrereqGoalTags?: string[]; + Node?: string; VictimNode?: string; + ConcurrentMissionKeyNames?: string[]; ConcurrentNodeReqs?: number[]; ConcurrentNodes?: string[]; RegionIdx?: number; Regions?: number[]; MissionKeyName?: string; + Reward?: IMissionReward; InterimRewards?: IMissionReward[]; BonusReward?: IMissionReward; @@ -77,6 +89,8 @@ export interface IGoal { ScoreVar?: string; ScoreMaxTag?: string; + ScoreLocTag?: string; + NightLevel?: string; } diff --git a/static/webui/index.html b/static/webui/index.html index 5a9707be..374459d8 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -1040,6 +1040,32 @@ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+