diff --git a/config-vanilla.json b/config-vanilla.json index 12600852..a8834907 100644 --- a/config-vanilla.json +++ b/config-vanilla.json @@ -72,6 +72,8 @@ "resourceBoost": false, "tennoLiveRelay": false, "galleonOfGhouls": 0, + "ghoulEmergenceOverride": null, + "plagueStarOverride": null, "starDaysOverride": null, "eidolonOverride": "", "vallisOverride": "", diff --git a/src/services/configService.ts b/src/services/configService.ts index f46c747b..0ad8acc7 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -84,6 +84,8 @@ export interface IConfig { tennoLiveRelay?: boolean; baroTennoConRelay?: boolean; galleonOfGhouls?: number; + ghoulEmergenceOverride?: boolean; + plagueStarOverride?: boolean; starDaysOverride?: boolean; eidolonOverride?: string; vallisOverride?: string; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 1125be42..4d8c9439 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -76,7 +76,7 @@ import { } from "@/src/services/worldStateService"; import { config } from "@/src/services/configService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; -import { ISyndicateMissionInfo } from "@/src/types/worldStateTypes"; +import { IGoal, ISyndicateMissionInfo } from "@/src/types/worldStateTypes"; import { fromOid } from "@/src/helpers/inventoryHelpers"; import { TAccountDocument } from "@/src/services/loginService"; import { ITypeCount } from "@/src/types/commonTypes"; @@ -1259,6 +1259,8 @@ export const addMissionRewards = async ( } } + AffiliationMods ??= []; + if (rewardInfo.JobStage != undefined && rewardInfo.jobId) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [jobType, unkIndex, hubNode, syndicateMissionId] = rewardInfo.jobId.split("_"); @@ -1266,9 +1268,29 @@ export const addMissionRewards = async ( if (syndicateMissionId) { pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); } - const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); + let syndicateEntry: ISyndicateMissionInfo | IGoal | undefined = syndicateMissions.find( + m => m._id.$oid === syndicateMissionId + ); + if ( + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty" + ].some(prefix => jobType.startsWith(prefix)) + ) { + const { Goals } = getWorldState(undefined); + syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId); + if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!; + } if (syndicateEntry && syndicateEntry.Jobs) { let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; + if ( + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty" + ].some(prefix => jobType.startsWith(prefix)) + ) { + currentJob = syndicateEntry.Jobs.find(j => j.jobType === jobType)!; + } if (syndicateEntry.Tag === "EntratiSyndicate") { if ( [ @@ -1311,31 +1333,35 @@ export const addMissionRewards = async ( `Giving ${medallionAmount} medallions for the ${rewardInfo.JobStage} stage of the ${rewardInfo.JobTier} tier bounty` ); } else { - if (rewardInfo.JobTier! >= 0) { + const specialCase = [ + { endings: ["Heists/HeistProfitTakerBountyOne"], stage: 2, amount: 1000 }, + { endings: ["Hunts/AllTeralystsHunt"], stage: 2, amount: 5000 }, + { + endings: [ + "Hunts/TeralystHunt", + "Heists/HeistProfitTakerBountyTwo", + "Heists/HeistProfitTakerBountyThree", + "Heists/HeistProfitTakerBountyFour", + "Heists/HeistExploiterBountyOne" + ], + amount: 1000 + } + ]; + const specialCaseReward = specialCase.find( + rule => + rule.endings.some(e => jobType.endsWith(e)) && + (rule.stage === undefined || rewardInfo.JobStage === rule.stage) + ); + + if (specialCaseReward) { + addStanding(inventory, syndicateEntry.Tag, specialCaseReward.amount, AffiliationMods); + } else { addStanding( inventory, syndicateEntry.Tag, Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)), AffiliationMods ); - } else { - if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) { - addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods); - } - if (jobType.endsWith("Hunts/AllTeralystsHunt") && rewardInfo.JobStage === 2) { - addStanding(inventory, syndicateEntry.Tag, 5000, AffiliationMods); - } - if ( - [ - "Hunts/TeralystHunt", - "Heists/HeistProfitTakerBountyTwo", - "Heists/HeistProfitTakerBountyThree", - "Heists/HeistProfitTakerBountyFour", - "Heists/HeistExploiterBountyOne" - ].some(ending => jobType.endsWith(ending)) - ) { - addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods); - } } } } @@ -1672,7 +1698,19 @@ function getRandomMissionDrops( if (syndicateMissionId) { pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); } - const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); + let syndicateEntry: ISyndicateMissionInfo | IGoal | undefined = syndicateMissions.find( + m => m._id.$oid === syndicateMissionId + ); + if ( + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty" + ].some(prefix => jobType.startsWith(prefix)) + ) { + const { Goals } = getWorldState(undefined); + syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId); + if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!; + } if (syndicateEntry && syndicateEntry.Jobs) { let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; @@ -1757,6 +1795,14 @@ function getRandomMissionDrops( } } } + if ( + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty" + ].some(prefix => jobType.startsWith(prefix)) + ) { + job = syndicateEntry.Jobs.find(j => j.jobType === jobType)!; + } rewardManifests = [job.rewards]; if (job.xpAmounts.length > 1) { const curentStage = RewardInfo.JobStage! + 1; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index a7b09372..23d942bd 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -131,6 +131,13 @@ const eidolonNarmerJobs: readonly string[] = [ "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib" ]; +const eidolonGhoulJobs: readonly string[] = [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyHunt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyRes" +]; + const venusJobs: readonly string[] = [ "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation", @@ -1556,6 +1563,72 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); } + const firstNovemberWeekday = new Date(Date.UTC(date.getUTCFullYear(), 10, 1)).getUTCDay(); + const firstNovemberMondayOffset = (8 - firstNovemberWeekday) % 7; + + const plagueStarStart = Date.UTC(date.getUTCFullYear(), 10, firstNovemberMondayOffset + 1, 16); + const plagueStarEnd = Date.UTC(date.getUTCFullYear(), 10, firstNovemberMondayOffset + 15, 16); + + const isPlagueStarActive = timeMs >= plagueStarStart && timeMs < plagueStarEnd; + if (config.worldState?.plagueStarOverride ?? isPlagueStarActive) { + worldState.Goals.push({ + _id: { $oid: "654a5058c757487cdb11824f" }, + Activation: { + $date: { + $numberLong: config.worldState?.plagueStarOverride ? "1699372800000" : plagueStarStart.toString() + } + }, + Expiry: { + $date: { + $numberLong: config.worldState?.plagueStarOverride ? "2000000000000" : plagueStarEnd.toString() + } + }, + Tag: "InfestedPlains", + RegionIdx: 2, + Faction: "FC_INFESTATION", + Desc: "/Lotus/Language/InfestedPlainsEvent/InfestedPlainsBountyName", + ToolTip: "/Lotus/Language/InfestedPlainsEvent/InfestedPlainsBountyDesc", + Icon: "/Lotus/Materials/Emblems/PlagueStarEventBadge_e.png", + JobAffiliationTag: "EventSyndicate", + Jobs: [ + { + jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableRewards", + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [50, 300, 100, 575] + }, + { + jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBountyAdvanced", + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableRewards", + minEnemyLevel: 55, + maxEnemyLevel: 65, + xpAmounts: [200, 1000, 300, 1700], + requiredItems: [ + "/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventIngredient", + "/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventClanIngredient" + ], + useRequiredItemsAsMiscItemFee: true + }, + { + jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBountySteelPath", + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableSteelPathRewards", + minEnemyLevel: 100, + maxEnemyLevel: 110, + xpAmounts: [200, 1100, 400, 2100], + masteryReq: 10, + requiredItems: [ + "/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventIngredient", + "/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventClanIngredient" + ], + useRequiredItemsAsMiscItemFee: true + } + ], + Transmission: "/Lotus/Sounds/Dialog/PlainsMeteorLeadUp/LeadUp/DLeadUp0021Lotus", + InstructionalItem: "/Lotus/Types/StoreItems/Packages/PlagueStarEventStoreItem" + }); + } + // Nightwave Challenges const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel); if (nightwaveSyndicateTag) { @@ -1626,6 +1699,103 @@ export const getWorldState = (buildLabel?: string): IWorldState => { pushClassicBounties(worldState.SyndicateMissions, bountyCycle); } while (isBeforeNextExpectedWorldStateRefresh(timeMs, bountyCycleEnd) && ++bountyCycle); + const ghoulsCycleDay = day % 21; + const isGhoulEmergenceActive = ghoulsCycleDay >= 17 && ghoulsCycleDay <= 20; // 4 days for event and 17 days for break + if (config.worldState?.ghoulEmergenceOverride ?? isGhoulEmergenceActive) { + const ghoulPool = [...eidolonGhoulJobs]; + const pastGhoulPool = [...eidolonGhoulJobs]; + + const seed = new SRng(bountyCycle).randomInt(0, 100_000); + const pastSeed = new SRng(bountyCycle - 1).randomInt(0, 100_000); + + const rng = new SRng(seed); + const pastRng = new SRng(pastSeed); + + const activeStartDay = day - ghoulsCycleDay + 17; + const activeEndDay = activeStartDay + 5; + const dayWithFraction = (timeMs - EPOCH) / 86400000; + + const progress = (dayWithFraction - activeStartDay) / (activeEndDay - activeStartDay); + const healthPct = 1 - Math.min(Math.max(progress, 0), 1); + + worldState.Goals.push({ + _id: { $oid: "687ebbe6d1d17841c9c59f38" }, + Activation: { + $date: { + $numberLong: config.worldState?.ghoulEmergenceOverride + ? "1753204900185" + : Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate() + (day - ghoulsCycleDay + 17) + ).toString() + } + }, + Expiry: { + $date: { + $numberLong: config.worldState?.ghoulEmergenceOverride + ? "2000000000000" + : Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate() + (day - ghoulsCycleDay + 21) + ).toString() + } + }, + HealthPct: config.worldState?.ghoulEmergenceOverride ? 1 : healthPct, + VictimNode: "SolNode228", + Regions: [2], + Success: 0, + Desc: "/Lotus/Language/GameModes/RecurringGhoulAlert", + ToolTip: "/Lotus/Language/GameModes/RecurringGhoulAlertDesc", + Icon: "/Lotus/Interface/Icons/Categories/IconGhouls256.png", + Tag: "GhoulEmergence", + JobAffiliationTag: "CetusSyndicate", + JobCurrentVersion: { + $oid: ((bountyCycle * 9000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" + }, + Jobs: [ + { + jobType: rng.randomElementPop(ghoulPool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableARewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [270, 270, 270, 400] // not faithful + }, + { + jobType: rng.randomElementPop(ghoulPool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableBRewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 50, + xpAmounts: [480, 480, 480, 710] // not faithful + } + ], + JobPreviousVersion: { + $oid: (((bountyCycle - 1) * 9000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" + }, + PreviousJobs: [ + { + jobType: pastRng.randomElementPop(pastGhoulPool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableARewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [270, 270, 270, 400] // not faithful + }, + { + jobType: pastRng.randomElementPop(pastGhoulPool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableBRewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 50, + xpAmounts: [480, 480, 480, 710] // not faithful + } + ] + }); + } + if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ _id: { $oid: "5b23106f283a555109666672" }, diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 4ab502c7..a1b7feca 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -37,19 +37,46 @@ export interface IGoal { _id: IOid; Activation: IMongoDate; Expiry: IMongoDate; - Count: number; - Goal: number; - Success: number; - Personal: boolean; + Count?: number; + Goal?: number; + HealthPct?: number; + Success?: number; + Personal?: boolean; Bounty?: boolean; + Faction?: string; ClampNodeScores?: boolean; Desc: string; ToolTip?: string; + Transmission?: string; + InstructionalItem?: string; Icon: string; Tag: string; - Node: string; + Node?: string; + VictimNode?: string; + RegionIdx?: number; + Regions?: number[]; MissionKeyName?: string; Reward?: IMissionReward; + + JobAffiliationTag?: string; + Jobs?: ISyndicateJob[]; + PreviousJobs?: ISyndicateJob[]; + JobCurrentVersion?: IOid; + JobPreviousVersion?: IOid; +} + +export interface ISyndicateJob { + jobType?: string; + rewards: string; + masteryReq?: number; + minEnemyLevel: number; + maxEnemyLevel: number; + xpAmounts: number[]; + endless?: boolean; + locationTag?: string; + isVault?: boolean; + requiredItems?: string[]; + useRequiredItemsAsMiscItemFee?: boolean; } export interface ISyndicateMissionInfo { @@ -59,17 +86,7 @@ export interface ISyndicateMissionInfo { Tag: string; Seed: number; Nodes: string[]; - Jobs?: { - jobType?: string; - rewards: string; - masteryReq: number; - minEnemyLevel: number; - maxEnemyLevel: number; - xpAmounts: number[]; - endless?: boolean; - locationTag?: string; - isVault?: boolean; - }[]; + Jobs?: ISyndicateJob[]; } export interface IGlobalUpgrade { diff --git a/static/webui/index.html b/static/webui/index.html index b827fc45..99193f78 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -946,6 +946,22 @@ +
+ + +
+
+ + +