From ed82225ec19a3b57a363a9bd05d3ceacaf58f5b9 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 11 Aug 2025 20:17:20 +0200 Subject: [PATCH] feat: dog days Re #1103 --- config-vanilla.json | 2 + src/controllers/api/inboxController.ts | 6 +- src/models/inboxModel.ts | 4 +- src/services/configService.ts | 2 + src/services/missionInventoryUpdateService.ts | 122 ++++++- src/services/worldStateService.ts | 297 +++++++++++++++++- src/types/worldStateTypes.ts | 43 +++ static/webui/index.html | 20 ++ static/webui/translations/de.js | 6 + static/webui/translations/en.js | 6 + static/webui/translations/es.js | 6 + static/webui/translations/fr.js | 6 + static/webui/translations/ru.js | 6 + static/webui/translations/uk.js | 6 + static/webui/translations/zh.js | 6 + 15 files changed, 518 insertions(+), 20 deletions(-) diff --git a/config-vanilla.json b/config-vanilla.json index a8834907..36e4b388 100644 --- a/config-vanilla.json +++ b/config-vanilla.json @@ -75,6 +75,8 @@ "ghoulEmergenceOverride": null, "plagueStarOverride": null, "starDaysOverride": null, + "waterFightOverride": null, + "waterFightRewardsOverride": null, "eidolonOverride": "", "vallisOverride": "", "duviriOverride": "", diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 8e775c10..5f36c998 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -13,7 +13,8 @@ import { addItems, combineInventoryChanges, getEffectiveAvatarImageType, - getInventory + getInventory, + updateCurrency } from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; import { ExportFlavour } from "warframe-public-export-plus"; @@ -100,6 +101,9 @@ export const inboxController: RequestHandler = async (req, res) => { } } } + if (message.RegularCredits) { + updateCurrency(inventory, -message.RegularCredits, false, inventoryChanges); + } await inventory.save(); res.json({ InventoryChanges: inventoryChanges }); } else if (latestClientMessageId) { diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index 0ec39442..27a5f0a7 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -47,6 +47,7 @@ export interface IMessage { acceptAction?: string; declineAction?: string; hasAccountAction?: boolean; + RegularCredits?: number; } export interface Arg { @@ -139,7 +140,8 @@ const messageSchema = new Schema( contextInfo: String, acceptAction: String, declineAction: String, - hasAccountAction: Boolean + hasAccountAction: Boolean, + RegularCredits: Number }, { id: false } ); diff --git a/src/services/configService.ts b/src/services/configService.ts index 0ad8acc7..a5170518 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -87,6 +87,8 @@ export interface IConfig { ghoulEmergenceOverride?: boolean; plagueStarOverride?: boolean; starDaysOverride?: boolean; + waterFightOverride?: boolean; + waterFightRewardsOverride?: number; eidolonOverride?: string; vallisOverride?: string; duviriOverride?: string; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 6bd6ce58..3551281a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -50,7 +50,7 @@ import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { IMissionCredits, IMissionReward } from "@/src/types/missionTypes"; import { crackRelic } from "@/src/helpers/relicHelper"; -import { createMessage } from "@/src/services/inboxService"; +import { createMessage, IMessageCreationTemplate } from "@/src/services/inboxService"; import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json"; import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; @@ -624,37 +624,93 @@ export const addMissionInventoryUpdates = async ( if (goal && goal.Personal) { inventory.PersonalGoalProgress ??= []; const goalProgress = inventory.PersonalGoalProgress.find(x => x.goalId.equals(goal._id.$oid)); - if (goalProgress) { - goalProgress.Best = Math.max(goalProgress.Best, uploadProgress.Best); - goalProgress.Count += uploadProgress.Count; - } else { + if (!goalProgress) { inventory.PersonalGoalProgress.push({ Best: uploadProgress.Best, Count: uploadProgress.Count, Tag: goal.Tag, goalId: new Types.ObjectId(goal._id.$oid) }); + } + const currentNode = inventoryUpdates.RewardInfo!.node; + let currentMissionKey; + if (currentNode == goal.Node) { + currentMissionKey = goal.MissionKeyName; + } else if (goal.ConcurrentNodes && goal.ConcurrentMissionKeyNames) { + for (let i = 0; i < goal.ConcurrentNodes.length; i++) { + if (currentNode == goal.ConcurrentNodes[i]) { + currentMissionKey = goal.ConcurrentMissionKeyNames[i]; + break; + } + } + } + if (currentMissionKey && currentMissionKey in goalMessagesByKey) { + const totalCount = (goalProgress?.Count ?? 0) + uploadProgress.Count; + let reward; + + if (goal.InterimGoals && goal.InterimRewards) { + for (let i = 0; i < goal.InterimGoals.length; i++) { + if ( + goal.InterimGoals[i] && + goal.InterimGoals[i] <= totalCount && + (!goalProgress || goalProgress.Count < goal.InterimGoals[i]) && + goal.InterimRewards[i] + ) { + reward = goal.InterimRewards[i]; + break; + } + } + } if ( - goal.Reward && - goal.Reward.items && - goal.MissionKeyName && - goal.MissionKeyName in goalMessagesByKey + !reward && + goal.Goal && + goal.Goal <= totalCount && + (!goalProgress || goalProgress.Count < goal.Goal) && + goal.Reward ) { - // Send reward via inbox - const info = goalMessagesByKey[goal.MissionKeyName]; - await createMessage(inventory.accountOwnerId, [ - { + reward = goal.Reward; + } + if ( + !reward && + goal.BonusGoal && + goal.BonusGoal <= totalCount && + (!goalProgress || goalProgress.Count < goal.BonusGoal) && + goal.BonusReward + ) { + reward = goal.BonusReward; + } + if (reward) { + if (currentMissionKey in goalMessagesByKey) { + // Send reward via inbox + const info = goalMessagesByKey[currentMissionKey]; + const message: IMessageCreationTemplate = { sndr: info.sndr, msg: info.msg, - att: goal.Reward.items.map(x => (isStoreItem(x) ? fromStoreItem(x) : x)), sub: info.sub, icon: info.icon, highPriority: true + }; + + if (reward.items) { + message.att = reward.items.map(x => (isStoreItem(x) ? fromStoreItem(x) : x)); } - ]); + if (reward.countedItems) { + message.countedAtt = reward.countedItems; + } + if (reward.credits) { + message.RegularCredits = reward.credits; + } + + await createMessage(inventory.accountOwnerId, [message]); + } } } + + if (goalProgress) { + goalProgress.Best = Math.max(goalProgress.Best, uploadProgress.Best); + goalProgress.Count += uploadProgress.Count; + } } } break; @@ -1011,8 +1067,16 @@ export const addMissionRewards = async ( if (rewardInfo.goalId) { const goal = getWorldState().Goals.find(x => x._id.$oid == rewardInfo.goalId); - if (goal?.MissionKeyName) { - levelKeyName = goal.MissionKeyName; + if (goal) { + if (rewardInfo.node == goal.Node && goal.MissionKeyName) levelKeyName = goal.MissionKeyName; + if (goal.ConcurrentNodes && goal.ConcurrentMissionKeyNames) { + for (let i = 0; i < goal.ConcurrentNodes.length && i < goal.ConcurrentMissionKeyNames.length; i++) { + if (rewardInfo.node == goal.ConcurrentNodes[i]) { + levelKeyName = goal.ConcurrentMissionKeyNames[i]; + break; + } + } + } } } @@ -2149,5 +2213,29 @@ const goalMessagesByKey: Record { Sorties: [], LiteSorties: [], ActiveMissions: [], + FlashSales: [], GlobalUpgrades: [], Invasions: [], VoidTraders: [], @@ -1401,7 +1402,8 @@ export const getWorldState = (buildLabel?: string): IWorldState => { EndlessXpChoices: [], KnownCalendarSeasons: [], ...staticWorldState, - SyndicateMissions: [...staticWorldState.SyndicateMissions] + SyndicateMissions: [...staticWorldState.SyndicateMissions], + InGameMarket: staticWorldState.InGameMarket }; // Old versions seem to really get hung up on not being able to load these. @@ -1629,6 +1631,299 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); } + const firstAugustWeekday = new Date(Date.UTC(date.getUTCFullYear(), 7, 1)).getUTCDay(); + const firstAugustWednesdayOffset = (3 - firstAugustWeekday + 7) % 7; + const waterFightStart = Date.UTC(date.getUTCFullYear(), 7, 1 + firstAugustWednesdayOffset, 15); + + const firstSeptemberWeekday = new Date(Date.UTC(date.getUTCFullYear(), 8, 1)).getUTCDay(); + const firstSeptemberWednesdayOffset = (3 - firstSeptemberWeekday + 7) % 7; + const waterFightEnd = Date.UTC(date.getUTCFullYear(), 8, 1 + firstSeptemberWednesdayOffset, 15); + + const isWaterFightActive = timeMs >= waterFightStart && timeMs < waterFightEnd; + logger.debug(isWaterFightActive); + if (config.worldState?.waterFightOverride ?? isWaterFightActive) { + const activationTimeStamp = config.worldState?.waterFightOverride + ? "1699372800000" + : waterFightStart.toString(); + const expiryTimeStamp = config.worldState?.waterFightOverride ? "2000000000000" : waterFightEnd.toString(); + const rewards = [ + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Upgrades/Skins/Weapons/Redeemer/RedeemerRelayWaterSkin"] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileHydroidRelay"] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/RelayHydroidBobbleHead"] + }, + { + items: [ + "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + ], + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/DogDays2023ASigil"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 25 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyBeachKavat"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 50 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyRucksackKubrow"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 75 + } + ] + }, + { + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneBeachcomber"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 100 + } + ] + } + ], + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageDogDays2024Glyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 25 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/DogDays2024Poster"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 50 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/DogDaysKubrowBadgeItem"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 75 + } + ] + }, + { + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/DogDays2024LisetPropCleaningDroneBeachcomber"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 100 + } + ] + } + ], + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysHydroidGlyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 25 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysLokiGlyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 50 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysNovaGlyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 75 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysValkyrGlyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 100 + } + ] + } + ] + ]; + + const year = config.worldState?.waterFightRewardsOverride ?? 3; + + worldState.Goals.push({ + _id: { + $oid: ((waterFightStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "c57487c3768936df" + }, + Activation: { $date: { $numberLong: activationTimeStamp } }, + Expiry: { $date: { $numberLong: expiryTimeStamp } }, + Count: 0, + Goal: 100, + InterimGoals: [25, 50], + BonusGoal: 200, + Success: 0, + Personal: true, + Bounty: true, + ClampNodeScores: true, + Node: "EventNode25", + ConcurrentMissionKeyNames: [ + "/Lotus/Types/Keys/TacAlertKeyWaterFightB", + "/Lotus/Types/Keys/TacAlertKeyWaterFightC", + "/Lotus/Types/Keys/TacAlertKeyWaterFightD" + ], + ConcurrentNodeReqs: [25, 50, 100], + ConcurrentNodes: ["EventNode24", "EventNode34", "EventNode35"], + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyWaterFightA", + Faction: "FC_CORPUS", + Desc: "/Lotus/Language/Alerts/TacAlertWaterFight", + Icon: "/Lotus/Interface/Icons/StoreIcons/Emblems/SplashEventIcon.png", + Tag: "WaterFight", + InterimRewards: rewards[year].slice(0, 2), + Reward: rewards[year][2], + BonusReward: rewards[year][3], + ScoreVar: "Team1Score", + NightLevel: "/Lotus/Levels/GrineerBeach/GrineerBeachEventNight.level" + }); + + const baseStoreItem = { + ShowInMarket: true, + HideFromMarket: false, + SupporterPack: false, + Discount: 0, + BogoBuy: 0, + BogoGet: 0, + StartDate: { $date: { $numberLong: activationTimeStamp } }, + EndDate: { $date: { $numberLong: expiryTimeStamp } }, + ProductExpiryOverride: { $date: { $numberLong: expiryTimeStamp } } + }; + + const storeItems = [ + { + TypeName: "/Lotus/Types/StoreItems/Packages/WaterFightNoggleBundle", + PremiumOverride: 240, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFBeastMasterBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFChargerBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFEngineerBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFGruntBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/StoreItems/AvatarImages/ImagePopsicleGrineerPurple", + PremiumOverride: 0, + RegularOverride: 1 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFHealerBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFHeavyBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFHellionBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFSniperBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFTankBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/StoreItems/SuitCustomizations/ColourPickerRollers", + PremiumOverride: 75, + RegularOverride: 0 + } + ]; + + worldState.FlashSales.push(...storeItems.map(item => ({ ...baseStoreItem, ...item }))); + + const seasonalItems = storeItems.map(item => item.TypeName); + + const seasonalCategory = worldState.InGameMarket.LandingPage.Categories.find(c => c.CategoryName == "SEASONAL"); + + if (seasonalCategory) { + seasonalCategory.Items ??= []; + seasonalCategory.Items.push(...seasonalItems); + } else { + worldState.InGameMarket.LandingPage.Categories.push({ + CategoryName: "SEASONAL", + Name: "/Lotus/Language/Store/SeasonalCategoryTitle", + Icon: "seasonal", + AddToMenu: true, + Items: seasonalItems + }); + } + } + // Nightwave Challenges const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel); if (nightwaveSyndicateTag) { diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index a1b7feca..e04a1fdc 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -5,12 +5,14 @@ export interface IWorldState { Version: number; // for goals BuildLabel: string; Time: number; + InGameMarket: IInGameMarket; Goals: IGoal[]; Alerts: []; Sorties: ISortie[]; LiteSorties: ILiteSortie[]; SyndicateMissions: ISyndicateMissionInfo[]; ActiveMissions: IFissure[]; + FlashSales: IFlashSale[]; GlobalUpgrades: IGlobalUpgrade[]; Invasions: IInvasion[]; NodeOverrides: INodeOverride[]; @@ -39,6 +41,8 @@ export interface IGoal { Expiry: IMongoDate; Count?: number; Goal?: number; + InterimGoals?: number[]; + BonusGoal?: number; HealthPct?: number; Success?: number; Personal?: boolean; @@ -53,16 +57,24 @@ export interface IGoal { Tag: string; Node?: string; VictimNode?: string; + ConcurrentMissionKeyNames?: string[]; + ConcurrentNodeReqs?: number[]; + ConcurrentNodes?: string[]; RegionIdx?: number; Regions?: number[]; MissionKeyName?: string; Reward?: IMissionReward; + InterimRewards?: IMissionReward[]; + BonusReward?: IMissionReward; JobAffiliationTag?: string; Jobs?: ISyndicateJob[]; PreviousJobs?: ISyndicateJob[]; JobCurrentVersion?: IOid; JobPreviousVersion?: IOid; + + ScoreVar?: string; + NightLevel?: string; } export interface ISyndicateJob { @@ -321,6 +333,37 @@ export type TCircuitGameMode = | "Assassination" | "Alchemy"; +export interface IFlashSale { + TypeName: string; + ShowInMarket: boolean; + HideFromMarket: boolean; + SupporterPack: boolean; + Discount: number; + BogoBuy: number; + BogoGet: number; + PremiumOverride: number; + RegularOverride: number; + ProductExpiryOverride?: IMongoDate; + StartDate: IMongoDate; + EndDate: IMongoDate; +} + +export interface IInGameMarket { + LandingPage: ILandingPage; +} + +export interface ILandingPage { + Categories: IGameMarketCategory[]; +} + +export interface IGameMarketCategory { + CategoryName: string; + Name: string; + Icon: string; + AddToMenu?: boolean; + Items?: string[]; +} + export interface ITmp { cavabegin: string; PurchasePlatformLockEnabled: boolean; // Seems unused diff --git a/static/webui/index.html b/static/webui/index.html index 99193f78..c5d2b6ce 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -970,6 +970,26 @@ +
+
+ + +
+
+ + +
+