import baro, { type IBaroDataTradeOffer } from "../constants/baro.ts";
import varzia from "../constants/varzia.ts";
import fissureMissions from "../../static/fixed_responses/worldState/fissureMissions.json" with { type: "json" };
import sortieTilesets from "../../static/fixed_responses/worldState/sortieTilesets.json" with { type: "json" };
import sortieTilesetMissions from "../../static/fixed_responses/worldState/sortieTilesetMissions.json" with { type: "json" };
import syndicateMissions from "../../static/fixed_responses/worldState/syndicateMissions.json" with { type: "json" };
import darvoDeals from "../constants/darvoDeals.ts";
import invasionNodes from "../../static/fixed_responses/worldState/invasionNodes.json" with { type: "json" };
import invasionRewards from "../../static/fixed_responses/worldState/invasionRewards.json" with { type: "json" };
import pvpChallenges from "../../static/fixed_responses/worldState/pvpChallenges.json" with { type: "json" };
import { EPOCH, unixTimesInMs } from "../constants/timeConstants.ts";
import { config } from "./configService.ts";
import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "./rngService.ts";
import type { IMissionReward, IRegion, ITilesetMission, TFaction, TMissionType } from "warframe-public-export-plus";
import { ExportRegions, ExportSyndicates, ExportTilesets, ExportRecipes } from "warframe-public-export-plus";
import type {
ICalendarDay,
ICalendarEvent,
ICalendarSeason,
IAlert,
IGoal,
IInvasion,
ILiteSortie,
IPrimeVaultTrader,
IPrimeVaultTraderOffer,
IPVPChallengeInstance,
ISeasonChallenge,
ISortie,
ISortieMission,
ISyndicateMissionInfo,
ITmp,
IVoidStorm,
IVoidTrader,
IWorldState,
TCircuitGameMode,
IFlashSale,
IAlertMissionInfo,
IEndlessXpChoice,
IGoalV9
} from "../types/worldStateTypes.ts";
import { toMongoDate2, toOid, toOid2 } from "../helpers/inventoryHelpers.ts";
import { logger } from "../utils/logger.ts";
import { DailyDeal, Fissure } from "../models/worldStateModel.ts";
import { toStoreItem, fromStoreItem, getRegions } from "./itemDataService.ts";
import { factionToInt, getConquest, getMissionTypeForLegacyOverride } from "./conquestService.ts";
import { getDescent } from "./descentService.ts";
import { catBreadHash } from "../helpers/stringHelpers.ts";
import { Guild } from "../models/guildModel.ts";
import { libraryTargetToAvatar } from "../constants/synthesis.ts";
import { BL_LATEST, BV_LATEST } from "../constants/gameVersions.ts";
import { isRegionAvailableIn } from "./itemDataService.ts";
import gameToBuildVersionInt from "../constants/gameToBuildVersionInt.ts";
import { buildVersionToInt } from "../helpers/versionHelper.ts";
import {
dogDaysFlashSales,
naberusNightsFlashSales,
prideMonthFlashSales,
qqtcFlashSales,
saintPatrickDayFlashSales,
tennobaumFlashSales,
type IFlashSaleData
} from "../constants/flashSales.ts";
const sortieBosses = [
"SORTIE_BOSS_HYENA",
"SORTIE_BOSS_KELA",
"SORTIE_BOSS_VOR",
"SORTIE_BOSS_RUK",
"SORTIE_BOSS_HEK",
"SORTIE_BOSS_KRIL",
"SORTIE_BOSS_TYL",
"SORTIE_BOSS_JACKAL",
"SORTIE_BOSS_ALAD",
"SORTIE_BOSS_AMBULAS",
"SORTIE_BOSS_NEF",
"SORTIE_BOSS_RAPTOR",
"SORTIE_BOSS_PHORID",
"SORTIE_BOSS_LEPHANTIS",
"SORTIE_BOSS_INFALAD",
"SORTIE_BOSS_CORRUPTED_VOR"
] as const;
type TSortieBoss = (typeof sortieBosses)[number];
const sortieBossToFaction: Record<TSortieBoss, TFaction> = {
SORTIE_BOSS_HYENA: "FC_CORPUS",
SORTIE_BOSS_KELA: "FC_GRINEER",
SORTIE_BOSS_VOR: "FC_GRINEER",
SORTIE_BOSS_RUK: "FC_GRINEER",
SORTIE_BOSS_HEK: "FC_GRINEER",
SORTIE_BOSS_KRIL: "FC_GRINEER",
SORTIE_BOSS_TYL: "FC_GRINEER",
SORTIE_BOSS_JACKAL: "FC_CORPUS",
SORTIE_BOSS_ALAD: "FC_CORPUS",
SORTIE_BOSS_AMBULAS: "FC_CORPUS",
SORTIE_BOSS_NEF: "FC_CORPUS",
SORTIE_BOSS_RAPTOR: "FC_CORPUS",
SORTIE_BOSS_PHORID: "FC_INFESTATION",
SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION",
SORTIE_BOSS_INFALAD: "FC_INFESTATION",
SORTIE_BOSS_CORRUPTED_VOR: "FC_OROKIN"
};
const sortieFactionToSystemIndexes: Record<string, number[]> = {
FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 17, 18],
FC_CORPUS: [1, 4, 7, 8, 12, 15, 17],
FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16],
FC_OROKIN: [14]
};
const sortieFactionToFactions: Record<string, TFaction[]> = {
FC_GRINEER: ["FC_GRINEER"],
FC_CORPUS: ["FC_CORPUS"],
FC_INFESTATION: ["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION"],
FC_OROKIN: ["FC_OROKIN"]
};
const sortieBossNode: Record<Exclude<TSortieBoss, "SORTIE_BOSS_CORRUPTED_VOR">, string> = {
SORTIE_BOSS_ALAD: "SolNode53",
SORTIE_BOSS_AMBULAS: "SolNode51",
SORTIE_BOSS_HEK: "SolNode24",
SORTIE_BOSS_HYENA: "SolNode127",
SORTIE_BOSS_INFALAD: "SolNode166",
SORTIE_BOSS_JACKAL: "SolNode104",
SORTIE_BOSS_KELA: "SolNode193",
SORTIE_BOSS_KRIL: "SolNode99",
SORTIE_BOSS_LEPHANTIS: "SolNode712",
SORTIE_BOSS_NEF: "SettlementNode20",
SORTIE_BOSS_PHORID: "SolNode171",
SORTIE_BOSS_RAPTOR: "SolNode210",
SORTIE_BOSS_RUK: "SolNode32",
SORTIE_BOSS_TYL: "SolNode105",
SORTIE_BOSS_VOR: "SolNode108"
};
const sortieAssassinationOnlyNodes: string[] = ["SolNode193", "SolNode105", "SolNode108"];
const configAlerts: Record<string, IAlert & { minBuildVersion?: number }> = {
voidCorruption2025Week1: {
_id: { $oid: "677d452e2f324ee7b90f8ccf" },
Activation: { $date: { $numberLong: "1736524800000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode61",
missionType: "MT_SABOTAGE",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 30000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedFireRateDamagePistol"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipCoreSabotage",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipSquadA",
extraEnemySpec: "/Lotus/Types/Game/EnemySpecs/GamemodeExtraEnemySpecs/CorpusSabotageTiersA",
minEnemyLevel: 10,
maxEnemyLevel: 15
}
},
voidCorruption2025Week2: {
_id: { $oid: "677d45811daeae9de40e8c0f" },
Activation: { $date: { $numberLong: "1737129600000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SettlementNode11",
missionType: "MT_DEFENSE",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 30000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritChanceFireRatePistol"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipDefense",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipSquadDefenseB",
minEnemyLevel: 20,
maxEnemyLevel: 25,
maxRotations: 2
}
},
voidCorruption2025Week3: {
_id: { $oid: "677d45a494ad716c90006b9a" },
Activation: { $date: { $numberLong: "1737734400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode118",
missionType: "MT_ARTIFACT",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 30000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritDamagePistol"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipDisruption",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipSurvivalA",
extraEnemySpec: "/Lotus/Types/Game/EnemySpecs/SpecialMissionSpecs/DisruptionCorpusShip",
customAdvancedSpawners: ["/Lotus/Types/Enemies/AdvancedSpawners/ErrantSpecterInvasion"],
minEnemyLevel: 30,
maxEnemyLevel: 35
}
},
voidCorruption2025Week4: {
_id: { $oid: "677d4700682d173abb0e19fe" },
Activation: { $date: { $numberLong: "1738339200000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode4",
missionType: "MT_EXTERMINATION",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 30000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedDamageRecoilPistol"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipExterminate",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipExterminateMixed",
minEnemyLevel: 40,
maxEnemyLevel: 45
}
},
dagathAlerts2026Week1: {
_id: { $oid: "6949862e3f3f35491e02232c" },
Activation: { $date: { $numberLong: "1769022000000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode147",
missionType: "MT_TERRITORY",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Recipes/WarframeRecipes/DagathChassisComponent"]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerShipyardsInterception",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerShipyardsDefenseA",
minEnemyLevel: 25,
maxEnemyLevel: 35,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
dagathAlerts2026Week2: {
_id: { $oid: "6949863a548d83b650011de8" },
Activation: { $date: { $numberLong: "1769626800000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode404",
missionType: "MT_SABOTAGE",
faction: "FC_OROKIN",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Recipes/WarframeRecipes/DagathHelmetComponent"]
},
levelOverride: "/Lotus/Levels/Proc/Orokin/OrokinTowerSabotageForest",
enemySpec: "/Lotus/Types/Game/EnemySpecs/OrokinSquadOne",
extraEnemySpec: "/Lotus/Types/Game/EnemySpecs/OrokinSabotageGrineerForest",
minEnemyLevel: 25,
maxEnemyLevel: 35,
vipAgent: "/Lotus/Types/Enemies/Grineer/Vip/JetpackSisters/JetpackSisters",
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
dagathAlerts2026Week3: {
_id: { $oid: "69498642a0836c98e10f5e34" },
Activation: { $date: { $numberLong: "1770231600000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode100",
missionType: "MT_SURVIVAL",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Recipes/WarframeRecipes/DagathSystemsComponent"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusGasCitySurvival",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusGasSurvival",
minEnemyLevel: 25,
maxEnemyLevel: 35,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
dagathAlerts2026Week4: {
_id: { $oid: "69498623a7342ab1500cc99b" },
Activation: { $date: { $numberLong: "1768417200000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode103",
missionType: "MT_EXTERMINATION",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Recipes/WarframeRecipes/DagathBlueprint"]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerAsteroidExterminate",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerExterminateBlades",
minEnemyLevel: 25,
maxEnemyLevel: 35,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
starDaysAlerts2026Week1: {
_id: { $oid: "696935998429008d89100af9" },
Activation: { $date: { $numberLong: "1770393600000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode22",
missionType: "MT_DEFENSE",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 25000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageStarDaysStarDaysMarieGlyph"],
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/OrokinCatalyst", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusOutpostDefense",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusSquadDefenseB",
minEnemyLevel: 25,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/LotusGiftDesc",
maxRotations: 1
},
Tag: "LotusGift",
ForceUnlock: true
},
starDaysAlerts2026Week2: {
_id: { $oid: "696935b47918af0b1c0c4c2f" },
Activation: { $date: { $numberLong: "1770998400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode11",
missionType: "MT_MOBILE_DEFENSE",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 25000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageStarDaysLyonGlyph"],
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/OrokinReactor", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerSettlementMobileDefense",
enemySpec: "/Lotus/Types/Game/EnemySpecs/DesertGrineerSquadOne",
minEnemyLevel: 25,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
starDaysAlerts2026Week3: {
_id: { $oid: "696935d518288b03bb0224ae" },
Activation: { $date: { $numberLong: "1771603200000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode121",
missionType: "MT_EXTERMINATION",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 25000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageStarDaysRoatheGlyph"],
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/Forma", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusGasCityExterminate",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusGasExterminateMixed",
minEnemyLevel: 25,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
starDaysAlerts2026Week4: {
_id: { $oid: "696935ed8429008d89100cea" },
Activation: { $date: { $numberLong: "1772208000000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode82",
missionType: "MT_SABOTAGE",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 25000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Props/Seasonal/StarDays2026HeartPictureFrame"],
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/UtilityUnlocker", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerGalleonCoreSabotage",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerSquadSix",
extraEnemySpec: "/Lotus/Types/Game/EnemySpecs/GamemodeExtraEnemySpecs/GrineerSabotageTiersTwo",
minEnemyLevel: 25,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
destiny2TributeAlert: {
_id: { $oid: "6a29b21c614c39ecbb0aa925" },
Activation: { $date: { $numberLong: "1781708400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode20",
missionType: "MT_EXTERMINATION",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
countedItems: [{ ItemType: "/Lotus/Types/Items/Titles/GuardianTitle", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerGalleonExterminate",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerExterminateSimple",
minEnemyLevel: 10,
maxEnemyLevel: 15,
descText: "/Lotus/Language/Alerts/LotusGiftDesc",
maxWaveNum: 167
},
Tag: "LotusGift",
ForceUnlock: true,
minBuildVersion: gameToBuildVersionInt["43.0.0"]
}
};
const alertGeneratorConfig: Record<
string,
{
alertInterval: number;
alertLength: number;
allowedSystemIndexes: number[];
baseAlert: Partial<IAlert>;
baseMissionInfo: Partial<IAlertMissionInfo>;
}
> = {
"12MinWarEvent": {
alertInterval: unixTimesInMs.hour / 2,
alertLength: unixTimesInMs.hour * 1.5,
allowedSystemIndexes: [14],
baseAlert: {
Tag: "12MinWarEvent",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/BloodOfPeritaEventBadge.png"
},
baseMissionInfo: {
minEnemyLevel: 65,
maxEnemyLevel: 70,
maxRotations: 1,
missionReward: {
credits: 8900, // should be random
countedItems: [
{
ItemType: "/Lotus/Types/Gameplay/Tau/Resources/TwelveResourceCurrencyItem",
ItemCount: 20
}
]
},
descText: "/Lotus/Language/TauPrequel/TauPrequelFinal/TauPrequelEventAlertTitle",
questReq: "/Lotus/Types/Keys/TauPrequel/TauPrequelQuestKeyChain",
leadersAlwaysAllowed: true
}
},
JadeShadows: {
alertInterval: unixTimesInMs.hour / 2,
alertLength: unixTimesInMs.hour * 2,
allowedSystemIndexes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18],
baseAlert: {
Tag: "JadeShadows",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/JadeShadowsEventBadge.png"
},
baseMissionInfo: {
minEnemyLevel: 50,
maxEnemyLevel: 50,
maxRotations: 1,
missionReward: {
credits: 8900, // should be random
countedItems: [
{
ItemType: "/Lotus/Types/Gameplay/JadeShadows/Resources/AscensionEventResourceItem",
ItemCount: 10
}
]
},
descText: "/Lotus/Language/JadeShadows/EventAlertTitle",
questReq: "/Lotus/Types/Keys/JadeShadows/JadeShadowQuestKeyChain",
leadersAlwaysAllowed: true
}
}
};
const eidolonJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt",
"/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap",
"/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft",
"/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache",
"/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne",
"/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo",
"/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab",
"/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc"
];
const eidolonNarmerJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft",
"/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",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy"
];
const venusNarmerJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate",
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate",
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense",
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation"
];
const microplanetJobs: readonly string[] = [
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty"
];
const microplanetEndlessJobs: readonly string[] = [
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
];
const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => {
return nowMs + 300_000 > thenMs;
};
const getSortieTime = (day: number): number => {
const dayStart = EPOCH + day * 86400000;
const date = new Date(dayStart);
date.setUTCHours(12);
const isDst = new Intl.DateTimeFormat("en-US", {
timeZone: "America/Toronto",
timeZoneName: "short"
})
.formatToParts(date)
.find(part => part.type === "timeZoneName")!
.value.includes("DT");
return dayStart + (isDst ? 16 : 17) * 3600000;
};
const pushSyndicateMissions = (
worldState: IWorldState,
day: number,
seed: number,
idSuffix: string,
syndicateTag: string
): void => {
const nodeOptions: string[] = [...syndicateMissions];
const rng = new SRng(seed);
const nodes: string[] = [];
for (let i = 0; i != 6; ++i) {
const index = rng.randomInt(0, nodeOptions.length - 1);
nodes.push(nodeOptions[index]);
nodeOptions.splice(index, 1);
}
const dayStart = getSortieTime(day);
const dayEnd = getSortieTime(day + 1);
worldState.SyndicateMissions.push({
_id: toOid2(((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + idSuffix, worldState.BuildLabel),
Activation: toMongoDate2(dayStart, worldState.BuildLabel),
Expiry: toMongoDate2(dayEnd, worldState.BuildLabel),
Tag: syndicateTag,
Seed: seed,
Nodes: nodes
});
};
type TSortieTileset = keyof typeof sortieTilesetMissions;
const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): void => {
switch (tileset) {
case "GrineerForestTileset":
modifiers.push("SORTIE_MODIFIER_HAZARD_FOG");
break;
case "CorpusShipTileset":
case "GrineerGalleonTileset":
case "InfestedCorpusShipTileset":
modifiers.push("SORTIE_MODIFIER_HAZARD_MAGNETIC");
modifiers.push("SORTIE_MODIFIER_HAZARD_FIRE");
modifiers.push("SORTIE_MODIFIER_HAZARD_ICE");
break;
case "CorpusIcePlanetTileset":
case "CorpusIcePlanetTilesetCaves":
modifiers.push("SORTIE_MODIFIER_HAZARD_COLD");
break;
}
};
export const getSortie = (day: number, buildVersion: number): ISortie => {
// Seeds featuring CorpusIcePlanet to test seed validation: 14197, 23418, 71281
const seed = new SRng(day).randomInt(0, 100_000);
logger.trace(`sortie seed: ${seed}`);
const rng = new SRng(seed);
const boss = rng.randomElement(sortieBosses)!;
const enemyFaction = sortieBossToFaction[boss];
const nodes: string[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
key in sortieTilesets &&
(key != "SolNode228" || enemyFaction == "FC_GRINEER") && // PoE only works for grineer enemies
sortieFactionToSystemIndexes[enemyFaction].includes(value.systemIndex) &&
sortieFactionToFactions[enemyFaction].includes(value.faction!) &&
isRegionAvailableIn(key, value, buildVersion)
) {
nodes.push(key);
}
}
const willHaveAssassination = boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2;
if (willHaveAssassination) {
const index = nodes.indexOf(sortieBossNode[boss]);
if (index != -1) {
nodes.splice(index, 1);
}
}
const selectedNodes: ISortieMission[] = [];
const missionTypes = new Set();
if (enemyFaction == "FC_INFESTATION") {
// MT_RETRIEVAL may not be chosen for infested enemies (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2907)
missionTypes.add("MT_RETRIEVAL");
}
for (let i = 0; i < 3; i++) {
let randomIndex, node;
do {
randomIndex = rng.randomInt(0, nodes.length - 1);
node = nodes[randomIndex];
} while (sortieAssassinationOnlyNodes.indexOf(node) != -1);
const modifiers = [
"SORTIE_MODIFIER_LOW_ENERGY",
"SORTIE_MODIFIER_IMPACT",
"SORTIE_MODIFIER_SLASH",
"SORTIE_MODIFIER_PUNCTURE",
"SORTIE_MODIFIER_EXIMUS",
"SORTIE_MODIFIER_MAGNETIC",
"SORTIE_MODIFIER_CORROSIVE",
"SORTIE_MODIFIER_VIRAL",
"SORTIE_MODIFIER_ELECTRICITY",
"SORTIE_MODIFIER_RADIATION",
"SORTIE_MODIFIER_FIRE",
"SORTIE_MODIFIER_EXPLOSION",
"SORTIE_MODIFIER_FREEZE",
"SORTIE_MODIFIER_POISON",
"SORTIE_MODIFIER_SECONDARY_ONLY",
"SORTIE_MODIFIER_SHOTGUN_ONLY",
"SORTIE_MODIFIER_SNIPER_ONLY",
"SORTIE_MODIFIER_RIFLE_ONLY",
"SORTIE_MODIFIER_BOW_ONLY"
];
if (i == 2 && willHaveAssassination) {
const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] as TSortieTileset;
pushTilesetModifiers(modifiers, tileset);
const modifierType = rng.randomElement(modifiers)!;
selectedNodes.push({
missionType: "MT_ASSASSINATION",
modifierType,
node: sortieBossNode[boss],
tileset
});
continue;
}
const tileset = sortieTilesets[node as keyof typeof sortieTilesets] as TSortieTileset;
pushTilesetModifiers(modifiers, tileset);
const missionType = rng.randomElement(sortieTilesetMissions[tileset])! as TMissionType;
if (missionTypes.has(missionType) || missionType == "MT_ASSASSINATION") {
i--;
continue;
}
modifiers.push("SORTIE_MODIFIER_MELEE_ONLY"); // not an assassination mission, can now push this
if (missionType != "MT_TERRITORY") {
modifiers.push("SORTIE_MODIFIER_HAZARD_RADIATION");
}
modifiers.push(enemyFaction == "FC_CORPUS" ? "SORTIE_MODIFIER_SHIELDS" : "SORTIE_MODIFIER_ARMOR");
const modifierType = rng.randomElement(modifiers)!;
selectedNodes.push({
missionType,
modifierType,
node,
tileset
});
nodes.splice(randomIndex, 1);
missionTypes.add(missionType);
}
let clientSeed = rng.randomInt(0, 100_000);
while (!validateSortieSeed(clientSeed, enemyFaction, selectedNodes)) {
clientSeed = rng.randomInt(0, 100_000);
}
const dayStart = getSortieTime(day);
const dayEnd = getSortieTime(day + 1);
return {
_id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "d4d932c97c0a3acd" },
Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards",
Seed: clientSeed,
Boss: boss,
Variants: selectedNodes
};
};
const SORTIE_FALLBACK_MISSION_TYPES: TMissionType[] = [
"MT_EXTERMINATION",
"MT_SURVIVAL",
"MT_RESCUE",
"MT_SABOTAGE",
//"MT_CAPTURE",
"MT_INTEL",
"MT_DEFENSE",
"MT_MOBILE_DEFENSE",
"MT_TERRITORY",
"MT_RETRIEVAL",
"MT_HIVE",
"MT_EXCAVATE",
"MT_ARTIFACT"
];
const validateSortieSeed = (seed: number, enemyFaction: TFaction, variants: ISortieMission[]): boolean => {
const rng = new SRng(seed);
for (const variant of variants) {
// Gather variant data as the client would
let missionType = variant.missionType;
const tileset = ExportTilesets[variant.tileset];
if (missionType != "MT_ASSASSINATION") {
let missionPermutation = tileset.missions[variant.missionType];
if (!missionPermutation) {
const options = SORTIE_FALLBACK_MISSION_TYPES.filter(missionType => missionType in tileset.missions);
missionType = options[rng.randomInt(0, options.length - 1)];
missionPermutation = tileset.missions[missionType]!;
logger.trace(
`${variant.missionType} not supported by ${variant.tileset}; picking ${missionType} instead`
);
}
if (missionPermutation.enemySpecs?.length) {
rng.randomInt(1, missionPermutation.enemySpecs.length);
}
if (missionPermutation.extraEnemySpecs?.length) {
rng.randomInt(1, missionPermutation.extraEnemySpecs.length);
}
rng.randomFloat(); // difficulty
}
if (enemyFaction == "FC_INFESTATION" && tileset.faction != "FC_INFESTATION") {
// infested enemySpec
if (missionType == "MT_DEFENSE" || missionType == "MT_TERRITORY" || missionType == "MT_SURVIVAL") {
rng.randomInt(0, 1);
} else if (missionType != "MT_EXCAVATE") {
rng.randomInt(0, 3);
}
}
const locationTextureIndex =
missionType == "MT_ARENA" || missionType == "MT_JUNCTION" ? 0 : rng.randomInt(0, 1);
// Reject this seed if the data looks bad
if (missionType == "MT_RETRIEVAL" && enemyFaction == "FC_INFESTATION") {
return false;
}
if (
(variant.tileset == "CorpusIcePlanetTileset" || variant.tileset == "CorpusIcePlanetTilesetCaves") &&
locationTextureIndex != 0
) {
return false; // For the corpus ice planet tileset, index 1 is an infested corpus ship image, which we don't want.
}
}
return true;
};
interface IRotatingSeasonChallengePools {
daily: readonly string[];
weekly: string[];
hardWeekly: string[];
weeklyPermanent: string[];
}
export const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => {
const syndicate = ExportSyndicates[syndicateTag];
if (!syndicate.dailyChallenges || !syndicate.weeklyChallenges) {
throw new Error(`invalid syndicate tag for nightwave: ${syndicateTag}`);
}
return {
daily: syndicate.dailyChallenges,
weekly: syndicate.weeklyChallenges.filter(
x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
!x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
),
hardWeekly: syndicate.weeklyChallenges.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")),
weeklyPermanent: syndicate.weeklyChallenges.filter(x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
)
};
};
const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => {
const dayStart = EPOCH + day * 86400000;
const dayEnd = EPOCH + (day + 3) * 86400000;
return {
_id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") },
Daily: true,
Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } },
Challenge: sequentiallyUniqueRandomElement(pools.daily, day, 2, 605732938)!
};
};
const pushSeasonWeeklyChallenge = (
activeChallenges: ISeasonChallenge[],
pool: string[],
nightwaveSeason: number,
week: number,
id: number
): void => {
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id;
const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
let challenge: string;
do {
challenge = rng.randomElement(pool)!;
} while (activeChallenges.some(x => x.Challenge == challenge));
activeChallenges.push({
_id: {
$oid:
(nightwaveSeason + 1).toString().padStart(4, "0") +
"bb2d9d00cb47" +
challengeId.toString().padStart(8, "0")
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: challenge
});
};
export const pushWeeklyActs = (
activeChallenges: ISeasonChallenge[],
pools: IRotatingSeasonChallengePools,
week: number,
nightwaveStartTimestamp: number,
nightwaveSeason: number
): void => {
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 0);
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 1);
if (pools.weeklyPermanent.length > 0) {
const weekStart = EPOCH + week * unixTimesInMs.week;
const weekEnd = weekStart + unixTimesInMs.week;
const nightwaveWeekStart = ((): number => {
let ts = nightwaveStartTimestamp - EPOCH;
ts -= ts % unixTimesInMs.week;
return EPOCH + ts;
})();
const nightwaveWeek = Math.trunc((weekStart - nightwaveWeekStart) / unixTimesInMs.week);
const weeklyPermanentIndex = (nightwaveWeek * 3) % pools.weeklyPermanent.length;
for (let i = 0; i < 3; i++) {
activeChallenges.push({
_id: {
$oid:
(nightwaveSeason + 1).toString().padStart(4, "0") +
"b96e9d00cb47" +
(week * 7 + 2 + i).toString().padStart(8, "0")
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: pools.weeklyPermanent[weeklyPermanentIndex + i]
});
}
} else {
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 2);
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 3);
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 4);
}
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 5);
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 6);
};
const eidolonEpoch = 1391990400;
const timeOfDayRate = 0.0026670000515878;
const eidolonCycleDuration = 24 / timeOfDayRate;
const eidolonHourDuration = eidolonCycleDuration / 24;
const bountyEpoch = eidolonEpoch + 5 * eidolonHourDuration;
const generateXpAmounts = (rng: SRng, stageCount: number, minXp: number, maxXp: number): number[] => {
const step = minXp < 1000 ? 1 : 10;
const totalDeciXp = rng.randomInt(minXp / step, maxXp / step);
const xpAmounts: number[] = [];
if (stageCount < 4) {
const perStage = Math.ceil(totalDeciXp / stageCount) * step;
for (let i = 0; i != stageCount; ++i) {
xpAmounts.push(perStage);
}
} else {
const perStage = Math.ceil(Math.round(totalDeciXp * 0.667) / (stageCount - 1)) * step;
for (let i = 0; i != stageCount - 1; ++i) {
xpAmounts.push(perStage);
}
xpAmounts.push(Math.ceil(totalDeciXp * 0.332) * step);
}
return xpAmounts;
};
// Test vectors:
//console.log(generateXpAmounts(new SRng(1337n), 5, 5000, 5000)); // [840, 840, 840, 840, 1660]
//console.log(generateXpAmounts(new SRng(1337n), 3, 40, 40)); // [14, 14, 14]
//console.log(generateXpAmounts(new SRng(1337n), 5, 150, 150)); // [25, 25, 25, 25, 50]
//console.log(generateXpAmounts(new SRng(1337n), 4, 10, 10)); // [2, 2, 2, 4]
//console.log(generateXpAmounts(new SRng(1337n), 4, 15, 15)); // [4, 4, 4, 5]
//console.log(generateXpAmounts(new SRng(1337n), 4, 20, 20)); // [5, 5, 5, 7]
export const pushClassicBounties = (
syndicateMissions: ISyndicateMissionInfo[],
bountyCycle: number,
buildVersion: number
): void => {
const table = String.fromCharCode(65 + (bountyCycle % 3));
const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3));
const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2));
const seed = new SRng(bountyCycle).randomInt(0, 100_000);
const bountyCycleStart = Math.trunc((bountyEpoch + bountyCycle * eidolonCycleDuration) * 1000);
const bountyCycleEnd = Math.trunc(bountyCycleStart + eidolonCycleDuration * 1000);
if (buildVersion >= gameToBuildVersionInt["22.0.0"]) {
const rng = new SRng(seed);
const pool = [...eidolonJobs];
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "CetusSyndicate",
Seed: seed,
Nodes: [],
Jobs: [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`,
// Should be U22.10
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 0
}),
minEnemyLevel: 5,
maxEnemyLevel: 15,
xpAmounts: generateXpAmounts(rng, 3, 1000, 1500)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`,
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 1
}),
minEnemyLevel: 10,
maxEnemyLevel: 30,
xpAmounts: generateXpAmounts(rng, 3, 1750, 2250)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`,
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 2
}),
minEnemyLevel: 20,
maxEnemyLevel: 40,
xpAmounts: generateXpAmounts(rng, 4, 2500, 3000)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`,
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 3
}),
minEnemyLevel: 30,
maxEnemyLevel: 50,
xpAmounts: generateXpAmounts(rng, 5, 3250, 3750)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`,
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 4
}),
minEnemyLevel: 40,
maxEnemyLevel: 60,
xpAmounts: generateXpAmounts(rng, 5, 4000, 4500)
},
// U28.1
...(buildVersion >= gameToBuildVersionInt["28.3.0"]
? [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`,
masteryReq: 10,
minEnemyLevel: 100,
maxEnemyLevel: 100,
xpAmounts: [840, 840, 840, 840, 1660]
}
]
: []),
...(buildVersion >= gameToBuildVersionInt["31.0.0"]
? [
{
jobType: rng.randomElement(eidolonNarmerJobs),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable${table}Rewards`,
masteryReq: 0,
minEnemyLevel: 50,
maxEnemyLevel: 70,
xpAmounts: generateXpAmounts(rng, 5, 4500, 5000)
}
]
: [])
]
});
}
if (buildVersion >= gameToBuildVersionInt["24.0.0"]) {
const rng = new SRng(seed);
const pool = [...venusJobs];
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "SolarisSyndicate",
Seed: seed,
Nodes: [],
Jobs: [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`,
masteryReq: 0,
minEnemyLevel: 5,
maxEnemyLevel: 15,
xpAmounts: generateXpAmounts(rng, 3, 1000, 1500)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`,
masteryReq: 1,
minEnemyLevel: 10,
maxEnemyLevel: 30,
xpAmounts: generateXpAmounts(rng, 3, 1750, 2250)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`,
masteryReq: 2,
minEnemyLevel: 20,
maxEnemyLevel: 40,
xpAmounts: generateXpAmounts(rng, 4, 2500, 3000)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`,
masteryReq: 3,
minEnemyLevel: 30,
maxEnemyLevel: 50,
xpAmounts: generateXpAmounts(rng, 5, 3250, 3750)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`,
masteryReq: 5,
minEnemyLevel: 40,
maxEnemyLevel: 60,
xpAmounts: generateXpAmounts(rng, 5, 4000, 4500)
},
// U28.1
...(buildVersion >= gameToBuildVersionInt["28.3.0"]
? [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`,
masteryReq: 10,
minEnemyLevel: 100,
maxEnemyLevel: 100,
xpAmounts: [840, 840, 840, 840, 1660]
}
]
: []),
...(buildVersion >= gameToBuildVersionInt["31.0.0"]
? [
{
jobType: rng.randomElement(venusNarmerJobs),
rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards",
masteryReq: 0,
minEnemyLevel: 50,
maxEnemyLevel: 70,
xpAmounts: generateXpAmounts(rng, 5, 4500, 5000)
}
]
: [])
]
});
}
if (buildVersion >= gameToBuildVersionInt["29.0.0"]) {
const rng = new SRng(seed);
const pool = [...microplanetJobs];
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "EntratiSyndicate",
Seed: seed,
Nodes: [],
Jobs: [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`,
masteryReq: 0,
minEnemyLevel: 5,
maxEnemyLevel: 15,
xpAmounts: generateXpAmounts(rng, 3, 12, 18)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`,
masteryReq: 1,
minEnemyLevel: 15,
maxEnemyLevel: 25,
xpAmounts: generateXpAmounts(rng, 3, 24, 36)
},
{
jobType: rng.randomElement(microplanetEndlessJobs),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTable${table}Rewards`,
masteryReq: 5,
minEnemyLevel: 25,
maxEnemyLevel: 30,
endless: true,
xpAmounts: [14, 14, 14]
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`,
masteryReq: 2,
minEnemyLevel: 30,
maxEnemyLevel: 40,
xpAmounts: generateXpAmounts(rng, 4, 72, 88)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`,
masteryReq: 3,
minEnemyLevel: 40,
maxEnemyLevel: 60,
xpAmounts: generateXpAmounts(rng, 5, 115, 135)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`,
masteryReq: 10,
minEnemyLevel: 100,
maxEnemyLevel: 100,
xpAmounts: [25, 25, 25, 25, 50]
},
{
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATable${vaultTable}Rewards`,
masteryReq: 5,
minEnemyLevel: 30,
maxEnemyLevel: 40,
xpAmounts: [2, 2, 2, 4],
locationTag: "ChamberB",
isVault: true
},
{
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTable${vaultTable}Rewards`,
masteryReq: 5,
minEnemyLevel: 40,
maxEnemyLevel: 50,
xpAmounts: [4, 4, 4, 5],
locationTag: "ChamberA",
isVault: true
},
{
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTable${vaultTable}Rewards`,
masteryReq: 5,
minEnemyLevel: 50,
maxEnemyLevel: 60,
xpAmounts: [5, 5, 5, 7],
locationTag: "ChamberC",
isVault: true
}
]
});
}
};
const birthdays: number[] = [
1, // Kaya
45, // Lettie
74, // Minerva (MinervaVelemirDialogue_rom.dialogue)
143, // Amir
166, // Flare
191, // Aoi
306, // Eleanor
307, // Arthur
338, // Quincy
355 // Velimir (MinervaVelemirDialogue_rom.dialogue)
];
const getCalendarSeason = (week: number): ICalendarSeason => {
const seasonIndex = week % 4;
const seasonDay1 = [1, 91, 182, 274][seasonIndex];
const seasonDay91 = seasonDay1 + 90;
const eventDays: ICalendarDay[] = [];
for (const day of birthdays) {
if (day < seasonDay1) {
continue;
}
if (day >= seasonDay91) {
break;
}
logger.trace(`birthday on day ${day}`);
eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0
}
const rng = new SRng(new SRng(week).randomInt(0, 100_000));
const challenges = [
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTankHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeHard"
];
const rewardRanges: number[] = [];
const upgradeRanges: number[] = [];
for (let i = 0; i != 6; ++i) {
const chunkDay1 = seasonDay1 + i * 15;
const chunkDay13 = chunkDay1 - 1 + 13;
let challengeDay: number;
do {
challengeDay = rng.randomInt(chunkDay1, chunkDay13);
} while (birthdays.indexOf(challengeDay) != -1);
let challengeIndex;
let challenge;
do {
challengeIndex = rng.randomInt(0, challenges.length - 1);
challenge = challenges[challengeIndex];
} while (i < 2 && !challenge.endsWith("Easy")); // First 2 challenges should be easy
challenges.splice(challengeIndex, 1);
logger.trace(`challenge on day ${challengeDay}`);
eventDays.push({
day: challengeDay,
events: [{ type: "CET_CHALLENGE", challenge }]
});
rewardRanges.push(challengeDay);
if (i == 0 || i == 3 || i == 5) {
upgradeRanges.push(challengeDay);
}
}
rewardRanges.push(seasonDay91);
upgradeRanges.push(seasonDay91);
const rewards = [
"/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker",
"/Lotus/StoreItems/Types/Recipes/Components/FormaAuraBlueprint",
"/Lotus/StoreItems/Types/Recipes/Components/FormaBlueprint",
"/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker",
"/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle",
"/Lotus/StoreItems/Types/BoosterPacks/CalendarRivenPack",
"/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall",
"/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleLarge",
"/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack",
"/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack",
"/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem",
"/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem",
"/Lotus/Types/StoreItems/Boosters/ResourceDropChance3DayStoreItem",
"/Lotus/StoreItems/Types/Items/MiscItems/Forma",
"/Lotus/StoreItems/Types/Recipes/Components/OrokinCatalystBlueprint",
"/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker",
"/Lotus/Types/StoreItems/Packages/Calendar/CalendarVosforPack",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalOrange",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalGreen",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalViolet"
];
for (let i = 0; i != rewardRanges.length - 1; ++i) {
const events: ICalendarEvent[] = [];
for (let j = 0; j != 2; ++j) {
const rewardIndex = rng.randomInt(0, rewards.length - 1);
events.push({ type: "CET_REWARD", reward: rewards[rewardIndex] });
rewards.splice(rewardIndex, 1);
}
logger.trace(`trying to fit rewards between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`);
let day: number;
do {
day = rng.randomInt(rewardRanges[i] + 1, rewardRanges[i + 1] - 1);
} while (eventDays.find(x => x.day == day));
eventDays.push({ day, events });
}
const upgradesByHexMember = [
[
"/Lotus/Upgrades/Calendar/AttackAndMovementSpeedOnCritMelee",
"/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump",
"/Lotus/Upgrades/Calendar/ElectricDamagePerDistance",
"/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance",
"/Lotus/Upgrades/Calendar/OvershieldCap",
"/Lotus/Upgrades/Calendar/SpeedBuffsWhenAirborne"
],
[
"/Lotus/Upgrades/Calendar/AbilityStrength",
"/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange",
"/Lotus/Upgrades/Calendar/MagnetStatusPull",
"/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts",
"/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent",
"/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts"
],
[
"/Lotus/Upgrades/Calendar/EnergyWavesOnCombo",
"/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier",
"/Lotus/Upgrades/Calendar/MeleeAttackSpeed",
"/Lotus/Upgrades/Calendar/MeleeCritChance",
"/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit",
"/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy"
],
[
"/Lotus/Upgrades/Calendar/Armor",
"/Lotus/Upgrades/Calendar/CloneActiveCompanionForEnergySpent",
"/Lotus/Upgrades/Calendar/CompanionDamage",
"/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer",
"/Lotus/Upgrades/Calendar/CompanionsRadiationChance",
"/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage",
"/Lotus/Upgrades/Calendar/ReviveEnemyAsSpectreOnKill"
],
[
"/Lotus/Upgrades/Calendar/EnergyOrbsGrantShield",
"/Lotus/Upgrades/Calendar/EnergyRestoration",
"/Lotus/Upgrades/Calendar/ExplodingHealthOrbs",
"/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill",
"/Lotus/Upgrades/Calendar/HealingEffects",
"/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup"
],
[
"/Lotus/Upgrades/Calendar/BlastEveryXShots",
"/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary",
"/Lotus/Upgrades/Calendar/GuidingMissilesChance",
"/Lotus/Upgrades/Calendar/MagazineCapacity",
"/Lotus/Upgrades/Calendar/PunchToPrimary",
"/Lotus/Upgrades/Calendar/RefundBulletOnStatusProc",
"/Lotus/Upgrades/Calendar/StatusChancePerAmmoSpent"
]
];
for (let i = 0; i != upgradeRanges.length - 1; ++i) {
// Pick 3 unique hex members
const hexMembersPickedForThisDay: number[] = [];
for (let j = 0; j != 3; ++j) {
let hexMemberIndex: number;
do {
hexMemberIndex = rng.randomInt(0, upgradesByHexMember.length - 1);
} while (hexMembersPickedForThisDay.indexOf(hexMemberIndex) != -1);
hexMembersPickedForThisDay.push(hexMemberIndex);
}
hexMembersPickedForThisDay.sort(); // Always present them in the same order
// For each hex member, pick an upgrade that was not yet picked this season.
const events: ICalendarEvent[] = [];
for (const hexMemberIndex of hexMembersPickedForThisDay) {
const upgrades = upgradesByHexMember[hexMemberIndex];
const upgradeIndex = rng.randomInt(0, upgrades.length - 1);
events.push({ type: "CET_UPGRADE", upgrade: upgrades[upgradeIndex] });
upgrades.splice(upgradeIndex, 1);
}
logger.trace(`trying to fit upgrades between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`);
let day: number;
do {
day = rng.randomInt(upgradeRanges[i] + 1, upgradeRanges[i + 1] - 1);
} while (eventDays.find(x => x.day == day));
eventDays.push({ day, events });
}
eventDays.sort((a, b) => a.day - b.day);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
return {
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Days: eventDays,
Season: (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[seasonIndex],
YearIteration: Math.trunc(week / 4),
Version: 19,
UpgradeAvaliabilityRequirements: ["/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement"]
};
};
// Not very faithful, but to avoid the same node coming up back-to-back (which is not valid), I've split these into 2 arrays which we're alternating between.
const voidStormMissions = {
VoidT1: [
"CrewBattleNode519",
"CrewBattleNode518",
"CrewBattleNode515",
"CrewBattleNode503",
"CrewBattleNode509",
"CrewBattleNode522",
"CrewBattleNode511",
"CrewBattleNode512"
],
VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530", "CrewBattleNode535", "CrewBattleNode533"],
VoidT3: ["CrewBattleNode521", "CrewBattleNode516", "CrewBattleNode524", "CrewBattleNode525"],
VoidT4: [
"CrewBattleNode555",
"CrewBattleNode553",
"CrewBattleNode554",
"CrewBattleNode539",
"CrewBattleNode531",
"CrewBattleNode527",
"CrewBattleNode542",
"CrewBattleNode538",
"CrewBattleNode543",
"CrewBattleNode536",
"CrewBattleNode550",
"CrewBattleNode529"
]
} as const;
const voidStormLookbehind = {
VoidT1: 3,
VoidT2: 1,
VoidT3: 1,
VoidT4: 3
} as const;
const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
const expiry = activation + 90 * unixTimesInMs.minute;
let accum = 0;
const tierIdx = { VoidT1: hour * 2, VoidT2: hour, VoidT3: hour, VoidT4: hour * 2 };
for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) {
arr.push({
_id: {
$oid:
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"0321e89b" +
(accum++).toString().padStart(8, "0")
},
Node: sequentiallyUniqueRandomElement(
voidStormMissions[tier],
tierIdx[tier]++,
voidStormLookbehind[tier],
2051969264
)!,
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
ActiveMissionTier: tier
});
}
};
interface ITimeConstraint {
name: string;
isValidTime: (timeSecs: number) => boolean;
getIdealTimeBefore: (timeSecs: number) => number;
}
const eidolonDayConstraint: ITimeConstraint = {
name: "eidolon day",
isValidTime: (timeSecs: number): boolean => {
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / eidolonCycleDuration);
const eidolonCycleStart = eidolonEpoch + eidolonCycle * eidolonCycleDuration;
const hour = 24 * (((timeSecs - eidolonEpoch) % eidolonCycleDuration) / eidolonCycleDuration);
// const isDay = hour > 5 && hour < 21.9;
if (hour > 5) {
const eidolonCycleNightStart = eidolonCycleStart + 21.9 * eidolonHourDuration;
return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000);
}
return false;
},
getIdealTimeBefore: (timeSecs: number): number => {
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / eidolonCycleDuration);
const eidolonCycleStart = eidolonEpoch + eidolonCycle * eidolonCycleDuration;
const hour = 24 * (((timeSecs - eidolonEpoch) % eidolonCycleDuration) / eidolonCycleDuration);
if (hour > 5) {
return Math.trunc(eidolonCycleStart + 5 * eidolonHourDuration); // Today's morning
} else {
return Math.trunc(eidolonCycleStart - eidolonCycleDuration + 5 * eidolonHourDuration); // Yesterday's morning
}
}
};
const eidolonNightConstraint: ITimeConstraint = {
name: "eidolon night",
isValidTime: (timeSecs: number): boolean => {
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / eidolonCycleDuration);
const eidolonCycleStart = eidolonEpoch + eidolonCycle * eidolonCycleDuration;
const hour = 24 * (((timeSecs - eidolonEpoch) % eidolonCycleDuration) / eidolonCycleDuration);
if (hour > 5) {
const eidolonCycleNightStart = eidolonCycleStart + 21.9 * eidolonHourDuration;
return timeSecs >= eidolonCycleNightStart;
} else {
const eidolonCycleDayStart = eidolonCycleStart + 5 * eidolonHourDuration;
return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleDayStart * 1000);
}
},
getIdealTimeBefore: (timeSecs: number): number => {
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / eidolonCycleDuration);
const eidolonCycleStart = eidolonEpoch + eidolonCycle * eidolonCycleDuration;
const eidolonCycleNightStart = eidolonCycleStart + 21.9 * eidolonHourDuration;
if (eidolonCycleNightStart > timeSecs) {
// Night hasn't started yet, but we need to return a time in the past.
return Math.trunc(eidolonCycleNightStart - eidolonCycleDuration);
}
return Math.trunc(eidolonCycleNightStart);
}
};
const venusColdConstraint: ITimeConstraint = {
name: "venus cold",
isValidTime: (timeSecs: number): boolean => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
const vallisCycleEnd = vallisCycleStart + 1600;
const vallisCycleColdStart = vallisCycleStart + 400;
return (
timeSecs >= vallisCycleColdStart &&
!isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000)
);
},
getIdealTimeBefore: (timeSecs: number): number => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
const vallisCycleColdStart = vallisCycleStart + 400;
if (vallisCycleColdStart > timeSecs) {
// Cold hasn't started yet, but we need to return a time in the past.
return vallisCycleColdStart - 1600;
}
return vallisCycleColdStart;
}
};
const venusWarmConstraint: ITimeConstraint = {
name: "venus warm",
isValidTime: (timeSecs: number): boolean => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
const vallisCycleColdStart = vallisCycleStart + 400;
return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000);
},
getIdealTimeBefore: (timeSecs: number): number => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
return vallisCycleStart;
}
};
const getIdealTimeSatsifyingConstraints = (constraints: ITimeConstraint[]): number => {
let timeSecs = Math.trunc(Date.now() / 1000);
let allGood;
do {
allGood = true;
for (const constraint of constraints) {
if (!constraint.isValidTime(timeSecs)) {
logger.trace(`${constraint.name} is not happy with ${timeSecs}`);
const prevTimeSecs = timeSecs;
const suggestion = constraint.getIdealTimeBefore(timeSecs);
timeSecs = suggestion;
do {
timeSecs += 60;
if (timeSecs >= prevTimeSecs || !constraint.isValidTime(timeSecs)) {
timeSecs = suggestion; // Can't find a compromise; just take the suggestion and try to compromise on another constraint.
break;
}
} while (!constraints.every(constraint => constraint.isValidTime(timeSecs)));
allGood = false;
break;
}
}
} while (!allGood);
return timeSecs;
};
const fullyStockBaro = (vt: IVoidTrader, buildVersion: number): void => {
const tempManifest: IBaroDataTradeOffer[] = [];
for (const item of baro.evilBaro) {
tempManifest.push(item);
}
for (const armorSet of baro.armorSets) {
if (armorSet.bundle) {
tempManifest.push(armorSet.bundle);
}
tempManifest.push(...armorSet.items);
}
for (const item of baro.rest) {
tempManifest.push(item);
}
for (const item of baro.evergreen) {
tempManifest.push(item);
}
vt.Manifest.push(
...tempManifest
.filter(({ minBuildVersionInt }) => buildVersion >= minBuildVersionInt)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ minBuildVersionInt, ...offer }) => offer)
);
};
const getVarziaRotation = (week: number, buildVersion: number): string => {
const seed = new SRng(week).randomInt(0, 100_000);
const rng = new SRng(seed);
const [itemType, rotation] = rng.randomElement(Object.entries(varzia.primeDualPacks))!;
if (buildVersion < rotation.minBuildVersionInt) {
return itemType;
} else {
return "/Lotus/StoreItems/Types/StoreItems/Packages/MegaPrimeVault/LastChanceItemC";
}
};
const getVarziaManifest = (dualPack: string, buildVersion: number): IPrimeVaultTraderOffer[] => {
const rotationManifest = varzia.primeDualPacks[dualPack];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!rotationManifest || buildVersion < rotationManifest.minBuildVersionInt) {
return [];
}
const mainPack = [{ ItemType: dualPack, PrimePrice: 10 }];
const singlePacks: IPrimeVaultTraderOffer[] = [];
const vanguardRelics: IPrimeVaultTraderOffer[] = [];
const items: IPrimeVaultTraderOffer[] = [];
const bobbleHeads: IPrimeVaultTraderOffer[] = [];
if (config.worldState?.vanguardVaultRelics && buildVersion >= gameToBuildVersionInt["41.0.0"]) {
vanguardRelics.push(...varzia.vanguardVaultRelics);
}
for (const singlePackType of rotationManifest.SinglePacks) {
singlePacks.push({ ItemType: singlePackType, PrimePrice: 6 });
const sp = varzia.primeSinglePacks[singlePackType];
items.push(...sp.Items);
sp.BobbleHeads.forEach(bobbleHead => {
bobbleHeads.push({ ItemType: bobbleHead, PrimePrice: 1 });
});
}
const relics = rotationManifest.Relics.map(relic => ({ ItemType: relic, RegularPrice: 1 }));
return [singlePacks[0], ...mainPack, singlePacks[1], ...vanguardRelics, ...items, ...bobbleHeads, ...relics];
};
const getAllVarziaManifests = (buildVersion: number): IPrimeVaultTraderOffer[] => {
const dualPacks: IPrimeVaultTraderOffer[] = [];
const singlePacks: IPrimeVaultTraderOffer[] = [];
const vanguardRelics: IPrimeVaultTraderOffer[] = [];
const items: IPrimeVaultTraderOffer[] = [];
const bobbleHeads: IPrimeVaultTraderOffer[] = [];
const relics: IPrimeVaultTraderOffer[] = [];
const singlePackSet = new Set<string>();
const itemsSet = new Set<string>();
const bobbleHeadsSet = new Set<string>();
if (config.worldState?.vanguardVaultRelics && buildVersion >= gameToBuildVersionInt["41.0.0"]) {
vanguardRelics.push(...varzia.vanguardVaultRelics);
}
Object.entries(varzia.primeDualPacks)
.filter(([_, dualPack]) => buildVersion >= dualPack.minBuildVersionInt)
.forEach(([dualPackItemType, dualPack]) => {
dualPacks.push({ ItemType: dualPackItemType, PrimePrice: 10 });
dualPack.SinglePacks.forEach(singlePackKey => {
if (!singlePackSet.has(singlePackKey)) {
singlePackSet.add(singlePackKey);
singlePacks.push({ ItemType: singlePackKey, PrimePrice: 6 });
}
const sp = varzia.primeSinglePacks[singlePackKey];
sp.Items.forEach(item => {
if (!itemsSet.has(item.ItemType)) {
itemsSet.add(item.ItemType);
items.push(item);
}
});
sp.BobbleHeads.forEach(bobbleHead => {
if (!bobbleHeadsSet.has(bobbleHead)) {
bobbleHeadsSet.add(bobbleHead);
bobbleHeads.push({ ItemType: bobbleHead, PrimePrice: 1 });
}
});
});
relics.push(...dualPack.Relics.map(relic => ({ ItemType: relic, RegularPrice: 1 })));
});
return [...dualPacks, ...vanguardRelics, ...singlePacks, ...items, ...bobbleHeads, ...relics];
};
const createInvasion = (day: number, idx: number, buildVersion: number): IInvasion => {
const id = day * 3 + idx;
const defender = (["FC_GRINEER", "FC_CORPUS", day % 2 ? "FC_GRINEER" : "FC_CORPUS"] as const)[idx];
const rng = new SRng(new SRng(id).randomInt(0, 1_000_000));
const isInfestationOutbreak = rng.randomInt(0, 1) == 0;
const attacker = isInfestationOutbreak ? "FC_INFESTATION" : defender == "FC_GRINEER" ? "FC_CORPUS" : "FC_GRINEER";
const startMs = EPOCH + day * 86400_000;
const oid =
((startMs / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"fd148cb8" +
(idx & 0xffffffff).toString(16).padStart(8, "0");
const node = sequentiallyUniqueRandomElement(invasionNodes[defender], id, 5, 690175)!; // Can't repeat the other 2 on this day nor the last 3
const progress = (Date.now() - startMs) / 86400_000;
const countMultiplier = isInfestationOutbreak || rng.randomInt(0, 1) ? -1 : 1; // if defender is winning, count is negative
const fiftyPercent = rng.randomInt(1000, 29000); // introduce some 'yitter' for the percentages
const rewardFloat = rng.randomFloat();
const rewardTier = rewardFloat < 0.201 ? "RARE" : rewardFloat < 0.7788 ? "COMMON" : "UNCOMMON";
const attackerReward: IMissionReward = {};
const defenderReward: IMissionReward = {};
if (isInfestationOutbreak) {
defenderReward.countedItems = [
rng.randomElement(invasionRewards[rng.randomInt(0, 1) ? "FC_INFESTATION" : defender][rewardTier])!
];
} else {
attackerReward.countedItems = [rng.randomElement(invasionRewards[attacker][rewardTier])!];
defenderReward.countedItems = [rng.randomElement(invasionRewards[defender][rewardTier])!];
}
return {
_id: toOid2(oid, buildVersion),
Faction: attacker,
DefenderFaction: defender,
Node: node,
Count: Math.round(
(progress < 0.5 ? progress * 2 * fiftyPercent : fiftyPercent + (30_000 - fiftyPercent) * (progress - 0.5)) *
countMultiplier
),
Goal: 30000, // Value seems to range from 30000 to 98000 in intervals of 1000. Higher values are increasingly rare. I don't think this is relevant for the frontend besides dividing count by it.
LocTag: isInfestationOutbreak
? ExportRegions[node].missionType == "MT_ASSASSINATION"
? "/Lotus/Language/Menu/InfestedInvasionBoss"
: "/Lotus/Language/Menu/InfestedInvasionGeneric"
: attacker == "FC_CORPUS"
? "/Lotus/Language/Menu/CorpusInvasionGeneric"
: "/Lotus/Language/Menu/GrineerInvasionGeneric",
Completed: startMs + 86400_000 < Date.now(), // Sorta unfaithful. Invasions on live are (at least in part) in fluenced by people completing them. And otherwise also probably not hardcoded to last 24 hours.
ChainID: { $oid: oid },
AttackerReward: attackerReward,
AttackerMissionInfo: {
seed: rng.randomInt(0, 1_000_000),
faction: defender
},
DefenderReward: defenderReward,
DefenderMissionInfo: {
seed: rng.randomInt(0, 1_000_000),
faction: attacker
},
Activation: toMongoDate2(startMs, buildVersion)
};
};
export const getInvasionByOid = (oid: string): IInvasion | undefined => {
const arr = oid.split("fd148cb8");
if (arr.length == 2 && arr[0].length == 8 && arr[1].length == 8) {
return createInvasion(idToDay(oid), parseInt(arr[1], 16), BV_LATEST);
}
return undefined;
};
export const getWorldState = (
buildLabel: string = BL_LATEST,
convertGoals: boolean = true,
changeLegacyTags: boolean = config.unfaithfulBugFixes?.useAnniversaryTagForOldGoals || false
): IWorldState => {
const buildVersion = buildVersionToInt(buildLabel);
const constraints: ITimeConstraint[] = [];
if (config.worldState?.eidolonOverride) {
constraints.push(config.worldState.eidolonOverride == "day" ? eidolonDayConstraint : eidolonNightConstraint);
}
if (config.worldState?.vallisOverride) {
constraints.push(config.worldState.vallisOverride == "cold" ? venusColdConstraint : venusWarmConstraint);
}
if (config.worldState?.duviriOverride) {
const duviriMoods = ["sorrow", "fear", "joy", "anger", "envy"];
const desiredMood = duviriMoods.indexOf(config.worldState.duviriOverride);
if (desiredMood == -1) {
logger.warn(`ignoring invalid config value for worldState.duviriOverride`, {
value: config.worldState.duviriOverride,
valid_values: duviriMoods
});
} else {
constraints.push({
name: `duviri ${config.worldState.duviriOverride}`,
isValidTime: (timeSecs: number): boolean => {
const moodIndex = Math.trunc(timeSecs / 7200);
return moodIndex % 5 == desiredMood;
},
getIdealTimeBefore: (timeSecs: number): number => {
let moodIndex = Math.trunc(timeSecs / 7200);
moodIndex -= ((moodIndex % 5) - desiredMood + 5) % 5; // while (moodIndex % 5 != desiredMood) --moodIndex;
const moodStart = moodIndex * 7200;
return moodStart;
}
});
}
}
const timeSecs = getIdealTimeSatsifyingConstraints(constraints);
if (constraints.length != 0) {
const delta = Math.trunc(Date.now() / 1000) - timeSecs;
if (delta > 1) {
logger.debug(
`reported time is ${delta} seconds behind real time to satisfy selected constraints (${constraints.map(x => x.name).join(", ")})`
);
}
}
const timeMs = timeSecs * 1000;
const day = Math.trunc((timeMs - EPOCH) / 86400000);
const week = Math.trunc(day / 7);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const date = new Date(timeMs);
const defenseWavesPerRotation = buildVersion < gameToBuildVersionInt["38.5.0"] ? 5 : 3;
const worldState: IWorldState = {
Version: 10,
BuildLabel: buildLabel,
Time: timeSecs,
Goals: [],
Events: [],
Alerts: [],
Sorties: [],
LiteSorties: [],
ActiveMissions: [],
FlashSales: [],
GlobalUpgrades: [],
Invasions: [],
VoidTraders: [],
PrimeVaultTraders: [],
VoidStorms: [],
DailyDeals: [],
//EndlessXpChoices: [],
//EndlessXpSchedule: [],
KnownCalendarSeasons: [],
PVPChallengeInstances: [],
FeaturedGuilds: [],
NodeOverrides: [],
LibraryInfo: { LastCompletedTargetType: "/Lotus/Types/Game/Library/Targets/Research7Target" },
PrimeVaultAvailabilities: [false, false, false, false, false],
PrimeTokenAvailability: true,
PrimeAccessAvailability: { State: "PRIME1" },
PersistentEnemies: [],
PVPAlternativeModes: [],
PVPActiveTournaments: [],
ConstructionProjects: [],
ProjectPct: [0, 0, 0],
TwitchPromos: [],
ForceLogoutVersion: 0,
ExperimentRecommended: [],
SyndicateMissions: [],
InGameMarket: {
LandingPage: {
Categories: [
{
CategoryName: "NEW_PLAYER",
Name: "/Lotus/Language/Store/NewPlayerCategoryTitle",
Icon: "newplayer",
AddToMenu: true,
Items: [
"/Lotus/Types/StoreItems/Packages/2024Bundles/WeaponStarterPack",
"/Lotus/StoreItems/Powersuits/MonkeyKing/MonkeyKing",
"/Lotus/StoreItems/Weapons/Tenno/Melee/SwordsAndBoards/MeleeContestWinnerOne/TennoSwordShield",
"/Lotus/StoreItems/Upgrades/Skins/Effects/WerewolfEphemera",
"/Lotus/StoreItems/Types/StoreItems/SlotItems/TwoWeaponSlotItem",
"/Lotus/StoreItems/Powersuits/Wisp/Wisp",
"/Lotus/StoreItems/Weapons/Tenno/Shotgun/Shotgun",
"/Lotus/StoreItems/Powersuits/Rhino/Rhino",
"/Lotus/StoreItems/Weapons/Corpus/Pistols/CrpAirPistol/CrpAirPistolArray",
"/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem"
]
},
{
CategoryName: "NEW",
Name: "/Lotus/Language/Menu/Store_New",
Icon: "new",
Items: []
},
{
CategoryName: "POPULAR",
Name: "/Lotus/Language/Menu/StorePopular",
Icon: "popular",
AddToMenu: true,
Items: [
"/Lotus/Types/StoreItems/Packages/2025Bundles/TC2025DigitalPack",
"/Lotus/Types/StoreItems/Packages/2025Bundles/EncoreCompSupPack",
"/Lotus/Types/StoreItems/Packages/2025Bundles/EncoreGeminiSupPack",
"/Lotus/Types/StoreItems/Packages/WarframeBundles/TempleItemsBundle",
"/Lotus/Types/StoreItems/Packages/FormaPack",
"/Lotus/StoreItems/Upgrades/Skins/Saryn/WF1999SarynSkin",
"/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DaxDuviriKatana/DaxDuviriKatanaWeapon",
"/Lotus/StoreItems/Upgrades/Skins/Jade/WF1999NyxSkin",
"/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/NinjaColourPickerItem",
"/Lotus/StoreItems/Upgrades/Skins/Mag/WF1999MagSkin",
"/Lotus/StoreItems/Upgrades/Skins/Frost/WF1999FrostSkin",
"/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DaxDuviriTwoHandedKatana/DaxDuviriTwoHandedKatanaWeapon",
"/Lotus/StoreItems/Upgrades/Skins/Harlequin/MirageDeluxeSkin",
"/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/DaxDuviriHammer/DaxDuviriHammerWeapon"
]
},
{
CategoryName: "SEASONAL",
Name: "/Lotus/Language/Store/SeasonalCategoryTitle",
Icon: "seasonal",
AddToMenu: true,
Items: []
},
{
CategoryName: "COMMUNITY",
Name: "/Lotus/Language/Store/CommunityCategoryTitle",
Icon: "community",
AddToMenu: true,
Items: []
},
{
CategoryName: "HEIRLOOM",
Name: "/Lotus/Language/Store/HeirloomCategoryTitle",
Icon: "heirloom",
AddToMenu: true,
Items: [
"/Lotus/StoreItems/Upgrades/Skins/Berserker/ValkyrHeirloomSkin",
"/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomValkyrCrown",
"/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerValkyrHeirloom",
"/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardValkyrHeirloom",
"/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomValkyrGlyph",
"/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomValkyrSigil",
"/Lotus/Types/StoreItems/Packages/HeirloomPackRhino",
"/Lotus/StoreItems/Upgrades/Skins/Rhino/RhinoHeirloomSkin",
"/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomRhinoCrown",
"/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerRhinoHeirloom",
"/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardRhinoHeirloom",
"/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomRhinoGlyph",
"/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomRhinoSigil",
"/Lotus/Types/StoreItems/Packages/HeirloomPackEmber",
"/Lotus/StoreItems/Upgrades/Skins/Ember/EmberHeirloomSkin",
"/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomEmberCrown",
"/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerEmberHeirloom",
"/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardEmberHeirloom",
"/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomEmberGlyph",
"/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomEmberSigil"
]
},
{
CategoryName: "TENNOGEN",
Name: "/Lotus/Language/Menu/Store_Tennogen",
Icon: "tennogen",
AddToMenu: true,
Items: [
"/Lotus/StoreItems/Upgrades/Skins/Armor/SWEndocitosShoulderArmor/SWEndocitosShoulderArmorA",
"/Lotus/StoreItems/Upgrades/Skins/Scarves/SWLunariusSyandana",
"/Lotus/StoreItems/Upgrades/Skins/Scarves/SWRauSyandana",
"/Lotus/StoreItems/Upgrades/Skins/Hoplite/SWStyanaxHuzarrSkin",
"/Lotus/StoreItems/Upgrades/Skins/Werewolf/VorunaDemionnaSkin"
]
},
{
CategoryName: "SALE",
Name: "/Lotus/Language/Menu/Store_Sale",
Icon: "sale",
AddToMenu: true,
Items: []
},
{
CategoryName: "WISH_LIST",
Name: "/Lotus/Language/Menu/Store_Wishlist",
Icon: "wishlist",
Items: []
},
{
CategoryName: "QUICK_BUY",
Name: "/Lotus/Language/Store/TopSeller_Title",
Icon: "quickbuy",
Items: [
"/Lotus/Types/StoreItems/Packages/FormaPack",
"/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker"
]
}
]
}
}
};
worldState.Events.push({
Msg: "Join the OpenWF Discord!",
Messages: [
{ LanguageCode: "fr", Message: "Rejoignez le Discord OpenWF!" },
{ LanguageCode: "it", Message: "Unisciti al Discord di OpenWF!" },
{ LanguageCode: "de", Message: "Trete dem OpenWF Discord bei!" },
{ LanguageCode: "es", Message: "Únete al Discord de OpenWF!" },
{ LanguageCode: "pt", Message: "Junte-se ao Discord do OpenWF!" },
{ LanguageCode: "ru", Message: "Присоединяйтесь к OpenWF Discord!" },
{ LanguageCode: "pl", Message: "Dołącz do Discord OpenWF!" },
{ LanguageCode: "uk", Message: "Приєднуйтесь до OpenWF Discord!" },
{ LanguageCode: "tr", Message: "OpenWF Discord'a katıl!" },
{ LanguageCode: "ja", Message: "OpenWFのDiscordに参加しよう!" },
{ LanguageCode: "zh", Message: "加入OpenWF Discord!" },
{ LanguageCode: "ko", Message: "OpenWF Discord에 가입하세요!" },
{ LanguageCode: "tc", Message: "加入OpenWF Discord!" }
],
Prop: "https://discord.gg/PNNZ3asUuY",
Icon: "/Lotus/Interface/Icons/DiscordIconNoBacker.png"
});
if (config.worldState?.tennoLiveRelay) {
worldState.Goals.push({
_id: toOid2("687bf9400000000000000000", buildVersion),
Activation: toMongoDate2(1752955200000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Locations/RelayStationTennoConB",
ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDescB",
Icon: "/Lotus/Interface/Icons/Categories/IconTennoLive.png",
Tag: "TennoConRelayB",
Node: "TennoConBHUB6"
});
}
if (config.worldState?.baroTennoConRelay) {
worldState.Goals.push({
_id: toOid2("687bb2f00000000000000000", buildVersion),
Activation: toMongoDate2(1752937200000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
//"Faction": "FC_GRINEER",
Desc: "/Lotus/Language/Locations/RelayStationTennoCon",
ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDesc",
Icon:
buildVersion >= gameToBuildVersionInt["43.0.0"]
? "/Lotus/Interface/Icons/Categories/IconTennoConBaroVip.png"
: "/Lotus/Interface/Icons/Categories/IconTennoConSigil.png",
Tag: "TennoConRelay",
Node: "TennoConHUB2"
});
const vt: IVoidTrader = {
_id: toOid2("687809030379266d790495c6", buildVersion),
Activation: toMongoDate2(1752937200000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Character: "Baro'Ki Teel",
Node: "TennoConHUB2",
Manifest: []
};
worldState.VoidTraders.push(vt);
fullyStockBaro(vt, buildVersion);
}
if (config.worldState) {
for (const [key, alert] of Object.entries(configAlerts)) {
if (config.worldState[key as keyof typeof config.worldState]) {
const { minBuildVersion, ...wsAlert } = alert;
if (!minBuildVersion || buildVersion >= minBuildVersion) {
if (wsAlert.MissionInfo.missionType == "MT_DEFENSE") {
wsAlert.MissionInfo.maxWaveNum =
defenseWavesPerRotation * (wsAlert.MissionInfo.maxRotations ?? 1);
wsAlert.MissionInfo.maxRotations = undefined;
}
worldState.Alerts.push(wsAlert);
}
}
}
}
if (config.worldState?.qtccAlerts) {
const activation = 1759327200000;
const expiry = 2000000000000;
worldState.Alerts.push(
{
_id: toOid2("68dc23c42e9d3acfa708ff3b", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
MissionInfo: {
location: "SolNode123",
missionType: "MT_SURVIVAL",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/Plushy2021QTCC"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipSurvivalRaid",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipSurvivalA",
minEnemyLevel: 20,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/TennoUnitedAlert",
maxWaveNum: 5
},
Tag: "LotusGift",
ForceUnlock: true
},
{
_id: toOid2("68dc2466e298b4f04206687a", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
MissionInfo: {
location: "SolNode149",
missionType: "MT_DEFENSE",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/Plushy2022QTCC"]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerShipyardsDefense",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerShipyardsDefenseA",
minEnemyLevel: 20,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/TennoUnitedAlert",
maxWaveNum: defenseWavesPerRotation * 1
},
Tag: "LotusGift",
ForceUnlock: true
},
{
_id: toOid2("68dc26865e7cb56b820b4252", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
MissionInfo: {
location: "SolNode39",
missionType: "MT_EXCAVATE",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyVirminkQTCC"]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerForestExcavation",
enemySpec: "/Lotus/Types/Game/EnemySpecs/ForestGrineerExcavationA",
minEnemyLevel: 20,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/TennoUnitedAlert",
maxWaveNum: 5
},
Tag: "LotusGift",
ForceUnlock: true
}
);
pushFlashSales(worldState, qqtcFlashSales, activation, expiry, "COMMUNITY", buildVersion);
}
const isFebruary = date.getUTCMonth() == 1;
if ((config.worldState?.starDaysOverride ?? isFebruary) && buildVersion >= gameToBuildVersionInt["29.10.0"]) {
worldState.Goals.push({
_id: toOid2("67a4dcce2a198564d62e1647", buildVersion),
Activation: {
$date: {
$numberLong: config.worldState?.starDaysOverride
? "1738868400000"
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1).toString()
}
},
Expiry: {
$date: {
$numberLong: config.worldState?.starDaysOverride
? "2000000000000"
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1).toString()
}
},
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Events/ValentinesFortunaName",
ToolTip: "/Lotus/Language/Events/ValentinesFortunaName",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/ValentinesEventIcon.png",
Tag: "FortunaValentines",
Node: "SolarisUnitedHub1"
});
}
if (buildVersion >= gameToBuildVersionInt["38.6.0"]) {
// The client gets kinda confused when multiple goals have the same tag, so considering these mutually exclusive.
if (config.worldState?.galleonOfGhouls == 1) {
worldState.Goals.push({
_id: toOid2("6814ddf00000000000000000", buildVersion),
Activation: toMongoDate2(1746198000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode19",
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlert",
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
Tag: "GalleonRobbery",
Reward: {
items: [
"/Lotus/StoreItems/Types/Recipes/Weapons/GrnChainSawTonfaBlueprint",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
} else if (config.worldState?.galleonOfGhouls == 2) {
worldState.Goals.push({
_id: toOid2("681e18700000000000000000", buildVersion),
Activation: toMongoDate2(1746802800000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode28", // Incompatible with Wolf Hunt, Orphix Venom, Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertB",
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
Tag: "GalleonRobbery",
Reward: {
items: [
"/Lotus/StoreItems/Types/Recipes/Weapons/MortiforShieldAndSwordBlueprint",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
} else if (config.worldState?.galleonOfGhouls == 3) {
worldState.Goals.push({
_id: toOid2("682752f00000000000000000", buildVersion),
Activation: toMongoDate2(1747407600000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode19",
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertC",
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
Tag: "GalleonRobbery",
Reward: {
items: [
"/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
}
}
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) &&
buildVersion >= gameToBuildVersionInt["22.7.0"]
) {
worldState.Goals.push({
_id: toOid2("654a5058c757487cdb11824f", buildVersion),
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"
});
}
const firstAugustWeekday = new Date(Date.UTC(date.getUTCFullYear(), 7, 1)).getUTCDay();
const firstAugustWednesdayOffset = (3 - firstAugustWeekday + 7) % 7;
const dogDaysStart = 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 dogDaysEnd = Date.UTC(date.getUTCFullYear(), 8, 1 + firstSeptemberWednesdayOffset, 15);
const isDogDaysActive = timeMs >= dogDaysStart && timeMs < dogDaysEnd;
if ((config.worldState?.dogDaysOverride ?? isDogDaysActive) && buildVersion >= gameToBuildVersionInt["25.7.0"]) {
const activation = config.worldState?.dogDaysOverride ? 1699372800000 : dogDaysStart;
const expiry = config.worldState?.dogDaysOverride ? 2000000000000 : dogDaysEnd;
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"], // 2023.07.26.16.38
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 25
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyBeachKavat"], // 2024.02.16.17.13
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 50
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyRucksackKubrow"], // 2024.02.16.17.13
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 75
}
]
},
{
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneBeachcomber"], // 2023.07.26.16.38
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 100
}
]
}
],
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageDogDays2024Glyph"], // 2024.07.17.17.07
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 25
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/DogDays2024Poster"], // 2024.07.17.17.07
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 50
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/DogDaysKubrowBadgeItem"], // 2024.07.17.17.07
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 75
}
]
},
{
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/DogDays2024LisetPropCleaningDroneBeachcomber"], // 2024.07.17.17.07
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 100
}
]
}
],
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysHydroidGlyph"], // 2025.06.23.11.39
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 25
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysLokiGlyph"], // 2025.06.23.11.39
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 50
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysNovaGlyph"], // 2025.06.23.11.39
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 75
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysValkyrGlyph"], // 2025.06.23.11.39
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 100
}
]
}
],
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysKelaGlyph"], // 2026.07.09.15.36
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 25
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysRusalkaGlyph"], // 2026.07.09.15.36
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 50
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysErraGlyph"], // 2026.07.09.15.36
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 75
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysGareshGlyph"], // 2026.07.09.15.36
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 100
}
]
}
]
];
const fallbackYear =
buildVersion >= gameToBuildVersionInt["43.0.0"]
? 4
: buildVersion >= gameToBuildVersionInt["39.0.0"]
? 3
: buildVersion >= gameToBuildVersionInt["36.1.2"]
? 2
: buildVersion >= gameToBuildVersionInt["33.6.0"]
? 1
: 0;
const year = config.worldState?.dogDaysRewardsOverride ?? fallbackYear;
worldState.Goals.push({
_id: {
$oid:
((dogDaysStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"c57487c3768936d" +
year.toString(16)
},
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
Count: 0,
Goal: 100,
InterimGoals: [25, 50],
BonusGoal: 200,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode25", // Incompatible with Hallowed Flame, Hallowed Nightmares, Warframe Anniversary
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/TacAlertKeyWaterFightB",
"/Lotus/Types/Keys/TacAlertKeyWaterFightC",
"/Lotus/Types/Keys/TacAlertKeyWaterFightD"
],
ConcurrentNodeReqs: [25, 50, 100],
ConcurrentNodes: ["EventNode24", "EventNode34", "EventNode35"], // Incompatible with Hallowed Flame, Hallowed Nightmares, Warframe Anniversary
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"
});
pushFlashSales(worldState, dogDaysFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
// there no strict start day so lets pick first march monday
const firstMarchWeekday = new Date(Date.UTC(date.getUTCFullYear(), 2, 1)).getUTCDay();
const firstMarchMondayOffset = (8 - firstMarchWeekday) % 7;
const saintPatrickStart = Date.UTC(date.getUTCFullYear(), 2, firstMarchMondayOffset + 1, 16);
const saintPatrickEnd = Date.UTC(date.getUTCFullYear(), 2, firstMarchMondayOffset + 22, 16);
const isSaintPatrickActive = timeMs >= saintPatrickStart && timeMs < saintPatrickEnd;
if (
(config.worldState?.saintPatrickOverride ?? isSaintPatrickActive) &&
buildVersion >= gameToBuildVersionInt["13.0.0"]
) {
const activation = config.worldState?.saintPatrickOverride ? 1772467200000 : saintPatrickStart;
const expiry = config.worldState?.saintPatrickOverride ? 2000000000000 : saintPatrickEnd;
pushFlashSales(worldState, saintPatrickDayFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
// Pride Month - June
const isJune = date.getUTCMonth() == 5;
if ((config.worldState?.prideOverride ?? isJune) && buildVersion >= gameToBuildVersionInt["29.5.0"]) {
const activation = config.worldState?.prideOverride ? 1772467200000 : Date.UTC(date.getUTCFullYear(), 5, 1);
const expiry = config.worldState?.prideOverride ? 2000000000000 : Date.UTC(date.getUTCFullYear(), 6, 1);
pushFlashSales(worldState, prideMonthFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
const xmasStart = Date.UTC(date.getUTCFullYear(), 11, 1);
const xmasEnd = Date.UTC(date.getUTCFullYear() + 1, 1, 1);
const isXmas = timeMs >= xmasStart && timeMs < xmasEnd;
if ((config.worldState?.xmasOverride ?? isXmas) && buildVersion >= gameToBuildVersionInt["13.0.0"]) {
const activation = config.worldState?.xmasOverride ? 1772467200000 : xmasStart;
const expiry = config.worldState?.xmasOverride ? 2000000000000 : xmasEnd;
pushFlashSales(worldState, tennobaumFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
if (config.worldState?.anniversary != undefined) {
// Incompatible with: Use Tag from Warframe Anniversary for old Events, Wolf Hunt, Galleon Of Ghouls, Hallowed Flame, Hallowed Nightmares, Dog Days, Proxy Rebellion, Long Shadow
const goalsByWeek: Partial<IGoal>[][] = [
[
{
Node: "EventNode28",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2019E",
Tag: "Anniversary2019TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Excalibur/ExcaliburDexSkin"] }
},
{
Node: "EventNode26",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020F",
Tag: "Anniversary2020TacAlert",
Reward: { items: ["/Lotus/StoreItems/Types/Items/ShipDecos/ExcaliburDexBobbleHead"] }
},
{
Node: "EventNode19",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeA",
Tag: "Anniversary2024TacAlertCMA",
Reward: { items: ["/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker"] }
}
],
[
{
Node: "EventNode24",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017C",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Weapons/Tenno/LongGuns/DexTheThird/DexTheThird"] }
},
{
Node: "EventNode18",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020H",
Tag: "Anniversary2020TacAlert",
Reward: { items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageDexAnniversary"] }
}
],
[
{
Node: "EventNode18",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2022J",
Tag: "Anniversary2022TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Rhino/RhinoDexSkin"] }
},
{
Node: "EventNode38",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025D",
Tag: "Anniversary2020TacAlert",
Reward: { items: ["/Lotus/StoreItems/Types/Items/ShipDecos/RhinoDexBobbleHead"] }
},
{
Node: "EventNode27",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeA",
Tag: "Anniversary2024TacAlertCMA",
Reward: { items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"] }
}
],
[
{
Node: "EventNode2",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020G",
Tag: "Anniversary2020TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Liset/DexLisetSkin"] }
},
{
Node: "EventNode17",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017B",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DexTheSecond/DexTheSecond"] }
}
],
[
{
Node: "EventNode18",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017A",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Weapons/Tenno/Pistols/DexFuris/DexFuris"] }
},
{
Node: "EventNode26",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2023K",
Tag: "Anniversary2025TacAlert",
Reward: { items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageCommunityClemComic"] }
},
{
Node: "EventNode12",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeB",
Tag: "Anniversary2025TacAlertCMB",
Reward: { items: ["/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker"] }
}
],
[
{
Node: "EventNode17",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025A",
Tag: "Anniversary2025TacAlert",
Reward: {
items: [
"/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/KatanaAndWakizashi/Dex2023Nikana/Dex2023Nikana"
]
}
},
{
Node: "EventNode27",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2018D",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Scarves/DexScarf"] }
}
],
[
{
Node: "EventNode38",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025C",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Wisp/DexWispSkin"] }
},
{
Node: "EventNode12",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024L",
Tag: "Anniversary2024TacAlert",
Reward: { items: ["/Lotus/Types/StoreItems/Packages/OperatorDrifterDexBundle"] }
},
{
Node: "EventNode26",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeB",
Tag: "Anniversary2024TacAlertCMB",
Reward: { items: ["/Lotus/StoreItems/Types/Recipes/Components/UmbraFormaBlueprint"] }
}
],
[
{
Node: "EventNode37",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2021I",
Tag: "Anniversary2021TacAlert",
Reward: {
items: [
"/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorAArmor",
"/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorCArmor",
"/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorLArmor",
"/Lotus/StoreItems/Types/Game/CatbrowPet/CatbrowGeneticSignature"
],
countedItems: [
{ ItemType: "/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", ItemCount: 10 }
]
}
},
{
Node: "EventNode9",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025B",
Tag: "Anniversary2025TacAlert",
Reward: {
items: ["/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerAnniversaryEleven"]
}
}
]
];
goalsByWeek[config.worldState.anniversary].forEach((goal, i) => {
worldState.Goals.push({
_id: {
$oid:
"67c6d8e725b23feb" +
config.worldState?.anniversary!.toString(16).padStart(4, "0") +
i.toString(16).padStart(4, "0")
},
Activation: toMongoDate2(1745593200000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1,
Success: 0,
Personal: true,
ClampNodeScores: true,
Node: goal.Node,
MissionKeyName: goal.MissionKeyName,
Desc: goal.Tag!.endsWith("CMB")
? "/Lotus/Language/Events/Anniversary2024ChallengeMode"
: "/Lotus/Language/G1Quests/Anniversary2017MissionTitle",
Icon: "/Lotus/Interface/Icons/Player/GlyphLotus12Anniversary.png",
Tag: goal.Tag!,
Reward: goal.Reward
});
});
}
if (config.worldState?.wolfHunt != undefined && buildVersion >= gameToBuildVersionInt["25.0.0"]) {
if (config.worldState.wolfHunt == 0) {
worldState.Goals.push({
_id: toOid2("67ed7672798d6466172e3b9c", buildVersion),
Activation: { $date: { $numberLong: "1743616800000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
Count: 0,
Goal: 1,
BonusGoal: 2,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode29",
ConcurrentMissionKeyNames: ["/Lotus/Types/Keys/WolfTacAlertB"],
ConcurrentNodeReqs: [1],
ConcurrentNodes: ["EventNode28"], // Incompatible with Galleon Of Ghouls, Orphix Venom, Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/WolfTacAlertA",
Faction: "FC_GRINEER",
Desc: "/Lotus/Language/Alerts/WolfAlert",
Icon: "/Lotus/Interface/Icons/Npcs/Seasonal/WolfStalker.png",
Tag: "WolfHuntRedux", // unfaithful
Reward: {
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/Alertium", ItemCount: 10 }]
},
BonusReward: {
items: [
"/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
} else if (config.worldState.wolfHunt == 1) {
worldState.Goals.push({
_id: toOid2("67ed7672798d6466172e3b9d", buildVersion),
Activation: { $date: { $numberLong: "1743616800000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
Count: 0,
Goal: 3,
InterimGoals: [1, 2],
BonusGoal: 4,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode29",
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/WolfTacAlertReduxB",
"/Lotus/Types/Keys/WolfTacAlertReduxC",
"/Lotus/Types/Keys/WolfTacAlertReduxD"
],
ConcurrentNodeReqs: [1, 2, 3],
ConcurrentNodes: ["EventNode28", "EventNode39", "EventNode40"], // Incompatible with Galleon Of Ghouls, Orphix Venom, Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/WolfTacAlertReduxA",
Faction: "FC_GRINEER",
Desc: "/Lotus/Language/Alerts/WolfAlert",
Icon: "/Lotus/Interface/Icons/Npcs/Seasonal/WolfStalker.png",
Tag: "WolfHuntRedux",
InterimRewards: [
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerHandle"]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerHead"]
}
],
Reward: {
credits: 50000,
items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerMotor"]
},
BonusReward: {
credits: 50000,
items: [
"/Lotus/StoreItems/Types/Recipes/Weapons/ThrowingHammerBlueprint",
"/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
}
}
const tagsForOlderGoals: string[] = [
"Anniversary2018TacAlert",
"Anniversary2019TacAlert",
"Anniversary2020TacAlert",
"Anniversary2021TacAlert",
"Anniversary2022TacAlert",
"Anniversary2024TacAlert",
"Anniversary2024TacAlertCMA",
"Anniversary2025TacAlert",
"Anniversary2025TacAlertCMB"
];
if (config.worldState?.hallowedFlame && buildVersion >= gameToBuildVersionInt["26.0.0"]) {
worldState.Goals.push(
{
_id: toOid2("5db305403d34b5158873519a", buildVersion),
Activation: toMongoDate2(1699372800000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 3,
InterimGoals: [1, 2],
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode24", // Incompatible with Hallowed Nightmares, Dog Days
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/LanternEndlessEventKeyB",
"/Lotus/Types/Keys/LanternEndlessEventKeyC"
],
ConcurrentNodeReqs: [1, 2],
ConcurrentNodes: ["EventNode25", "EventNode34"], // Incompatible with Hallowed Nightmares, Dog Days
MissionKeyName: "/Lotus/Types/Keys/LanternEndlessEventKeyA",
Faction: "FC_INFESTATION",
Desc: "/Lotus/Language/Events/TacAlertHalloweenLantern",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
Tag: changeLegacyTags ? tagsForOlderGoals[0] : "Halloween19",
InterimRewards: [
{ items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"] },
{ items: ["/Lotus/StoreItems/Types/Items/MiscItems/Forma"] }
],
Reward: {
items: ["/Lotus/StoreItems/Types/Items/MiscItems/FormaAura"]
}
},
{
_id: toOid2("5db3054a3d34b5158873519c", buildVersion),
Activation: toMongoDate2(1699372800000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 900,
Success: 0,
Personal: true,
Bounty: true,
Best: true,
ClampNodeScores: true,
Node: "EventNode35",
MissionKeyName: "/Lotus/Types/Keys/LanternEndlessEventKeyD",
Faction: "FC_INFESTATION",
Desc: "/Lotus/Language/Events/TacAlertHalloweenLanternEndless",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
Tag: "Halloween19Endless",
PrereqGoalTags: [changeLegacyTags ? tagsForOlderGoals[0] : "Halloween19"],
Reward: {
items: [
"/Lotus/StoreItems/Upgrades/Skins/Effects/BatsEphemera",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
},
ScoreVar: "EndlessMissionTimeElapsed",
ScoreMaxTag: "Halloween19ScoreMax"
}
);
}
if (config.worldState?.hallowedNightmares && buildVersion >= gameToBuildVersionInt["18.0.2"]) {
const rewards = [
// 2018
[
{
items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/DotD2016Sigil"]
},
{
items: ["/Lotus/StoreItems/Upgrades/Skins/Halloween/HalloweenDread"]
},
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor"]
}
],
// 2016
[
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"]
},
{
items: [
"/Lotus/StoreItems/Upgrades/Skins/Sigils/DotD2016Sigil",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
},
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor"]
}
],
// 2015
[
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"]
},
{
items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"]
}
]
];
const year = config.worldState.hallowedNightmaresRewardsOverride ?? 0;
worldState.Goals.push({
_id: { $oid: "5bc98f00000000000000000" + year.toString(16) },
Activation: toMongoDate2(1539972000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
InterimGoals: [1],
Goal: 2,
Success: 0,
Personal: true,
Bounty: true,
Tag: changeLegacyTags ? tagsForOlderGoals[0] : "Halloween",
Faction: "FC_INFESTATION",
Desc: "/Lotus/Language/G1Quests/TacAlertHalloweenTitle",
ToolTip: "/Lotus/Language/G1Quests/TacAlertHalloweenToolTip",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
ClampNodeScores: true,
Node: "EventNode2", // Incompatible with Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyHalloween",
ConcurrentMissionKeyNames: ["/Lotus/Types/Keys/TacAlertKeyHalloweenBonus"],
ConcurrentNodeReqs: [1],
ConcurrentNodes: ["EventNode24"], // Incompatible with Hallowed Flame, Dog Days, Warframe Anniversary
InterimRewards: [rewards[year][0]],
Reward: rewards[year][1]
});
if (year != 2) {
worldState.Goals.push({
_id: { $oid: "5bc98f01000000000000000" + year.toString(16) },
Activation: toMongoDate2(1539972000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 666,
Success: 0,
Personal: true,
Bounty: true,
Best: true,
Tag: "Halloween",
PrereqGoalTags: [changeLegacyTags ? tagsForOlderGoals[0] : "Halloween"],
Faction: "FC_INFESTATION",
Desc: "Hallowed Nightmares - Time Attack",
ToolTip: "/Lotus/Language/G1Quests/TacAlertHalloweenToolTip",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
ClampNodeScores: true,
Node: "EventNode25", // Incompatible with Hallowed Flame, Dog Days
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyHalloweenTimeAttack",
ScoreVar: "TimeAttackScore",
ScoreMaxTag: "Halloween16",
Reward: rewards[year][2]
});
}
}
if (config.worldState?.proxyRebellion && buildVersion >= gameToBuildVersionInt["23.2.0"]) {
const rewards = [
// 2019
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod"]
},
{
credits: 50000,
items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"]
},
{
items: [
"/Lotus/StoreItems/Upgrades/Skins/Scarves/HornSkullScarf",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
],
// 2018
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Upgrades/Mods/FusionBundles/NightwatchFusionBundle"]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"]
},
{
credits: 50000,
items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"]
},
{
items: [
"/Lotus/StoreItems/Upgrades/Skins/Sigils/EnergySigilA",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
]
];
const year = config.worldState.proxyRebellionRewardsOverride ?? 0;
worldState.Goals.push({
_id: { $oid: "5b5b5da0000000000000000" + year.toString(16) },
Activation: toMongoDate2(1532714400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 3,
InterimGoals: [1, 2],
BonusGoal: 4,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode18", // Incompatible with Warframe Anniversary
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionTwo",
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionThree",
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionFour"
],
ConcurrentNodeReqs: [1, 2, 3],
ConcurrentNodes: ["EventNode7", "EventNode4", "EventNode17"], // Incompatible with Orphix venom, Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyProxyRebellionOne",
Faction: "FC_CORPUS",
Desc: "/Lotus/Language/Alerts/TacAlertProxyRebellion",
Icon: "/Lotus/Materials/Emblems/BountyBadge_e.png",
Tag: changeLegacyTags ? tagsForOlderGoals[1] : "ProxyRebellion",
InterimRewards: rewards[year].slice(0, 2),
Reward: rewards[year][2],
BonusReward: rewards[year][3]
});
}
if (config.worldState?.longShadow && buildVersion >= gameToBuildVersionInt["18.22.1"]) {
worldState.Goals.push({
_id: toOid2("5bc9e8f7272d5d184c8398c9", buildVersion),
Activation: toMongoDate2(1539972000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
InterimGoals: [1, 2],
Goal: 3,
BonusGoal: 4,
Success: 0,
Personal: true,
Bounty: true,
Tag: changeLegacyTags ? tagsForOlderGoals[2] : "NightwatchTacAlert",
Faction: "FC_GRINEER",
Desc: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertTitle",
Icon: "/Lotus/Materials/Emblems/BountyBadge_e.png",
ClampNodeScores: true,
Node: "EventNode9", // Incompatible with Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchEasy",
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatch",
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatchHard",
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatchBonus"
],
ConcurrentNodeReqs: [1, 2, 3],
ConcurrentNodes: ["SolNode136", "EventNode3", "EventNode0"],
InterimRewards: [
{
credits: 50000,
countedItems: [
{ ItemType: "/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle", ItemCount: 10 } // Not sure about that
]
},
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"]
}
],
Reward: {
items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"]
},
BonusReward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"] }
});
}
const isOctober = date.getUTCMonth() == 9; // October = month index 9
if ((config.worldState?.naberusNightsOverride ?? isOctober) && buildVersion >= gameToBuildVersionInt["29.3.1"]) {
const activation = config.worldState?.naberusNightsOverride
? 1727881200000
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
const expiry = config.worldState?.naberusNightsOverride
? 2000000000000
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
worldState.Goals.push({
_id: toOid2("66fd602de1778d583419e8e7", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Events/HalloweenNaberusName",
ToolTip: "/Lotus/Language/Events/HalloweenNaberusDesc",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
Tag: "DeimosHalloween",
Node: "DeimosHub"
});
pushFlashSales(worldState, naberusNightsFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
if (config.worldState?.bellyOfTheBeast && buildVersion >= gameToBuildVersionInt["36.0.0"]) {
worldState.Goals.push({
_id: toOid2("67a5035c2a198564d62e165e", buildVersion),
Activation: toMongoDate2(1738868400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
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"
});
pushGoalAlerts(worldState, "JadeShadows", buildVersion);
}
if (config.worldState?.operationAtramentum && buildVersion >= gameToBuildVersionInt["42.0.0"]) {
worldState.Goals.push({
_id: toOid2("69ce8b780000000000000000", buildVersion),
Activation: toMongoDate2(1738868400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: config.worldState.operationAtramentumProgressOverride ?? 0,
HealthPct: (config.worldState.operationAtramentumProgressOverride ?? 0) / 100,
Goal: 0,
Personal: true,
Community: true,
ClanGoal: [72, 216, 648, 1944, 5832],
Tag: "ShadowgrapherEvent",
Faction: "FC_TENNO",
Desc: "/Lotus/Language/Shadowgrapher/ShadowgrapherEventName",
ToolTip: "/Lotus/Language/Shadowgrapher/ShadowgrapherShortEventDesc",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/OperationAntramentumIcon.png",
ScoreLocTag: "/Lotus/Language/Shadowgrapher/ShadowgrapherEventScore",
Node: "SolNode239",
MissionKeyName: "/Lotus/Types/Keys/ShadowgrapherEventMission",
ItemType: "/Lotus/Types/Gameplay/Shadowgrapher/Resources/ShadowgrapherEventResource"
});
}
// <U39
if (config.worldState?.eightClaw && buildVersion >= gameToBuildVersionInt["39.0.0"]) {
worldState.Goals.push({
_id: toOid2("685c15f80000000000000000", buildVersion),
Activation: toMongoDate2(1750865400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
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"
});
}
if (config.worldState?.scarletSpear && buildVersion >= gameToBuildVersionInt["27.3.0"]) {
worldState.Goals.push({
_id: toOid2("5e7a3e2389e3090b0c6a998b", buildVersion),
Activation: { $date: { $numberLong: "1585070400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
Node: "ScenarioEventHub5",
Desc: "/Lotus/Language/G1Quests/FlotillaOperation",
Icon: "/Lotus/Interface/Graphics/ScarletSpear/ScarletSpearIcon.png",
Tag: "SquadLinkEvent",
ScoreVar: "ScenarioScore",
Personal: true,
Metadata:
'{"progressReq":100,"duration":180,"cooldown":10,"groundTiers":[1000,3000,5000],"spaceTiers":[1000,3000,5000]}',
CompletionBonus: [0, 2000, 6000, 10000],
InterimGoals: [10000, 30000],
InterimRewards: [
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/ScarletSpear/ScarletSpearOperationIEmblem"] },
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/ScarletSpear/ScarletSpearOperationIIEmblem"] }
],
Goal: 50000,
Reward: {
items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/ScarletSpear/ScarletSpearOperationIIIEmblem"]
},
AltExpiry: { $date: { $numberLong: "2000000000000" } },
AltActivation: { $date: { $numberLong: "2000000000000" } },
EpochNum: 114,
NextAltActivation: { $date: { $numberLong: "2000000000000" } },
NextAltExpiry: { $date: { $numberLong: "2000000000000" } },
PauseAutoScheduling: true
});
}
if (config.worldState?.orphixVenom && buildVersion >= gameToBuildVersionInt["29.6.8"]) {
worldState.Goals.push(
{
_id: toOid2("5fdcccb875d5ad500dc477d0", buildVersion),
Activation: toMongoDate2(1608320400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 500,
Success: 0,
Personal: true,
Best: true,
Node: "EventNode17", // Incompatible with Proxy Rebellion
MissionKeyName: "/Lotus/Types/Keys/MechSurvivalCorpusShip",
Faction: "FC_SENTIENT",
Desc: "/Lotus/Language/Events/MechEventMissionTier1",
Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png",
Tag: "MechSurvivalA",
ScoreVar: "MechSurvivalScore",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/MechEventEmblemItem"] }
},
{
_id: toOid2("5fdcccb875d5ad500dc477d1", buildVersion),
Activation: toMongoDate2(1608320400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1000,
Success: 0,
Personal: true,
Best: true,
Node: "EventNode28", // Incompatible with Galleon Of Ghouls, Wolf Hunt
MissionKeyName: "/Lotus/Types/Keys/MechSurvivalGrineerGalleon",
Faction: "FC_SENTIENT",
Desc: "/Lotus/Language/Events/MechEventMissionTier2",
Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png",
Tag: "MechSurvivalB",
PrereqGoalTags: ["MechSurvivalA"],
ScoreVar: "MechSurvivalScore",
Reward: { items: ["/Lotus/StoreItems/Types/Items/FusionTreasures/OroFusexJ"] }
},
{
_id: toOid2("5fdcccb875d5ad500dc477d2", buildVersion),
Activation: toMongoDate2(1608320400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 2000,
Success: 0,
Personal: true,
Best: true,
Node: "EventNode32",
MissionKeyName: "/Lotus/Types/Keys/MechSurvivalGasCity",
MissionKeyRotation: [
"/Lotus/Types/Keys/MechSurvivalGasCity",
"/Lotus/Types/Keys/MechSurvivalCorpusShipEndurance",
"/Lotus/Types/Keys/MechSurvivalGrineerGalleonEndurance"
],
MissionKeyRotationInterval: 3600, // 1 hour
Faction: "FC_SENTIENT",
Desc: "/Lotus/Language/Events/MechEventMissionTier3",
Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png",
Tag: "MechSurvival",
PrereqGoalTags: ["MechSurvivalA", "MechSurvivalB"],
ScoreVar: "MechSurvivalScore",
ScoreMaxTag: "MechSurvivalScoreMax",
Reward: {
items: [
"/Lotus/StoreItems/Types/Items/MiscItems/FormaAura",
"/Lotus/StoreItems/Upgrades/Skins/Necramech/MechWeapon/MechEventMausolonSkin"
]
}
}
);
}
if (config.worldState?.bloodOfPerita && buildVersion >= gameToBuildVersionInt["41.0.0"]) {
worldState.Goals.push({
_id: toOid2("694189080000000000000000", buildVersion),
Activation: toMongoDate2(1765902600000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
GracePeriod: { $date: { $numberLong: "2000000000000" } },
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/TauPrequel/TauPrequelFinal/TauPrequelEventName",
ToolTip: "/Lotus/Language/TauPrequel/TauPrequelFinal/BloodOfPeritaDetails",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/BloodOfPeritaEventBadgeSmall.png",
Tag: "12MinWarEvent",
Node: "SolNode251"
});
pushGoalAlerts(worldState, "12MinWarEvent", buildVersion);
}
if (config.worldState?.lunarNewYear) {
const reapeingItems: Record<string, (Partial<IFlashSale> & { TypeName: string })[]> = {
"2022": [
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyTiger", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2022HeavyBladeSkin",
PremiumOverride: 45
},
{ TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/LNYCarpSugatra", PremiumOverride: 15 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2022IgnisSkin",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2022Zarr", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/Kubrows/Armor/Lunar2022KubrowArmor", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/Effects/PeachBlossomsEphemera", PremiumOverride: 60 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/CNY2021Poster", RegularOverride: 1 },
{ TypeName: "/Lotus/Types/StoreItems/AvatarImages/ChineseNewYear2021Glyph", RegularOverride: 1 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/WeGame/LuckyKavat", PremiumOverride: 35 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/WeGame/LuckyKavatWhite", PremiumOverride: 35 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/WeGame/LuckyKavatGold", PremiumOverride: 35 },
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2020AcceltraSkin", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2020OrthosSkin", PremiumOverride: 20 },
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2020PyranaSkin", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/Promo/WeGame/WeGameMacheteSkin", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/WegameChinaKnotDangle", PremiumOverride: 15 },
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2020PyranaSkin", PremiumOverride: 25 }
],
"2023": [
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyLNY2023Rabbit", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2023CedoSkin",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Upgrades/Skins/Sigils/WeGameNewYearTigerSigil", PremiumOverride: 40 },
{ TypeName: "/Lotus/Upgrades/Skins/Sigils/WeGameNewYearRabbitSigil", PremiumOverride: 40 },
{ TypeName: "/Lotus/Interface/Graphics/CustomUI/LunarNewYearStyle", PremiumOverride: 50 },
{
TypeName: "/Lotus/Interface/Graphics/CustomUI/Backgrounds/SpringFestivalBackground",
PremiumOverride: 50
},
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2023NagantakaSkin",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Upgrades/Skins/Effects/LunarEphemera", PremiumOverride: 60 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2023CernosSkin",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Types/Items/Emotes/LNY2023Emote", PremiumOverride: 25 }
],
"2024": [
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyLNY2024Dragon", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2024Nukor",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2024DragonSigil", PremiumOverride: 40 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNYHookSword",
PremiumOverride: 75
},
{
TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/LNYBirdSugatra",
PremiumOverride: 15
},
{ TypeName: "/Lotus/Upgrades/Skins/Effects/LNYDragonEphemera", PremiumOverride: 60 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2024Ogris",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Types/Items/Emotes/LNY2024DragonEmote", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/Sentinels/Skins/LNYDragonSentinelSkin", PremiumOverride: 85 },
{ TypeName: "/Lotus/Upgrades/Skins/Sentinels/Masks/LNYDragonMask", PremiumOverride: 30 },
{ TypeName: "/Lotus/Upgrades/Skins/Sentinels/Wings/LNYDragonWings", PremiumOverride: 15 },
{ TypeName: "/Lotus/Upgrades/Skins/Sentinels/Tails/LNYDragonTail", PremiumOverride: 15 }
],
"2025": [
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushySnake", PremiumOverride: 35 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyLNYMirage", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/CNY2025ScytheSkin",
PremiumOverride: 25
},
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2025BoltorSkin",
PremiumOverride: 20
},
{
TypeName: "/Lotus/StoreItems/Upgrades/Skins/Catbrows/Armor/LNYKavatBoltorArmor",
PremiumOverride: 90
},
{ TypeName: "/Lotus/Upgrades/Skins/Effects/LNYStonesEphemera", PremiumOverride: 60 },
{ TypeName: "/Lotus/Upgrades/Skins/Sigils/WeGameNewYearSnakeSigil", PremiumOverride: 40 },
{
TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/LNYSnakeMeleeDangle",
PremiumOverride: 15
},
{ TypeName: "/Lotus/Types/Items/Emotes/LNY2025SnakeEmote", PremiumOverride: 15 }
],
"2026": [
{ TypeName: "/Lotus/Upgrades/Skins/Horse/DagathDeluxeLNYHorseBodySkin", PremiumOverride: 220 },
{ TypeName: "/Lotus/Upgrades/Skins/Horse/DagathDeluxeLNYHorseTail", PremiumOverride: 30 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyLNYKaithe", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/CNY2025ScytheSkin",
PremiumOverride: 25
},
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2025BoltorSkin",
PremiumOverride: 20
},
{ TypeName: "/Lotus/Upgrades/Skins/Effects/LNYKaitheDagathEphemera", PremiumOverride: 60 },
{ TypeName: "/Lotus/Upgrades/Skins/Sigils/WeGameNewYearHorseSigil", PremiumOverride: 40 },
{
TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/LNYFireSugatra",
PremiumOverride: 15
},
{ TypeName: "/Lotus/Types/Items/Emotes/LNY2026HorseEmote", PremiumOverride: 15 }
]
};
const storeItems: Record<string, (Partial<IFlashSale> & { TypeName: string })[]> = {
"2019": [{ TypeName: "/Lotus/Types/Items/ShipLayerCNY", RegularOverride: 1 }],
"2020": [{ TypeName: "/Lotus/Types/StoreItems/AvatarImages/ImageGengzi", RegularOverride: 1 }],
"2021": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2021BlessingsBundle", PremiumOverride: 130 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2021AnewBundle", PremiumOverride: 185 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2021LuminousBundle", PremiumOverride: 470 }
],
"2022": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2022BundleA", PremiumOverride: 135 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2022BundleB", PremiumOverride: 275 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2022BundleC", PremiumOverride: 515 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2022BundleD", PremiumOverride: 105 },
...[...reapeingItems["2022"]].sort((a, b) => a.TypeName.localeCompare(b.TypeName))
],
"2023": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2023BundleA", PremiumOverride: 135 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2023BundleB", PremiumOverride: 265 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2023BundleC", PremiumOverride: 500 },
{ TypeName: "/Lotus/Types/StoreItems/AvatarImages/CNY2023RabbitGlyph", RegularOverride: 1 },
...[...reapeingItems["2022"], ...reapeingItems["2023"]].sort((a, b) =>
a.TypeName.localeCompare(b.TypeName)
)
],
"2024": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2024BundleA", PremiumOverride: 135 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2024BundleB", PremiumOverride: 295 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2024BundleC", PremiumOverride: 575 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2024SentinelSkinBundle", PremiumOverride: 95 },
{
TypeName: "/Lotus/Types/StoreItems/AvatarImages/Seasonal/AvatarImageYearOfTheDragonGlyph",
RegularOverride: 1
},
...[...reapeingItems["2022"], ...reapeingItems["2023"], ...reapeingItems["2024"]].sort((a, b) =>
a.TypeName.localeCompare(b.TypeName)
)
],
"2025": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2025BundleA", PremiumOverride: 215 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2025BundleB", PremiumOverride: 590 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2025BundleC", PremiumOverride: 805 },
{
TypeName: "/Lotus/Types/StoreItems/AvatarImages/Seasonal/AvatarImageCNY2025SnakeGlyphB",
RegularOverride: 1
},
{ TypeName: "/Lotus/Upgrades/Skins/Clan/CNY2025SnakeEmblem", RegularOverride: 1 },
...[
...reapeingItems["2022"],
...reapeingItems["2023"],
...reapeingItems["2024"],
...reapeingItems["2025"]
].sort((a, b) => a.TypeName.localeCompare(b.TypeName))
],
"2026": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2026BundleA", PremiumOverride: 295 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2026BundleB", PremiumOverride: 480 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2026BundleC", PremiumOverride: 700 },
{
TypeName: "/Lotus/Types/StoreItems/AvatarImages/LNY2026HorseGlyph",
RegularOverride: 1
},
{ TypeName: "/Lotus/Upgrades/Skins/Clan/LNY2026HorseGlyph", RegularOverride: 1 },
...[
...reapeingItems["2022"],
...reapeingItems["2023"],
...reapeingItems["2024"],
...reapeingItems["2025"],
...reapeingItems["2026"]
].sort((a, b) => a.TypeName.localeCompare(b.TypeName))
]
};
const selectedStoreItems: (Partial<IFlashSale> & { TypeName: string })[] = [];
if (config.worldState.lunarNewYear == "all") {
const addedTypeNames = new Set<string>();
const packageItems = [];
const otherItems = [];
for (const items of Object.values(storeItems)) {
for (const item of items) {
if (!addedTypeNames.has(item.TypeName)) {
addedTypeNames.add(item.TypeName);
if (item.TypeName.includes("/Lotus/Types/StoreItems/Packages/")) {
packageItems.push(item);
} else {
otherItems.push(item);
}
}
}
}
selectedStoreItems.push(
...packageItems.sort((a, b) => a.TypeName.localeCompare(b.TypeName)),
...otherItems.sort((a, b) => a.TypeName.localeCompare(b.TypeName))
);
} else if (config.worldState.lunarNewYear in storeItems) {
selectedStoreItems.push(...storeItems[config.worldState.lunarNewYear]);
}
pushFlashSales(worldState, selectedStoreItems, 1750865400000, 2000000000000, "SEASONAL", buildVersion);
}
// Thermia Fractures activates for 14 days, with alternating 4 and 3-day breaks
const thermiaFracturesCycleDay = day % 35;
const isThermiaFracturesActive =
thermiaFracturesCycleDay < 14 || (thermiaFracturesCycleDay >= 18 && thermiaFracturesCycleDay < 32);
const activeThermiaFracturesCycleDay =
thermiaFracturesCycleDay - (thermiaFracturesCycleDay < 14 ? 0 : thermiaFracturesCycleDay < 18 ? 14 : 32);
if (
(config.worldState?.thermiaFracturesOverride ?? isThermiaFracturesActive) &&
buildVersion >= gameToBuildVersionInt["24.5.1"]
) {
const activeStartDay = day - activeThermiaFracturesCycleDay;
const count = config.worldState?.thermiaFracturesProgressOverride ?? 0;
const activation = config.worldState?.thermiaFracturesOverride ? 1740416400000 : getSortieTime(activeStartDay);
const expiry = config.worldState?.thermiaFracturesOverride ? 2000000000000 : getSortieTime(activeStartDay + 14);
// If we push it, the game may show the event even tho it's not activated yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2721)
if (timeMs >= activation) {
worldState.Goals.push({
_id: toOid2("5c7cb0d00000000000000000", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
Node: "SolNode129",
ScoreVar: "FissuresClosed",
ScoreLocTag: "/Lotus/Language/G1Quests/HeatFissuresEventScore",
Count: count,
HealthPct: count / 100,
Regions: [1],
Desc: "/Lotus/Language/G1Quests/HeatFissuresEventName",
ToolTip: "/Lotus/Language/G1Quests/HeatFissuresEventDesc",
OptionalInMission: true,
Tag: "HeatFissure",
UpgradeIds: [{ $oid: "5c81cefa4c4566791728eaa7" }, { $oid: "5c81cefa4c4566791728eaa6" }],
Personal: true,
Community: true,
Goal: 100,
Reward: {
items: ["/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpBFG/Vandal/VandalCrpBFG"]
},
InterimGoals: [5, 25, 50, 75],
InterimRewards: [
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/OrbBadgeItem"] },
{
items: [
"/Lotus/StoreItems/Upgrades/Mods/DualSource/Shotgun/ShotgunMedicMod",
"/Lotus/StoreItems/Upgrades/Mods/DualSource/Rifle/SerratedRushMod"
]
},
{
items: [
"/Lotus/StoreItems/Upgrades/Mods/DualSource/Pistol/MultishotDodgeMod",
"/Lotus/StoreItems/Upgrades/Mods/DualSource/Melee/CritDamageChargeSpeedMod"
]
},
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/OrbSigil"] }
]
});
worldState.NodeOverrides.push({
_id: toOid2("5c7cb0d00000000000000000", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
Node: "SolNode129",
Faction: "FC_CORPUS",
CustomNpcEncounters: ["/Lotus/Types/Gameplay/Venus/Encounters/Heists/ExploiterHeistFissure"]
});
if (count >= 35) {
worldState.GlobalUpgrades.push({
_id: toOid2("5c81cefa4c4566791728eaa6", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
ExpiryDate: { $date: { $numberLong: expiry.toString() } },
UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
Nodes: ["SolNode129"]
});
}
// Not sure about that
if (count == 100) {
worldState.GlobalUpgrades.push({
_id: toOid2("5c81cefa4c4566791728eaa7", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
ExpiryDate: { $date: { $numberLong: expiry.toString() } },
UpgradeType: "GAMEPLAY_PICKUP_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
Nodes: ["SolNode129"]
});
}
}
}
// Nightwave Challenges
const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildVersion);
if (nightwaveSyndicateTag) {
const nightwaveStartTimestamp = nightwaveTagToActivation[nightwaveSyndicateTag] ?? 1747851300000;
const nightwaveSeason = nightwaveTagToSeason[nightwaveSyndicateTag];
const nightwaveEpisode = ([0, 2, 4].includes(nightwaveSeason) && config.worldState?.nightwaveEpisode) || 1;
worldState.SeasonInfo = {
Activation: { $date: { $numberLong: nightwaveStartTimestamp.toString() } },
Expiry: { $date: { $numberLong: "2000000000000" } },
AffiliationTag: nightwaveSyndicateTag,
Season: nightwaveSeason,
Phase: nightwaveEpisode - 1,
Params: "",
ActiveChallenges: []
};
const pools = getSeasonChallengePools(nightwaveSyndicateTag);
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0));
if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) {
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1));
}
pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week, nightwaveStartTimestamp, nightwaveSeason);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
pushWeeklyActs(
worldState.SeasonInfo.ActiveChallenges,
pools,
week + 1,
nightwaveStartTimestamp,
nightwaveSeason
);
}
}
{
worldState.NodeOverrides.push(
{ _id: toOid2("549b18e9b029cef5991d6aec", buildVersion), Node: "EuropaHUB", Hide: true },
{ _id: toOid2("54a1737aeb658f6cbccf70ff", buildVersion), Node: "ErisHUB", Hide: true },
{ _id: toOid2("54a736ddec12f80bd6e9e326", buildVersion), Node: "VenusHUB", Hide: true }
);
if (buildVersion >= gameToBuildVersionInt["22.18.0"]) {
worldState.NodeOverrides.push({
_id: toOid2("5ad9f9bb6df82a56eabf3d44", buildVersion),
Node: "SolNode802",
// Elite Sanctuary Onslaught cycling every week
Seed: new SRng(week).randomInt(0, 0xff_ffff)
});
}
if (buildVersion >= gameToBuildVersionInt["25.7.0"]) {
worldState.NodeOverrides.push({
_id: toOid2("5d24d1f674491d51f8d44473", buildVersion),
Node: "MercuryHUB",
Hide: true,
LevelOverride: "/Lotus/Levels/Proc/Hub/RelayStationHubHydroid",
Activation: toMongoDate2(1563030000000, buildVersion)
});
}
if (buildVersion >= gameToBuildVersionInt["21.0.0"]) {
worldState.NodeOverrides.push({
_id: toOid2("5b8817c2bd4f253264d6aa91", buildVersion),
Node: "EarthHUB",
Hide: false,
LevelOverride: "/Lotus/Levels/Proc/Hub/RelayStationHubTwoB",
Activation: toMongoDate2(1535646600000, buildVersion)
});
}
}
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
let bountyCycle = Math.trunc((timeSecs - bountyEpoch) / eidolonCycleDuration);
let bountyCycleEnd: number | undefined;
do {
const bountyCycleStart = Math.trunc((bountyEpoch + bountyCycle * eidolonCycleDuration) * 1000);
bountyCycleEnd = Math.trunc(bountyCycleStart + eidolonCycleDuration * 1000);
if (buildVersion >= gameToBuildVersionInt["29.0.0"]) {
worldState.SyndicateMissions.push({
_id: toOid2(
((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000004",
buildVersion
),
Activation: toMongoDate2(bountyCycleStart, buildVersion),
Expiry: toMongoDate2(bountyCycleEnd, buildVersion),
Tag: "EntratiLabSyndicate",
Seed: bountyCycle,
Nodes: []
});
}
if (buildVersion >= gameToBuildVersionInt["31.5.0"]) {
worldState.SyndicateMissions.push({
_id: toOid2(
((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000029",
buildVersion
),
Activation: toMongoDate2(bountyCycleStart, buildVersion),
Expiry: toMongoDate2(bountyCycleEnd, buildVersion),
Tag: "ZarimanSyndicate",
Seed: bountyCycle,
Nodes: []
});
}
if (buildVersion >= gameToBuildVersionInt["38.0.0"]) {
worldState.SyndicateMissions.push({
_id: toOid2(
((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000006",
buildVersion
),
Activation: toMongoDate2(bountyCycleStart, buildVersion),
Expiry: toMongoDate2(bountyCycleEnd, buildVersion),
Tag: "HexSyndicate",
Seed: bountyCycle,
Nodes: []
});
}
pushClassicBounties(worldState.SyndicateMissions, bountyCycle, buildVersion);
} 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) &&
buildVersion >= gameToBuildVersionInt["22.8.2"]
) {
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) / unixTimesInMs.day;
const progress = (dayWithFraction - activeStartDay) / (activeEndDay - activeStartDay);
const healthPct = 1 - Math.min(Math.max(progress, 0), 1);
const bountyCycleStartSecs = Math.trunc(bountyEpoch + bountyCycle * eidolonCycleDuration);
worldState.Goals.push({
_id: toOid2("687ebbe6d1d17841c9c59f38", buildVersion),
Activation: {
$date: {
$numberLong: config.worldState?.ghoulEmergenceOverride
? "1753204900185"
: (EPOCH + activeStartDay * unixTimesInMs.day).toString()
}
},
Expiry: {
$date: {
$numberLong: config.worldState?.ghoulEmergenceOverride
? "2000000000000"
: (EPOCH + activeEndDay * unixTimesInMs.day).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: (bountyCycleStartSecs & 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?.breedingGrounds && buildVersion >= gameToBuildVersionInt["14.0.0"]) {
worldState.Goals.push({
_id: toOid2("53a35a043f9a5b16e0308ea7", buildVersion),
Activation: toMongoDate2(1753204900185, buildVersion),
Count: 0,
Desc: "/Lotus/Language/G1Quests/InfestedCorpusHiveEventName",
Expiry: toMongoDate2(2000000000000, buildVersion),
Goal: 48,
InterimGoals: [6, 17],
MissionKeyName: "/Lotus/Types/Keys/InfestedCorpusHiveEventKey",
Node: "EventNode10",
Success: 0,
ToolTip: "/Lotus/Language/G1Quests/InfestedCorpusHiveEventToolTip",
Personal: true,
Best: !config.unfaithfulBugFixes?.giveBreedingGroundsRewardsAtSum,
Tag: changeLegacyTags ? tagsForOlderGoals[3] : "HiveEvent", // Madeup tag
InterimRewards: [
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/HiveSabotageEventBadgeItem"] },
{
items: [
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/FireEventRifleMod",
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/FireEventShotgunMod",
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/FireEventPistolMod",
"/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/FireEventMeleeMod"
]
}
],
Reward: { items: ["/Lotus/StoreItems/Weapons/ClanTech/Energy/VandalElectroProd"] },
ScoreSumTag: "HiveEventScoreSum",
ScoreMaxTag: "HiveEvent"
});
}
if (config.worldState?.creditBoostMultiplier) {
worldState.GlobalUpgrades.push({
_id: toOid2("5b23106f283a555109666672", buildVersion),
Activation: toMongoDate2(1740164400000, buildVersion),
ExpiryDate: toMongoDate2(2000000000000, buildVersion),
UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT",
OperationType: "MULTIPLY",
Value: config.worldState.creditBoostMultiplier,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
if (config.worldState?.affinityBoostMultiplier) {
worldState.GlobalUpgrades.push({
_id: toOid2("5b23106f283a555109666673", buildVersion),
Activation: toMongoDate2(1740164400000, buildVersion),
ExpiryDate: toMongoDate2(2000000000000, buildVersion),
UpgradeType: "GAMEPLAY_KILL_XP_AMOUNT",
OperationType: "MULTIPLY",
Value: config.worldState.affinityBoostMultiplier,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
if (config.worldState?.resourceBoostMultiplier) {
worldState.GlobalUpgrades.push({
_id: toOid2("5b23106f283a555109666674", buildVersion),
Activation: toMongoDate2(1740164400000, buildVersion),
ExpiryDate: toMongoDate2(2000000000000, buildVersion),
UpgradeType: "GAMEPLAY_PICKUP_AMOUNT",
OperationType: "MULTIPLY",
Value: config.worldState.resourceBoostMultiplier,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
// Sheev parts were added in 19.6.3, so versions prior to that may take too kindly to seeing invasions.
if (buildVersion > gameToBuildVersionInt["19.5.3"]) {
// Rough outline of dynamic invasions.
// TODO: Invasions chains, e.g. an infestation mission would soon lead to other nodes on that planet also having an infestation invasion.
// TODO: Grineer/Corpus to fund their death stars with each invasion win.
{
worldState.Invasions.push(createInvasion(day, 0, buildVersion));
worldState.Invasions.push(createInvasion(day, 1, buildVersion));
worldState.Invasions.push(createInvasion(day, 2, buildVersion));
// Completed invasions stay for up to 24 hours as the winner 'occupies' that node
worldState.Invasions.push(createInvasion(day - 1, 0, buildVersion));
worldState.Invasions.push(createInvasion(day - 1, 1, buildVersion));
worldState.Invasions.push(createInvasion(day - 1, 2, buildVersion));
}
}
// Baro
// Introdused in U15.6
if (buildVersion > gameToBuildVersionInt["15.5.0"]) {
const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));
const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000;
const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12);
const baroEnd = baroStart + unixTimesInMs.day * 14;
const baroRelayOverride = config.worldState?.baroRelayOverride;
const baroNodeIndex = baroRelayOverride && baroRelayOverride > 0 ? baroRelayOverride - 1 : baroIndex % 4;
let baroNode = ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroNodeIndex];
if (baroNode == "MercuryHUB" && buildVersion < gameToBuildVersionInt["18.18.0"]) {
// This Pre-Star Chart 3.0 client won't know Larunda Relay, so move Baro elsewhere.
baroNode = "EarthHUB";
}
const evilBaroStage =
buildVersion >= gameToBuildVersionInt["40.0.0"] ? (config.worldState?.evilBaroStage ?? 0) : 0;
const baroCharacter = ["Baro'Ki Teel", "EvilBaroWeek1", "EvilBaroWeek2", "EvilBaroWeek3", "EvilBaroWeek4"][
evilBaroStage
];
const vt: IVoidTrader = {
_id: toOid2(
((baroStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "493c96d6067610bc",
buildVersion
),
Activation: toMongoDate2(baroActualStart, buildVersion),
Expiry: toMongoDate2(baroEnd, buildVersion),
Character: baroCharacter,
Node: baroNode,
Manifest: []
};
worldState.VoidTraders.push(vt);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, baroActualStart)) {
if (config.worldState?.baroFullyStocked) {
fullyStockBaro(vt, buildVersion);
} else {
const tempManifest: IBaroDataTradeOffer[] = [];
const rng = new SRng(new SRng(baroIndex).randomInt(0, 100_000));
// TOVERIFY: Constraint for upgrades amount?
// TOVERIFY: Constraint for weapon amount?
// TOVERIFY: Constraint for relics amount?
const armorSet = rng.randomElement(baro.armorSets)!;
const armorItems = armorSet.bundle && rng.randomInt(0, 1) == 0 ? [armorSet.bundle] : armorSet.items;
while (tempManifest.length + armorItems.length < 31) {
const item = rng.randomElement(baro.rest)!;
if (tempManifest.indexOf(item) == -1) {
const set = baro.allIfAny.find(set => set.indexOf(item.ItemType) != -1);
if (set) {
for (const itemType of set) {
tempManifest.push(baro.rest.find(x => x.ItemType == itemType)!);
}
} else {
tempManifest.push(item);
}
}
}
const overflow = 31 - (tempManifest.length + armorItems.length);
if (overflow > 0) {
tempManifest.splice(0, overflow);
}
tempManifest.push(...armorItems);
{
const evilBaroStock: string[] = [];
if (evilBaroStage >= 1) evilBaroStock.push("EvilBaroArcaArmorA", "BaroEvilEphemera");
if (evilBaroStage >= 2) evilBaroStock.push("GrimoireEvilBaroSkin", "EvilBaroArcaArmorC");
if (evilBaroStage >= 3) evilBaroStock.push("EvilBaroArcaArmorL", "EvilBaroSilvaAndAegis");
if (evilBaroStage >= 4) {
evilBaroStock.push(
"EvilBaroNecraloidSigil",
"DissolveEnemyMod",
"EvilBaroFloatingCandlesShipDeco"
);
}
tempManifest.unshift(
...baro.evilBaro.filter(item => evilBaroStock.some(end => item.ItemType.endsWith(end)))
);
}
for (const item of baro.evergreen) {
tempManifest.push(item);
}
vt.Manifest.push(
...tempManifest
.filter(({ minBuildVersionInt }) => buildVersion >= minBuildVersionInt)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ minBuildVersionInt, ...offer }) => offer)
);
}
}
}
// Varzia
// introduced in 30.9.4
if (buildVersion >= gameToBuildVersionInt["31.0.0"]) {
const pt: IPrimeVaultTrader = {
_id: { $oid: ((weekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "c36af423770eaa97" },
Activation: { $date: { $numberLong: weekStart.toString() } },
InitialStartDate: { $date: { $numberLong: "1662738144266" } },
Node: "TradeHUB1",
Manifest: [],
Expiry: { $date: { $numberLong: weekEnd.toString() } },
EvergreenManifest: varzia.evergreen,
ScheduleInfo: []
};
worldState.PrimeVaultTraders.push(pt);
const rotation = config.worldState?.varziaOverride || getVarziaRotation(week, buildVersion);
pt.Manifest = config.worldState?.varziaFullyStocked
? getAllVarziaManifests(buildVersion)
: getVarziaManifest(rotation, buildVersion);
if (config.worldState?.varziaOverride || config.worldState?.varziaFullyStocked) {
pt.Expiry = { $date: { $numberLong: "2000000000000" } };
} else {
pt.ScheduleInfo.push({
Expiry: { $date: { $numberLong: (weekEnd + unixTimesInMs.week).toString() } },
FeaturedItem: getVarziaRotation(week + 1, buildVersion)
});
}
}
// Void Storms
if (buildVersion >= gameToBuildVersionInt["30.0.0"]) {
const hour = Math.trunc(timeMs / unixTimesInMs.hour);
const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
if (overLastHourStormExpiry > timeMs) {
pushVoidStorms(worldState.VoidStorms, hour - 2);
}
pushVoidStorms(worldState.VoidStorms, hour - 1);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
pushVoidStorms(worldState.VoidStorms, hour);
}
}
// Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST)
if (buildVersion >= gameToBuildVersionInt["15.0.0"]) {
const rollover = getSortieTime(day);
// Omit sorties for pre-Star Chart 3.0 clients to avoid breaking them.
if (buildVersion >= gameToBuildVersionInt["18.18.0"]) {
if (timeMs < rollover) {
worldState.Sorties.push(getSortie(day - 1, buildVersion));
if (
buildVersion >= gameToBuildVersionInt["25.7.0"] &&
isBeforeNextExpectedWorldStateRefresh(timeMs, rollover)
) {
worldState.Sorties.push(getSortie(day, buildVersion));
}
} else {
worldState.Sorties.push(getSortie(day, buildVersion));
}
}
// The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
const sdy = timeMs >= rollover ? day : day - 1;
const rng = new SRng(sdy);
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804e", "NewLokaSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48050", "PerrinSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4805e", "RedVeilSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48061", "SteelMeridianSyndicate");
}
if (config.worldState?.communitySynthesisTarget) {
const targetType = `/Lotus/Types/Game/Library/Targets/Research${config.worldState.communitySynthesisTarget}Target`;
worldState.LibraryInfo = {
CurrentTarget: {
StartTime: toMongoDate2(0, buildVersion),
TargetType: targetType,
EnemyType: libraryTargetToAvatar[targetType],
PersonalScansRequired: 10,
ProgressPercent: config.worldState.communitySynthesisProgress ?? 0
}
};
if (config.worldState.communitySynthesisTarget != 1) {
worldState.LibraryInfo.LastCompletedTargetType = `/Lotus/Types/Game/Library/Targets/Research${config.worldState.communitySynthesisTarget - 1}Target`;
}
}
if (config.worldState?.snowdayShowdown && buildVersion >= gameToBuildVersionInt["19.4.1"]) {
worldState.PVPAlternativeModes.push({
TargetMode: "PVPMODE_TEAMDEATHMATCH",
TitleLoc: "/Lotus/Language/G1Quests/TacAlertSnowballFightTitle",
DescriptionLoc: "/Lotus/Language/G1Quests/TacAlertSnowballFightToolTip",
DisableEnergyPickups: true,
DisableEnergySurge: true,
DisableAmmoPickups: true,
DisableWeaponSwitching: true,
DisableWeaponHud: false,
EnergyCapOverride: 0,
ForceChangeLoadoutOnDeath: false,
MatchTimeOverride: 600,
MaxPlayersOverride: 8,
MinPlayersPerTeamOverride: 2,
MaxTeamCountDifferenceOverride: 2,
WeaponOverrides: [
{ Override: false, UseFirstAsDefault: true, Resources: [], OriginalVersions: [] },
{
Override: true,
UseFirstAsDefault: true,
Resources: ["/Lotus/Weapons/Tenno/ThrowingWeapons/VariantSnowBalls"],
OriginalVersions: []
},
{ Override: true, UseFirstAsDefault: true, Resources: [], OriginalVersions: [] }
],
MeleeWeaponOverride: {
Override: true,
UseFirstAsDefault: true,
Resources: ["/Lotus/Weapons/Tenno/Melee/Scythe/ParisScythe/VariantXmasScythe"],
OriginalVersions: [],
IsModularMeleeWeapon: false,
BalancesPool: [],
HandlesPool: [],
TipsPool: []
}
});
}
if (buildVersion >= gameToBuildVersionInt["17.7.1"]) {
if (buildVersion >= gameToBuildVersionInt["18.0.2"]) {
const conclaveWeekStart = weekStart + 40 * unixTimesInMs.minute - 2 * unixTimesInMs.day;
const conclaveWeekEnd = conclaveWeekStart + unixTimesInMs.week;
pushConclaveWeekly(worldState.PVPChallengeInstances, week, buildVersion);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, conclaveWeekEnd)) {
pushConclaveWeekly(worldState.PVPChallengeInstances, week + 1, buildVersion);
}
}
{
const conclaveDayStart =
EPOCH + day * unixTimesInMs.day + 5 * unixTimesInMs.hour + 30 * unixTimesInMs.minute;
const conclaveDayEnd = conclaveDayStart + unixTimesInMs.day;
pushConclaveDailys(worldState.PVPChallengeInstances, day, buildVersion);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, conclaveDayEnd)) {
pushConclaveDailys(worldState.PVPChallengeInstances, day + 1, buildVersion);
}
}
}
// Archon Hunt cycling every week
if (buildVersion >= gameToBuildVersionInt["32.0.0"]) {
worldState.LiteSorties.push(getLiteSortie(week));
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
worldState.LiteSorties.push(getLiteSortie(week + 1));
}
}
// Circuit choices cycling every week
if (buildVersion >= gameToBuildVersionInt["42.0.0"]) {
worldState.EndlessXpSchedule = [
{
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
CategoryChoices: getEndlessXpChoices(week, buildVersion)
}
];
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
worldState.EndlessXpSchedule.push({
Activation: { $date: { $numberLong: (weekStart + 604800000).toString() } },
Expiry: { $date: { $numberLong: (weekEnd + 604800000).toString() } },
CategoryChoices: getEndlessXpChoices(week + 1, buildVersion)
});
}
} else if (buildVersion >= gameToBuildVersionInt["33.0.0"]) {
worldState.EndlessXpChoices = getEndlessXpChoices(week, buildVersion);
}
// 1999 Calendar Season cycling every week + YearIteration every 4 weeks
if (buildVersion >= gameToBuildVersionInt["38.0.0"]) {
worldState.KnownCalendarSeasons.push(getCalendarSeason(week));
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
}
}
const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[week % 4];
const labConquest = getConquest("CT_LAB", week, null);
const hexConquest = getConquest("CT_HEX", week, season);
if (buildVersion >= gameToBuildVersionInt["40.0.0"]) {
worldState.Conquests = [labConquest, hexConquest];
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[(week + 1) % 4];
worldState.Conquests.push(getConquest("CT_LAB", week, null));
worldState.Conquests.push(getConquest("CT_HEX", week, season));
}
}
if (buildVersion >= gameToBuildVersionInt["41.0.0"]) {
worldState.Descents = [getDescent(week)];
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
worldState.Descents.push(getDescent(week + 1));
}
}
// Sentient Anomaly + Xtra Cheese cycles
const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2));
const hourInSeconds = 3600;
const cheeseInterval = hourInSeconds * 8;
const cheeseDuration = hourInSeconds * 2;
const cheeseIndex = Math.trunc(timeSecs / cheeseInterval);
let cheeseStart = cheeseIndex * cheeseInterval;
let cheeseEnd = cheeseStart + cheeseDuration;
let cheeseNext = (cheeseIndex + 1) * cheeseInterval;
// Live servers only update the start time once it happens, which makes the
// client show a negative countdown during off-hours. Optionally adjust the
// times so the next activation is always in the future.
if (config.unfaithfulBugFixes?.fixXtraCheeseTimer && timeSecs >= cheeseEnd) {
cheeseStart = cheeseNext;
cheeseEnd = cheeseStart + cheeseDuration;
cheeseNext += cheeseInterval;
}
const tmp: ITmp = {
cavabegin: "1690761600",
PurchasePlatformLockEnabled: true,
pgr: {
ts: "1732572900",
en: "CUSTOM DECALS @ ZEVILA",
fr: "DECALS CUSTOM @ ZEVILA",
it: "DECALCOMANIE PERSONALIZZATE @ ZEVILA",
de: "AUFKLEBER NACH WUNSCH @ ZEVILA",
es: "CALCOMANÍAS PERSONALIZADAS @ ZEVILA",
pt: "DECALQUES PERSONALIZADOS NA ZEVILA",
ru: "ПОЛЬЗОВАТЕЛЬСКИЕ НАКЛЕЙКИ @ ЗеВиЛа",
pl: "NOWE NAKLEJKI @ ZEVILA",
uk: "КОРИСТУВАЦЬКІ ДЕКОЛІ @ ЗІВІЛА",
tr: "ÖZEL ÇIKARTMALAR @ ZEVILA",
ja: "カスタムデカール @ ゼビラ",
zh: "定制贴花认准泽威拉",
ko: "커스텀 데칼 @ ZEVILA",
tc: "自訂貼花 @ ZEVILA",
th: "รูปลอกสั่งทำที่ ZEVILA"
},
ennnd: true,
mbrt: true,
fbst: {
a: cheeseStart,
e: cheeseEnd,
n: cheeseNext
},
lqo: {
mt: labConquest.Missions.map(x => getMissionTypeForLegacyOverride(x.missionType, "CT_LAB")),
mv: labConquest.Missions.map(x => x.difficulties[1].deviation),
c: labConquest.Missions.map(x => x.difficulties[1].risks),
fv: labConquest.Variables
},
hqo: {
mt: hexConquest.Missions.map(x => getMissionTypeForLegacyOverride(x.missionType, "CT_HEX")),
mv: hexConquest.Missions.map(x => x.difficulties[1].deviation),
mf: hexConquest.Missions.map(x => factionToInt(x.faction)),
c: hexConquest.Missions.map(x => x.difficulties[1].risks),
fv: hexConquest.Variables
},
sfn: [550, 553, 554, 555][halfHour % 4],
tcend26: true,
tcaz: 3
};
if (Array.isArray(config.worldState?.circuitGameModes)) {
tmp.edg = config.worldState.circuitGameModes as TCircuitGameMode[];
}
worldState.Tmp = JSON.stringify(tmp);
if (convertGoals && buildVersion < gameToBuildVersionInt["20.4.0"]) {
worldState.Version = 9;
convertGoalsToV9(worldState.Goals);
}
// This must be the last field in these versions.
if (buildVersion >= gameToBuildVersionInt["18.18.0"] && buildVersion <= gameToBuildVersionInt["20.4.0"]) {
worldState.WorldSeed = "4763605";
}
return worldState;
};
const convertGoalsToV9 = (goals: IGoal[]): void => {
for (let i = 0; i < goals.length; i++) {
const g = goals[i];
goals[i] = {
_id: g._id,
Activation: g.Activation,
AltActivation: g.AltActivation,
Expiry: g.Expiry,
Count: g.Count,
CountAlt: g.CountAlt,
ParentCountClamp: g.ParentCountClamp,
HealthPct: g.HealthPct,
Icon: g.Icon,
Desc: g.Desc,
CommunityReqDesc: g.CommunityReqDesc,
ToolTip: g.ToolTip,
Faction: g.Faction,
Goal: g.Goal,
GoalInterim: g.InterimGoals && g.InterimGoals[0] ? g.InterimGoals[0] : undefined,
GoalInterim2: g.InterimGoals && g.InterimGoals[1] ? g.InterimGoals[1] : undefined,
BonusGoal: g.BonusGoal,
Success: g.Success,
Personal: g.Personal,
Best: g.Best,
Ongoing: g.Ongoing,
Bounty: g.Bounty,
Fomorian: g.Fomorian,
Invasion: g.Invasion,
ClampNodeScores: g.ClampNodeScores,
Roaming: g.Roaming,
PvpRep: g.PvpRep,
Transmission: g.Transmission,
ItemType: g.ItemType,
Tag: g.Tag,
ParentTag: g.ParentTag,
PrereqGoalTags: g.PrereqGoalTags,
Node: g.Node,
VictimNode: g.VictimNode,
InvasionNode: g.InvasionNode,
ConcurrentMissionKeyNames: g.ConcurrentMissionKeyNames,
ConcurrentMissionInfo: g.ConcurrentMissionInfo,
ConcurrentNodeReqs: g.ConcurrentNodeReqs,
ConcurrentNodes: g.ConcurrentNodes,
MissionKeyName: g.MissionKeyName,
KeyRequired: g.KeyRequired,
Reward: g.Reward,
RewardInterim: g.InterimRewards && g.InterimRewards[0] ? g.InterimRewards[0] : undefined,
RewardInterim2: g.InterimRewards && g.InterimRewards[1] ? g.InterimRewards[1] : undefined,
BonusReward: g.BonusReward,
ScoreVar: g.ScoreVar,
ScoreLocTag: g.ScoreLocTag,
ScoreSumTag: g.ScoreSumTag,
ScoreMaxTag: g.ScoreMaxTag,
ScoreMaxNode: g.ScoreMaxNode,
ArchiveTag: g.JobAffiliationTag,
MissionInfo: g.MissionInfo,
SuccessHubEvent: g.SuccessHubEvent,
FailureHubEvent: g.FailureHubEvent,
ContinuousHubEvent: g.ContinuousHubEvent,
Types: g.Types,
RewardRegion: g.RewardRegion,
RegionDrops: g.RegionDrops,
ArchwingDrops: g.ArchwingDrops,
Diorama: g.Diorama,
RadioSound: g.RadioSound,
MaxConclave: g.MaxConclave,
BonusMaxConclave: g.BonusMaxConclave,
BonusLevelModifier: g.BonusLevelModifier,
BonusWaveModifier: g.BonusWaveModifier
} as IGoalV9;
}
};
export const populateFissures = async (worldState: IWorldState): Promise<void> => {
const buildVersion = buildVersionToInt(worldState.BuildLabel);
if (buildVersion < gameToBuildVersionInt["18.16.0"]) return;
if (config.worldState?.allTheFissures) {
let i = 0;
for (const [tier, nodes] of Object.entries(fissureMissions)) {
for (const node of nodes) {
const meta = ExportRegions[node];
worldState.ActiveMissions.push({
_id: toOid2((i++).toString().padStart(8, "0") + "8e0c70ba050f1eb7", buildVersion),
Region: meta.systemIndex + 1,
Seed: 1337,
Activation: toMongoDate2(1000000000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Node: node,
MissionType: meta.missionType,
Modifier: tier,
Hard: config.worldState.allTheFissures == "hard"
});
}
}
} else {
const fissures = await Fissure.find({});
for (const fissure of fissures) {
const meta = ExportRegions[fissure.Node];
worldState.ActiveMissions.push({
_id: toOid2(fissure._id, buildVersion),
Region: meta.systemIndex + 1,
Seed: 1337,
Activation:
fissure.Activation.getTime() < Date.now() // Activation is in the past?
? toMongoDate2(1000000000000, buildVersion) // Let the client know 'explicitly' to avoid interference from time constraints.
: toMongoDate2(fissure.Activation, buildVersion),
Expiry: toMongoDate2(fissure.Expiry, buildVersion),
Node: fissure.Node,
MissionType: meta.missionType,
Modifier: fissure.Modifier,
Hard: fissure.Hard
});
}
}
worldState.ActiveMissions = worldState.ActiveMissions.filter(fissure => {
const meta = ExportRegions[fissure.Node];
if (
(meta.faction == "FC_MITW" || fissure.Modifier == "VoidT6") &&
buildVersion < gameToBuildVersionInt["35.5.0"]
) {
return false;
}
if (fissure.Hard && buildVersion < gameToBuildVersionInt["32.0.0"]) {
return false;
}
if (meta.systemIndex == 16 && buildVersion < gameToBuildVersionInt["29.0.0"]) {
return false;
}
if (fissure.Modifier == "VoidT5" && buildVersion < gameToBuildVersionInt["26.0.0"]) {
return false;
}
if (fissure.MissionType == "MT_ARTIFACT" && buildVersion < gameToBuildVersionInt["25.7.0"]) {
return false;
}
if (
meta.systemIndex == 17 &&
buildVersion < gameToBuildVersionInt["18.0.2"] // Should be 18.0.0
) {
return false;
}
return true;
});
if (buildVersion < gameToBuildVersionInt["32.0.0"]) {
for (const fissure of worldState.ActiveMissions) {
delete fissure.Hard;
}
}
};
export const populateDailyDeal = async (worldState: IWorldState): Promise<void> => {
const buildVersion = buildVersionToInt(worldState.BuildLabel);
const dailyDeals = await DailyDeal.find({});
for (const dailyDeal of dailyDeals) {
const meta = darvoDeals.find(d => d.StoreItem == dailyDeal.StoreItem);
if ((!meta || buildVersion >= meta.minBuildVersionInt) && dailyDeal.Expiry.getTime() > Date.now()) {
worldState.DailyDeals.push({
StoreItem: dailyDeal.StoreItem,
Activation: toMongoDate2(dailyDeal.Activation, buildVersion),
Expiry: toMongoDate2(dailyDeal.Expiry, buildVersion),
Discount: dailyDeal.Discount,
OriginalPrice: dailyDeal.OriginalPrice,
SalePrice: dailyDeal.SalePrice,
AmountTotal: Math.round(dailyDeal.AmountTotal * (config.worldState?.darvoStockMultiplier ?? 1)),
AmountSold: dailyDeal.AmountSold
});
}
}
};
export const idToBountyCycle = (id: string): number => {
return Math.round((parseInt(id.substring(0, 8), 16) - bountyEpoch) / eidolonCycleDuration);
};
export const idToDay = (id: string): number => {
return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 86400_000);
};
export const idToWeek = (id: string): number => {
return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 604800_000);
};
export const getLiteSortie = (week: number): ILiteSortie => {
const boss = (["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"] as const)[week % 3];
const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3];
const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth
const nodes: string[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex === systemIndex &&
(value.faction == "FC_GRINEER" || value.faction == "FC_CORPUS") &&
!isArchwingMission(value) &&
value.missionType != "MT_ASSASSINATION" &&
value.missionType != "MT_JUNCTION" &&
value.missionType != "MT_LANDSCAPE" &&
value.missionType != "MT_RAILJACK" &&
key != "SolNode63" && // This node uses GrineerForestTilesetCaves which only supports MT_CAPTURE, which is not valid for LiteSorties.
key != "SolNode15" // This node uses GrineerGalleonTileset which seems to not support LiteSorties
) {
nodes.push(key);
}
}
const seed = new SRng(week).randomInt(0, 100_000);
const rng = new SRng(seed);
const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
const firstNode = nodes[firstNodeIndex];
nodes.splice(firstNodeIndex, 1);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
return {
_id: {
$oid: ((weekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "5e23a244740a190c"
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards",
Seed: seed,
Boss: boss,
Missions: [
{
missionType: rng.randomElement([
"MT_INTEL",
"MT_MOBILE_DEFENSE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_RESCUE"
])!,
node: firstNode
},
{
missionType: rng.randomElement([
"MT_DEFENSE",
"MT_TERRITORY",
"MT_ARTIFACT",
"MT_EXCAVATE",
"MT_SURVIVAL"
])!,
node: rng.randomElement(nodes)!
},
{
missionType: "MT_ASSASSINATION",
node: showdownNode
}
]
};
};
const getEndlessXpChoices = (week: number, buildVersion: number): IEndlessXpChoice[] => {
const normalChoices = [
["Nidus", "Octavia", "Harrow"],
["Gara", "Khora", "Revenant"],
["Garuda", "Baruuk", "Hildryn"],
["Excalibur", "Trinity", "Ember"],
["Loki", "Mag", "Rhino"],
["Ash", "Frost", "Nyx"],
["Saryn", "Vauban", "Nova"],
["Nekros", "Valkyr", "Oberon"],
["Hydroid", "Mirage", "Limbo"],
["Mesa", "Chroma", "Atlas"],
["Ivara", "Inaros", "Titania"]
];
const hardChoices = [
["Boar", "Gammacor", "Angstrum", "Gorgon", "Anku"],
["Bo", "Latron", "Furis", "Furax", "Strun"],
["Lex", "Magistar", "Boltor", "Bronco", "CeramicDagger"],
["Torid", "DualToxocyst", "DualIchor", "Miter", "Atomos"],
["AckAndBrunt", "Soma", "Vasto", "NamiSolo", "Burston"],
["Zylok", "Sibear", "Dread", "Despair", "Hate"],
["Dera", "Sybaris", "Cestra", "Sicarus", "Okina"],
...(buildVersion >= gameToBuildVersionInt["43.0.0"]
? [["Vectis", "Stug", "Ballistica", "Destreza", "Obex"]]
: []),
["Braton", "Lato", "Skana", "Paris", "Kunai"]
];
return [
{
Category: "EXC_NORMAL",
Choices: normalChoices[week % normalChoices.length]
},
{
Category: "EXC_HARD",
Choices: hardChoices[week % hardChoices.length]
}
];
};
export const isArchwingMission = (node: IRegion): boolean => {
if (node.name.indexOf("Archwing") != -1) {
return true;
}
// SettlementNode10
if (node.missionType == "MT_RACE") {
return true;
}
return false;
};
export const getNightwaveSyndicateTag = (buildVersion: number = BV_LATEST): string | undefined => {
if (config.worldState?.nightwaveOverride) {
if (config.worldState.nightwaveOverride in nightwaveTagToSeason) {
return config.worldState.nightwaveOverride;
}
if (config.worldState.nightwaveOverride == "disable") {
return undefined;
}
logger.warn(`ignoring invalid config value for worldState.nightwaveOverride`, {
value: config.worldState.nightwaveOverride,
valid_values: Object.keys(nightwaveTagToSeason)
});
}
if (buildVersion >= gameToBuildVersionInt["42.0.6"]) {
return "RadioLegionIntermission15Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["40.0.0"]) {
return "RadioLegionIntermission14Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["38.6.0"]) {
return "RadioLegionIntermission13Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["38.0.8"]) {
return "RadioLegionIntermission12Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["36.1.2"]) {
return "RadioLegionIntermission11Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["35.5.9"]) {
return "RadioLegionIntermission10Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["34.0.8"]) {
return "RadioLegionIntermission9Syndicate";
}
// Actual version is U33.0.11
if (buildVersion > gameToBuildVersionInt["33.0.10"]) {
return "RadioLegionIntermission8Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["32.2.0"]) {
return "RadioLegionIntermission7Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["31.6.4"]) {
return "RadioLegionIntermission6Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["31.2.0"]) {
return "RadioLegionIntermission5Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["30.6.0"]) {
return "RadioLegionIntermission4Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["29.7.0"]) {
return "RadioLegionIntermission3Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["28.3.2"]) {
return "RadioLegion3Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["25.8.2"]) {
return "RadioLegionIntermission2Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["25.3.0"]) {
return "RadioLegion2Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["25.1.2"]) {
return "RadioLegionIntermissionSyndicate";
}
if (buildVersion >= gameToBuildVersionInt["24.3.0"]) {
return "RadioLegionSyndicate";
}
return undefined;
};
export const nightwaveTagToSeason: Record<string, number> = {
RadioLegionIntermission15Syndicate: 17, // Nora's Mix: Time Tempests
RadioLegionIntermission14Syndicate: 16, // Nora's Mix: Dreams of the Dead
RadioLegionIntermission13Syndicate: 15, // Nora's Mix Vol. 9
RadioLegionIntermission12Syndicate: 14, // Nora's Mix Vol. 8
RadioLegionIntermission11Syndicate: 13, // Nora's Mix Vol. 7
RadioLegionIntermission10Syndicate: 12, // Nora's Mix Vol. 6
RadioLegionIntermission9Syndicate: 11, // Nora's Mix Vol. 5
RadioLegionIntermission8Syndicate: 10, // Nora's Mix Vol. 4
RadioLegionIntermission7Syndicate: 9, // Nora's Mix Vol. 3
RadioLegionIntermission6Syndicate: 8, // Nora's Mix Vol. 2
RadioLegionIntermission5Syndicate: 7, // Nora's Mix Vol. 1
RadioLegionIntermission4Syndicate: 6, // Nora's Choice
RadioLegionIntermission3Syndicate: 5, // Intermission III
RadioLegion3Syndicate: 4, // Glassmaker
RadioLegionIntermission2Syndicate: 3, // Intermission II
RadioLegion2Syndicate: 2, // The Emissary
RadioLegionIntermissionSyndicate: 1, // Intermission I
RadioLegionSyndicate: 0 // The Wolf of Saturn Six
};
export const nightwaveTagToSeasonName: Record<string, string> = {
RadioLegionIntermission15Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceTwelveSeasonTitle",
RadioLegionIntermission14Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceElevenSeasonTitle",
RadioLegionIntermission13Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceTenSeasonTitle",
RadioLegionIntermission12Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceNineSeasonTitle",
RadioLegionIntermission11Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceEightSeasonTitle",
RadioLegionIntermission10Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceSevenSeasonTitle",
RadioLegionIntermission9Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceSixSeasonTitle",
RadioLegionIntermission8Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceFiveSeasonTitle",
RadioLegionIntermission7Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceFourSeasonTitle",
RadioLegionIntermission6Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceThreeSeasonTitle",
RadioLegionIntermission5Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceTwoSeasonTitle",
RadioLegionIntermission4Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceSeasonTitle",
RadioLegionIntermission3Syndicate: "/Lotus/Language/Syndicates/RadioLegionShortSeasonTitle1",
RadioLegion3Syndicate: "/Lotus/Language/Syndicates/RadioLegionSeasonTitle3",
RadioLegionIntermission2Syndicate: "/Lotus/Language/Syndicates/RadioLegionShortSeasonTitle1",
RadioLegion2Syndicate: "/Lotus/Language/Syndicates/RadioLegionSeasonTitle2",
RadioLegionIntermissionSyndicate: "/Lotus/Language/Syndicates/RadioLegionShortSeasonTitle1",
RadioLegionSyndicate: "/Lotus/Language/Syndicates/RadioLegionSeasonTitle1"
};
const nightwaveTagToActivation: Record<string, number> = {
RadioLegionIntermission15Syndicate: 1775662200000,
RadioLegionIntermission14Syndicate: 1761589199000
};
const updateFissures = async (): Promise<void> => {
const fissures = await Fissure.find();
const activeNodes = new Set<string>();
const tierToFurthestExpiry: Record<string, number> = {
VoidT1: 0,
VoidT2: 0,
VoidT3: 0,
VoidT4: 0,
VoidT5: 0,
VoidT6: 0,
VoidT1Hard: 0,
VoidT2Hard: 0,
VoidT3Hard: 0,
VoidT4Hard: 0,
VoidT5Hard: 0,
VoidT6Hard: 0
};
for (const fissure of fissures) {
if (fissure.Expiry.getTime() > Date.now()) {
activeNodes.add(fissure.Node);
}
const key = fissure.Modifier + (fissure.Hard ? "Hard" : "");
tierToFurthestExpiry[key] = Math.max(tierToFurthestExpiry[key], fissure.Expiry.getTime());
}
const deadline = Date.now() - 6 * unixTimesInMs.minute;
for (const [tier, expiry] of Object.entries(tierToFurthestExpiry)) {
if (expiry < deadline) {
const numFissures = getRandomInt(1, 3);
logger.trace(`${tier} fissures expire soon, generating ${numFissures} new ones`);
for (let i = 0; i != numFissures; ++i) {
const modifier = tier.replace("Hard", "") as
| "VoidT1"
| "VoidT2"
| "VoidT3"
| "VoidT4"
| "VoidT5"
| "VoidT6";
let node: string = getRandomElement(fissureMissions[modifier])!;
while (activeNodes.has(node)) {
logger.trace(`tried to use ${node} for a fissure, but it's already in use`);
node = getRandomElement(fissureMissions[modifier])!;
}
activeNodes.add(node);
await Fissure.insertOne({
Activation: new Date(),
Expiry: new Date(Date.now() + getRandomInt(60, 120) * unixTimesInMs.minute),
Node: node,
Modifier: modifier,
Hard: tier.indexOf("Hard") != -1 ? true : undefined
});
}
}
}
};
const updateDailyDeal = async (): Promise<void> => {
let darvoIndex = Math.trunc((Date.now() - 25200000) / (26 * unixTimesInMs.hour));
let darvoEnd;
do {
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
darvoEnd = darvoStart + 26 * unixTimesInMs.hour;
const darvoOid = ((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
if (!(await DailyDeal.findById(darvoOid))) {
const seed = new SRng(darvoIndex).randomInt(0, 100_000);
const rng = new SRng(seed);
let deal;
do {
deal = rng.randomReward(darvoDeals)!; // Using an actual sampling collected over roughly a year because I can't extrapolate an algorithm from it with enough certainty.
//const [storeItem, meta] = rng.randomElement(Object.entries(darvoDeals))!;
//const discount = Math.min(rng.randomInt(1, 9) * 10, (meta as { MaxDiscount?: number }).MaxDiscount ?? 1);
} while (await DailyDeal.exists({ StoreItem: deal.StoreItem }));
await DailyDeal.insertOne({
_id: darvoOid,
StoreItem: deal.StoreItem,
Activation: new Date(darvoStart),
Expiry: new Date(darvoEnd),
Discount: deal.Discount,
OriginalPrice: deal.OriginalPrice,
SalePrice: deal.SalePrice, //Math.trunc(deal.OriginalPrice * (1 - discount))
AmountTotal: deal.AmountTotal,
AmountSold: 0
});
}
} while (darvoEnd < Date.now() + 6 * unixTimesInMs.minute && ++darvoIndex);
};
const alertStandardResources = [
{ path: "/Lotus/Types/Items/MiscItems/AlloyPlate", qty: 1500 },
{ path: "/Lotus/Types/Items/MiscItems/Circuits", qty: 1500 },
{ path: "/Lotus/Types/Items/MiscItems/ControlModule", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Ferrite", qty: 3000 },
{ path: "/Lotus/Types/Items/MiscItems/Gallium", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Morphic", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Nanospores", qty: 3000 },
{ path: "/Lotus/Types/Items/MiscItems/NeuralSensor", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Neurode", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/OrokinCell", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Plastids", qty: 300 },
{ path: "/Lotus/Types/Items/MiscItems/PolymerBundle", qty: 300 },
{ path: "/Lotus/Types/Items/MiscItems/Rubedo", qty: 450 },
{ path: "/Lotus/Types/Items/MiscItems/Salvage", qty: 300 }
];
const alertAuras = [
"/Lotus/Upgrades/Mods/Aura/PlayerEnemyRadarAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerEnergyRegenAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerHealthRegenAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerMeleeAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerPistolAmmoAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerRifleAmmoAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerRifleDamageAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerShellAmmoAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerSniperAmmoAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerHealthAuraMod",
"/Lotus/Upgrades/Mods/Aura/EnemyArmorReductionAuraMod",
"/Lotus/Upgrades/Mods/Aura/EnemyShieldReductionAuraMod",
"/Lotus/Upgrades/Mods/Aura/InfestationSpeedReductionAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerHolsterSpeedAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerSprintAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerSniperDamageAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerLootRadarAuraMod",
"/Lotus/Upgrades/Mods/Aura/RobotPoorAimAuraMod"
];
const alertHelmets = [
"/Lotus/Types/Recipes/Helmets/StatlessAshAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessBansheeAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessEmberAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessExcaliburAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessFrostAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessLokiAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessMagAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessNyxAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessRhinoAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessSarynAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessTrinityAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessVoltAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ValkyrBastetHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/OberonAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ZephyrCierzoHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/HarlequinAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/LimboAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/MirageAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2AshAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2BansheeAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2EmberAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2ExcaliburAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2FrostAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2LokiAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2MagAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2NyxAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2RhinoAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2SarynAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2TrinityAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2VoltAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/OberonAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ValkyrAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/PirateAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/LimboAristeasHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ZephyrTenguHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/CowgirlAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/MesaAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/DragonAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ChromaAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/VaubanHelmetSoldierBlueprint",
"/Lotus/Types/Recipes/Helmets/ExcaliburMordredHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/AnimaAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/RangerAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NezhaAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/SandmanAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/BrawlerAltTwoHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessVaubanAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2VaubanAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessNovaAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NekrosAraknidHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NovaQuantumHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NekrosShroudHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NovaSlipstreamHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/LokiEnigmaHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/PirateAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/BrawlerAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/WukongAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/FairyAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NidusAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/SandmanAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/RangerAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/BardAltHelmetBlueprint"
];
const alertVaubanParts = [
"/Lotus/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint",
"/Lotus/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint",
"/Lotus/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint"
];
const alertWeapons = [
"/Lotus/Types/Recipes/Weapons/CeramicDaggerBlueprint",
"/Lotus/Types/Recipes/Weapons/DarkDaggerBlueprint",
"/Lotus/Types/Recipes/Weapons/HeatDaggerBlueprint",
"/Lotus/Types/Recipes/Weapons/HeatSwordBlueprint",
"/Lotus/Types/Recipes/Weapons/JawBlueprint",
"/Lotus/Types/Recipes/Weapons/PangolinSwordBlueprint",
"/Lotus/Types/Recipes/Weapons/PlasmaSwordBlueprint",
"/Lotus/Types/Recipes/Weapons/GlaiveBlueprint",
"/Lotus/Types/Recipes/DarkSwordBlueprint",
"/Lotus/Types/Recipes/Weapons/Skins/DaggerAxeBlueprint",
"/Lotus/Types/Recipes/Weapons/Skins/DualDaggerAxeBlueprint",
"/Lotus/Types/Recipes/Weapons/Skins/GrnHammerBlueprint",
"/Lotus/Types/Recipes/Weapons/Skins/GrnAxeBlueprint"
];
const alertNightmareMods = [
"/Lotus/Upgrades/Mods/Pistol/DualStat/StunningSpeedMod",
"/Lotus/Upgrades/Mods/Shotgun/DualStat/BlazeMod",
"/Lotus/Upgrades/Mods/Rifle/DualStat/WildfireMod",
"/Lotus/Upgrades/Mods/Shotgun/DualStat/AcceleratedBlastMod",
"/Lotus/Upgrades/Mods/Warframe/DualStat/ConstitutionMod",
"/Lotus/Upgrades/Mods/Pistol/DualStat/IceStormMod",
"/Lotus/Upgrades/Mods/Warframe/DualStat/FortitudeMod",
"/Lotus/Upgrades/Mods/Rifle/DualStat/HammerShotMod",
"/Lotus/Upgrades/Mods/Melee/DualStat/FocusEnergyMod",
"/Lotus/Upgrades/Mods/Melee/DualStat/RendingStrikeMod",
"/Lotus/Upgrades/Mods/Rifle/DualStat/ShredMod",
"/Lotus/Upgrades/Mods/Pistol/DualStat/GrinderMod",
"/Lotus/Upgrades/Mods/Warframe/DualStat/VigorMod",
"/Lotus/Upgrades/Mods/Warframe/DualStat/RunSpeedArmorMod",
"/Lotus/Upgrades/Mods/Shotgun/DualStat/ReloadSpeedPunchThroughMod"
];
const alertOrokinBP = [
"/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
"/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
"/Lotus/Types/Recipes/Components/FormaBlueprint"
];
const alertDurationMultipliers = new Map<string, number>([
["/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint", 2],
["/Lotus/Types/Recipes/Components/OrokinReactorBlueprint", 2],
["/Lotus/Types/Recipes/Components/FormaBlueprint", 2],
["/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", 2],
["/Lotus/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint", 2],
["/Lotus/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint", 2],
["/Lotus/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint", 2],
["/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", 2],
["/Lotus/Types/Items/MiscItems/Eventium", 2]
]);
const getVersionAppropriateHelmet = (helmetPath: string, buildVersion: number): string => {
let isStore = false;
let typePath = helmetPath;
if (helmetPath.startsWith("/Lotus/StoreItems/")) {
isStore = true;
typePath = "/Lotus/" + helmetPath.substring("/Lotus/StoreItems/".length);
}
const isPreU13_2_3 = buildVersion < gameToBuildVersionInt["13.2.3"]; // arcane helmets were available until U13.2.3
let resultPath = typePath;
if (isPreU13_2_3) {
if (typePath.includes("Statless")) {
const arcanePath = typePath.replace("Statless", "");
if (arcanePath in ExportRecipes) {
resultPath = arcanePath;
}
}
} else {
if (!typePath.includes("Statless")) {
const baseName = typePath.replace("/Lotus/Types/Recipes/Helmets/", "");
const statlessPath = "/Lotus/Types/Recipes/Helmets/Statless" + baseName;
if (statlessPath in ExportRecipes) {
resultPath = statlessPath;
}
}
}
return isStore ? toStoreItem(resultPath) : resultPath;
};
const getEligibleAlertNodes = (regions: Record<string, IRegion>, buildVersion: number): string[] => {
const eligibleNodes: string[] = [];
const validMissionTypes = new Set([
"MT_SURVIVAL",
"MT_DEFENSE",
"MT_RESCUE",
"MT_CAPTURE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_MOBILE_DEFENSE",
"MT_EXCAVATE",
"MT_INTEL",
"MT_TERRITORY",
"MT_PURSUIT",
"MT_SABOTAGE",
"MT_RACE"
]);
const validFactions = new Set(["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION", "FC_CORRUPTED", "FC_OROKIN"]);
for (const [nodeId, nodeData] of Object.entries(regions)) {
if (!isRegionAvailableIn(nodeId, nodeData, buildVersion)) {
continue;
}
if (!nodeId.startsWith("SolNode") && !nodeId.startsWith("SettlementNode")) {
continue;
}
if (nodeData.nodeType !== 0) {
continue;
}
if (!validMissionTypes.has(nodeData.missionType)) {
continue;
}
if (!nodeData.faction || !validFactions.has(nodeData.faction)) {
continue;
}
if (
nodeData.systemIndex == 16 || // Deimos/Derelict
nodeData.systemIndex > 18 // Kuva fortress and more modern things
) {
continue;
}
eligibleNodes.push(nodeId);
}
return eligibleNodes;
};
const ALERT_INTERVAL_MS = 25 * 60 * 1000;
const ALERT_BASE_DURATION_MS = 40 * 60 * 1000;
const alertDescTexts: Record<string, string[] | undefined> = {
MT_ASSASSINATION: [
"/Lotus/Language/Alerts/AssassinationDesc1",
"/Lotus/Language/Alerts/AssassinationDesc9",
"/Lotus/Language/Alerts/AssassinationDesc10",
"/Lotus/Language/Alerts/AssassinationDesc11",
"/Lotus/Language/Alerts/AssassinationDesc14"
],
MT_EXTERMINATION: [
"/Lotus/Language/Alerts/ExterminationDesc3",
"/Lotus/Language/Alerts/ExterminationDesc4",
"/Lotus/Language/Alerts/ExterminationDesc5",
"/Lotus/Language/Alerts/ExterminationDesc6",
"/Lotus/Language/Alerts/ExterminationDesc7",
"/Lotus/Language/Alerts/ExterminationDesc8",
"/Lotus/Language/Alerts/ExterminationDesc9",
"/Lotus/Language/Alerts/ExterminationDesc11",
"/Lotus/Language/Alerts/ExterminationDesc13",
"/Lotus/Language/Alerts/ExterminationDesc14"
],
MT_SURVIVAL: [
"/Lotus/Language/Alerts/RaidDesc15",
"/Lotus/Language/Alerts/RaidDesc19",
"/Lotus/Language/Alerts/RaidDesc22",
"/Lotus/Language/Alerts/RaidDesc23",
"/Lotus/Language/Alerts/DefenseDesc9",
"/Lotus/Language/Alerts/DefenseDesc15"
],
MT_RESCUE: [
"/Lotus/Language/Alerts/RescueDesc1",
"/Lotus/Language/Alerts/RescueDesc2",
"/Lotus/Language/Alerts/RescueDesc3",
"/Lotus/Language/Alerts/RescueDesc4",
"/Lotus/Language/Alerts/RescueDesc5",
"/Lotus/Language/Alerts/RescueDesc6",
"/Lotus/Language/Alerts/RescueDesc7",
"/Lotus/Language/Alerts/RescueDesc8",
"/Lotus/Language/Alerts/RescueDesc9",
"/Lotus/Language/Alerts/RescueDesc10",
"/Lotus/Language/Alerts/RescueDesc11",
"/Lotus/Language/Alerts/RescueDesc12",
"/Lotus/Language/Alerts/RescueDesc14"
],
MT_SABOTAGE: [
"/Lotus/Language/Alerts/SabotageDesc2",
"/Lotus/Language/Alerts/SabotageDesc6",
"/Lotus/Language/Alerts/SabotageDesc7",
"/Lotus/Language/Alerts/SabotageDesc15",
"/Lotus/Language/Alerts/SabotageDesc16",
"/Lotus/Language/Alerts/SabotageDesc17",
"/Lotus/Language/Alerts/SabotageDesc18",
"/Lotus/Language/Alerts/SabotageDesc21",
"/Lotus/Language/Alerts/SabotageDesc23",
"/Lotus/Language/Alerts/SabotageDesc24",
"/Lotus/Language/Alerts/SabotageDesc26",
"/Lotus/Language/Alerts/SabotageDesc28"
],
MT_CAPTURE: [
"/Lotus/Language/Alerts/CaptureDesc1",
"/Lotus/Language/Alerts/CaptureDesc2",
"/Lotus/Language/Alerts/CaptureDesc3",
"/Lotus/Language/Alerts/CaptureDesc4",
"/Lotus/Language/Alerts/CaptureDesc5",
"/Lotus/Language/Alerts/CaptureDesc9",
"/Lotus/Language/Alerts/CaptureDesc10",
"/Lotus/Language/Alerts/CaptureDesc12",
"/Lotus/Language/Alerts/CaptureDesc14"
],
MT_COUNTER_INTEL: [
"/Lotus/Language/Alerts/CounterIntelDesc1",
"/Lotus/Language/Alerts/CounterIntelDesc2",
"/Lotus/Language/Alerts/CounterIntelDesc4",
"/Lotus/Language/Alerts/CounterIntelDesc5",
"/Lotus/Language/Alerts/CounterIntelDesc10",
"/Lotus/Language/Alerts/CounterIntelDesc14",
"/Lotus/Language/Alerts/CounterIntelDesc15",
"/Lotus/Language/Alerts/CounterIntelDesc20",
"/Lotus/Language/Alerts/CounterIntelDesc22",
"/Lotus/Language/Alerts/CounterIntelDesc25"
],
MT_INTEL: [
"/Lotus/Language/Alerts/IntelDesc1",
"/Lotus/Language/Alerts/IntelDesc3",
"/Lotus/Language/Alerts/IntelDesc10",
"/Lotus/Language/Alerts/IntelDesc15",
"/Lotus/Language/Alerts/IntelDesc20",
"/Lotus/Language/Alerts/IntelDesc21",
"/Lotus/Language/Alerts/IntelDesc24",
"/Lotus/Language/Alerts/IntelDesc25",
"/Lotus/Language/Alerts/IntelDesc29",
"/Lotus/Language/Alerts/IntelDesc30",
"/Lotus/Language/Alerts/IntelDesc31",
"/Lotus/Language/Alerts/IntelDesc32",
"/Lotus/Language/Alerts/IntelDesc33",
"/Lotus/Language/Alerts/IntelDesc34",
"/Lotus/Language/Alerts/IntelDesc36",
"/Lotus/Language/Alerts/IntelDesc37",
"/Lotus/Language/Alerts/IntelDesc39",
"/Lotus/Language/Alerts/IntelDesc40",
"/Lotus/Language/Alerts/IntelDesc42",
"/Lotus/Language/Alerts/IntelDesc43",
"/Lotus/Language/Alerts/IntelDesc44",
"/Lotus/Language/Alerts/IntelDesc47"
],
MT_DEFENSE: ["/Lotus/Language/Alerts/DefenseDesc9", "/Lotus/Language/Alerts/DefenseDesc15"],
MT_MOBILE_DEFENSE: [
"/Lotus/Language/Alerts/DefenseDesc2",
"/Lotus/Language/Alerts/DefenseDesc15",
"/Lotus/Language/Alerts/DefenseDesc19"
],
MT_TERRITORY: [
"/Lotus/Language/Alerts/IntelDesc26",
"/Lotus/Language/Alerts/IntelDesc25",
"/Lotus/Language/Alerts/CounterIntelDesc2"
],
MT_RETRIEVAL: [
"/Lotus/Language/Alerts/RaidDesc15",
"/Lotus/Language/Alerts/IntelDesc39",
"/Lotus/Language/Alerts/IntelDesc34"
],
MT_HIVE: ["/Lotus/Language/Alerts/SabotageDesc6"],
MT_EXCAVATE: ["/Lotus/Language/Alerts/IntelDesc4", "/Lotus/Language/Alerts/IntelDesc22"]
};
const getFilteredAlertHelmets = (buildVersion: number): string[] => {
return alertHelmets.filter(helmet => {
if (helmet.includes("BardAlt")) return buildVersion >= gameToBuildVersionInt["20.0.0"];
if (helmet.includes("NidusAlt")) return buildVersion >= gameToBuildVersionInt["19.5.3"];
if (helmet.includes("FairyAlt")) return buildVersion >= gameToBuildVersionInt["19.0.1"];
if (helmet.includes("WukongAlt")) return buildVersion >= gameToBuildVersionInt["18.0.2"];
if (helmet.includes("BrawlerAlt")) return buildVersion >= gameToBuildVersionInt["17.7.1"];
if (helmet.includes("SandmanAlt")) return buildVersion >= gameToBuildVersionInt["18.5.0"];
if (helmet.includes("NezhaAlt")) return buildVersion >= gameToBuildVersionInt["18.0.2"];
if (helmet.includes("RangerAlt")) return buildVersion >= gameToBuildVersionInt["18.0.2"];
if (helmet.includes("AnimaAlt")) return buildVersion >= gameToBuildVersionInt["17.7.1"];
if (helmet.includes("ChromaAlt") || helmet.includes("DragonAlt"))
return buildVersion >= gameToBuildVersionInt["16.0.2"];
if (helmet.includes("MesaAlt") || helmet.includes("CowgirlAlt"))
return buildVersion >= gameToBuildVersionInt["15.5.0"];
if (helmet.includes("LimboAlt") || helmet.includes("LimboAristeas"))
return buildVersion >= gameToBuildVersionInt["15.0.0"];
if (helmet.includes("HarlequinAlt") || helmet.includes("MirageAlt"))
return buildVersion >= gameToBuildVersionInt["14.0.0"];
if (helmet.includes("PirateAlt")) return buildVersion >= gameToBuildVersionInt["13.0.0"];
if (helmet.includes("Zephyr")) return buildVersion >= gameToBuildVersionInt["12.1.2"];
if (helmet.includes("Oberon")) return buildVersion >= gameToBuildVersionInt["11.1.3"];
if (helmet.includes("Valkyr")) return buildVersion >= gameToBuildVersionInt["11.1.3"];
if (helmet.includes("Vauban")) return buildVersion >= gameToBuildVersionInt["8.0.0"];
return true;
});
};
const generateSeededAlert = (
alertIndex: number,
eligibleNodes: string[],
usedNodes: Set<string>,
buildVersion: number,
regions: Record<string, IRegion>
): IAlert | undefined => {
const seed = new SRng(alertIndex * 2654435761).randomInt(0, 100_000);
const rng = new SRng(seed);
const availableNodes = eligibleNodes.filter(n => !usedNodes.has(n));
if (availableNodes.length === 0) return undefined;
const nodeId = rng.randomElement(availableNodes)!;
const nodeData = regions[nodeId];
const isPreU14 = buildVersion < gameToBuildVersionInt["14.0.0"];
const isPostU18 = buildVersion >= gameToBuildVersionInt["18.0.2"];
const isArch = isArchwingMission(nodeData);
const factions: TFaction[] = isArch ? ["FC_GRINEER", "FC_CORPUS"] : ["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION"];
let faction: TFaction = rng.randomInt(0, 9) < 7 ? (nodeData.faction as TFaction) : rng.randomElement(factions)!;
if (faction === "FC_OROKIN" && isPostU18) {
faction = "FC_CORRUPTED" as TFaction;
}
const missionTypes: TMissionType[] = [
"MT_SURVIVAL",
"MT_DEFENSE",
"MT_RESCUE",
"MT_CAPTURE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_MOBILE_DEFENSE",
"MT_INTEL"
];
let missionType: TMissionType;
let levelOverride = nodeData.levelOverride;
let enemySpec = nodeData.enemySpec;
let extraEnemySpec = nodeData.extraEnemySpec;
if (isPreU14) {
missionType = nodeData.missionType;
} else {
missionType = rng.randomInt(0, 9) < 7 ? nodeData.missionType : rng.randomElement(missionTypes)!;
}
if (missionType !== nodeData.missionType) {
levelOverride = undefined;
}
if (isArch) {
missionType = rng.randomElement(
faction == "FC_GRINEER"
? ["MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_TERRITORY", "MT_PURSUIT"]
: ["MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_SABOTAGE", "MT_RACE"]
)!;
}
if (missionType !== nodeData.missionType || faction !== nodeData.faction) {
let foundEnemySpec: string | undefined = undefined;
let foundExtraEnemySpec: string | undefined = undefined;
const searchFaction = (faction as string) === "FC_CORRUPTED" ? "FC_OROKIN" : faction;
if (isArch) {
const targetTileset =
faction == "FC_GRINEER" ? "ArchwingGrineerSpaceTileset" : "ArchwingCorpusTrenchrunTileset";
foundEnemySpec = rng.randomElement(ExportTilesets[targetTileset].missions[missionType]!.enemySpecs!)!;
}
if (!foundEnemySpec) {
const nodes = Object.values(regions).filter(node => node.faction == searchFaction && node.enemySpec);
const node = nodes.find(node => node.missionType == missionType) ?? nodes[0];
foundEnemySpec = node.enemySpec;
foundExtraEnemySpec = node.extraEnemySpec;
}
enemySpec = foundEnemySpec;
extraEnemySpec = foundExtraEnemySpec;
}
const difficulty = rng.randomInt(10, 99) / 100;
const minEnemyLevel = 10 + Math.round(difficulty * 20);
const maxEnemyLevel = minEnemyLevel + rng.randomInt(5, 10);
let rewardCredits = 2000 + Math.round(difficulty * 18000);
let rewardItems: string[] | undefined = undefined;
let rewardCountedItems: { ItemType: string; ItemCount: number }[] | undefined = undefined;
let isNightmare = false;
const filteredStdRes = [...alertStandardResources];
if (buildVersion >= gameToBuildVersionInt["14.0.0"]) {
filteredStdRes.push({ path: "/Lotus/Types/Items/MiscItems/ArgonCrystal", qty: 1 });
}
if (buildVersion >= gameToBuildVersionInt["10.8.0"]) {
filteredStdRes.push({ path: "/Lotus/Types/Items/MiscItems/OxiumAlloy", qty: 300 });
}
if (buildVersion >= gameToBuildVersionInt["15.5.0"]) {
filteredStdRes.push({ path: "/Lotus/Types/Items/MiscItems/Tellurium", qty: 1 });
}
const filteredSpecRes: { path: string; qty: number }[] = [];
if (buildVersion >= gameToBuildVersionInt["14.0.0"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", qty: 1 });
}
if (buildVersion >= gameToBuildVersionInt["18.5.0"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", qty: 5 });
}
if (buildVersion >= gameToBuildVersionInt["14.0.0"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Items/MiscItems/Eventium", qty: 5 });
}
if (buildVersion >= gameToBuildVersionInt["17.12.0"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Items/MiscItems/Alertium", qty: 1 });
}
if (buildVersion >= gameToBuildVersionInt["18.13.3"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Items/MiscItems/VoidTearDrop", qty: 20 });
}
const categories: { name: string; weight: number }[] = [
{ name: "CREDITS", weight: 60 },
{ name: "STANDARD_RESOURCES", weight: 120 },
{ name: "ENDO", weight: 40 },
{ name: "ALT_HELMETS", weight: 80 },
{ name: "WEAPONS", weight: 35 },
{ name: "AURAS", weight: 18 },
{ name: "OROKIN_BP", weight: 4 }
];
if (buildVersion >= gameToBuildVersionInt["9.1.0"]) categories.push({ name: "NIGHTMARE_MODS", weight: 30 });
if (filteredSpecRes.length > 0) categories.push({ name: "SPECIAL_RESOURCES", weight: 10 });
if (buildVersion >= gameToBuildVersionInt["7.11.0"]) {
categories.push({ name: "VAUBAN_PARTS", weight: 7 });
}
let totalWeight = 0;
for (const cat of categories) {
totalWeight += cat.weight;
}
let roll = rng.randomInt(0, totalWeight * 100) / 100;
let selectedCategory = "STANDARD_RESOURCES";
for (const cat of categories) {
if (roll < cat.weight) {
selectedCategory = cat.name;
break;
}
roll -= cat.weight;
}
switch (selectedCategory) {
case "CREDITS": {
rewardCredits = 5000 + Math.round(difficulty * 15000);
break;
}
case "STANDARD_RESOURCES": {
const res = rng.randomElement(filteredStdRes)!;
rewardCountedItems = [{ ItemType: res.path, ItemCount: res.qty }];
break;
}
case "ENDO": {
const isPreU19 = buildVersion < gameToBuildVersionInt["19.0.1"];
const endoRoll = rng.randomInt(0, 99);
if (isPreU19) {
let corePath = "/Lotus/Upgrades/Mods/FusionCores/CoreClassCommonThree";
if (endoRoll < 60) {
corePath = "/Lotus/Upgrades/Mods/FusionCores/CoreClassCommonThree";
} else if (endoRoll < 90) {
corePath = "/Lotus/Upgrades/Mods/FusionCores/CoreClassUncommonFive";
} else {
corePath = "/Lotus/Upgrades/Mods/FusionCores/CoreClassRareFive";
}
rewardItems = [toStoreItem(corePath)];
} else {
let endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleSmall";
if (endoRoll < 60) {
endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleSmall";
} else if (endoRoll < 90) {
endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleMedium";
} else {
endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleLarge";
}
rewardItems = [endoPath];
}
break;
}
case "SPECIAL_RESOURCES": {
const res = rng.randomElement(filteredSpecRes)!;
rewardCountedItems = [{ ItemType: res.path, ItemCount: res.qty }];
break;
}
case "AURAS": {
const aura = rng.randomElement(alertAuras)!;
rewardItems = [toStoreItem(aura)];
break;
}
case "ALT_HELMETS": {
const filteredHelmets = getFilteredAlertHelmets(buildVersion);
const helmet = rng.randomElement(filteredHelmets)!;
rewardItems = [toStoreItem(helmet)];
break;
}
case "VAUBAN_PARTS": {
const part = rng.randomElement(alertVaubanParts)!;
rewardItems = [toStoreItem(part)];
break;
}
case "WEAPONS": {
const weapon = rng.randomElement(alertWeapons)!;
rewardItems = [toStoreItem(weapon)];
break;
}
case "NIGHTMARE_MODS": {
const nmMod = rng.randomElement(alertNightmareMods)!;
rewardItems = [toStoreItem(nmMod)];
isNightmare = true;
break;
}
case "OROKIN_BP": {
const specialBP = rng.randomElement(alertOrokinBP)!;
rewardItems = [toStoreItem(specialBP)];
break;
}
}
let durationMs = ALERT_BASE_DURATION_MS + rng.randomInt(0, 40) * 60 * 1000;
let multiplier = 1;
if (rewardItems && rewardItems.length > 0) {
multiplier = alertDurationMultipliers.get(fromStoreItem(rewardItems[0])) ?? 1;
} else if (rewardCountedItems && rewardCountedItems.length > 0) {
multiplier = alertDurationMultipliers.get(rewardCountedItems[0].ItemType) ?? 1;
}
durationMs = Math.round(durationMs * multiplier);
const activation = EPOCH + alertIndex * ALERT_INTERVAL_MS;
const expiry = activation + durationMs;
const oid =
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"a1e4" +
seed.toString(16).padStart(5, "0").substring(0, 5) +
(alertIndex & 0xffffff).toString(16).padStart(7, "0");
const descTexts = alertDescTexts[missionType];
const descText = isPreU14 && descTexts ? rng.randomElement(descTexts) : undefined;
if (buildVersion < gameToBuildVersionInt["14.0.0"] && missionType === "MT_EXCAVATE") {
return undefined;
}
const alert: IAlert = {
_id: toOid2(oid, buildVersion),
Activation: toMongoDate2(activation, buildVersion),
Expiry: toMongoDate2(expiry, buildVersion),
MissionInfo: {
location: nodeId,
missionType: missionType,
faction: faction,
difficulty: difficulty,
missionReward: {
credits: rewardCredits,
items: rewardItems,
countedItems: rewardCountedItems
},
levelOverride: levelOverride,
enemySpec: enemySpec,
extraEnemySpec: extraEnemySpec,
minEnemyLevel: minEnemyLevel,
maxEnemyLevel: maxEnemyLevel,
descText: descText,
nightmare: isNightmare || undefined,
archwingRequired: isArchwingMission(nodeData) || undefined
}
};
return alert;
};
export const populateAlerts = async (worldState: IWorldState): Promise<void> => {
const buildLabel = worldState.BuildLabel;
const buildVersion = buildVersionToInt(buildLabel);
if (
config.worldState?.classicAlerts ??
(buildVersion >= gameToBuildVersionInt["5.1.0"] && buildVersion < gameToBuildVersionInt["24.3.0"]) // alerts were retired with U24.3.0
) {
const regions = await getRegions(buildLabel);
const validNodes = getEligibleAlertNodes(regions, buildVersion);
if (validNodes.length === 0) return;
const timeMs = worldState.Time * 1000;
const currentAlertIndex = Math.floor((timeMs - EPOCH) / ALERT_INTERVAL_MS);
const activeAlerts: IAlert[] = [];
const usedNodes = new Set<string>();
for (let idx = currentAlertIndex - 8; idx <= currentAlertIndex + 1; idx++) {
const alert = generateSeededAlert(idx, validNodes, usedNodes, buildVersion, regions);
if (alert) {
const activationTime =
"$date" in alert.Activation
? Number(alert.Activation.$date.$numberLong)
: alert.Activation.sec * 1000;
const expiryTime =
"$date" in alert.Expiry ? Number(alert.Expiry.$date.$numberLong) : alert.Expiry.sec * 1000;
if (timeMs >= activationTime && timeMs < expiryTime) {
usedNodes.add(alert.MissionInfo.location);
const isPreU14 = buildVersion < gameToBuildVersionInt["14.0.0"];
if (alert.MissionInfo.missionReward) {
if (alert.MissionInfo.missionReward.items) {
alert.MissionInfo.missionReward.items = alert.MissionInfo.missionReward.items.map(item => {
let processedItem = item;
if (processedItem.includes("/Recipes/Helmets/")) {
processedItem = getVersionAppropriateHelmet(processedItem, buildVersion);
}
if (isPreU14) {
const parts = processedItem.split("/");
const filename = parts[parts.length - 1];
if (processedItem.includes("/Recipes/WarframeRecipes/")) {
return `/Lotus/Types/Recipes/WarframeRecipes/${filename}StoreItem`;
}
const isPreU13 = buildVersion < gameToBuildVersionInt["13.0.0"];
if (filename === "FormaBlueprint") {
if (isPreU13) {
return "/Lotus/Types/StoreItems/Recipes/OrokinCatalystBlueprintStoreItem";
}
return "/Lotus/Types/Recipes/Components/FormaBlueprintStoreItem";
}
if (filename === "Forma") {
if (isPreU13) {
return "/Lotus/Types/StoreItems/Recipes/OrokinCatalystStoreItem";
}
return "/Lotus/Types/Recipes/Components/FormaStoreItem";
}
if (filename === "OrokinReactorBlueprint") {
if (isPreU13) {
return "/Lotus/Types/StoreItems/Recipes/OrokinReactorBlueprintStoreItem";
}
return "/Lotus/Types/Recipes/Components/OrokinReactorBlueprintStoreItem";
}
if (filename === "OrokinCatalystBlueprint") {
return "/Lotus/Types/StoreItems/Recipes/OrokinCatalystBlueprintStoreItem";
}
if (filename.endsWith("Blueprint")) {
return `/Lotus/Types/StoreItems/Recipes/${filename}StoreItem`;
}
return processedItem;
}
return processedItem;
});
}
if (alert.MissionInfo.missionReward.countedItems) {
if (isPreU14) {
alert.MissionInfo.missionReward.countedItems =
alert.MissionInfo.missionReward.countedItems.map(ci => {
if (ci.ItemType === "/Lotus/Types/Items/MiscItems/ArgonCrystal") {
return {
ItemType: "/Lotus/Types/Items/MiscItems/OrokinCell",
ItemCount: ci.ItemCount
};
}
if (ci.ItemType === "/Lotus/Types/Items/MiscItems/OxiumAlloy") {
return {
ItemType: "/Lotus/Types/Items/MiscItems/Gallium",
ItemCount: Math.max(1, Math.round(ci.ItemCount / 10))
};
}
if (ci.ItemType === "/Lotus/Types/Items/MiscItems/Tellurium") {
return {
ItemType: "/Lotus/Types/Items/MiscItems/Morphic",
ItemCount: ci.ItemCount
};
}
if (ci.ItemType === "/Lotus/Types/Items/MiscItems/Alertium") {
return {
ItemType: "/Lotus/Types/Items/MiscItems/Neurode",
ItemCount: ci.ItemCount
};
}
return ci;
});
}
}
}
activeAlerts.push(alert);
}
}
}
worldState.Alerts.push(...activeAlerts);
}
};
export const updateWorldStateCollections = async (): Promise<void> => {
await Promise.all([updateFissures(), updateDailyDeal()]);
};
const pushConclaveDaily = (
activeChallenges: IPVPChallengeInstance[],
PVPMode: string,
pool: {
key: string;
ScriptParamValue: number;
PVPModeAllowed: string[];
SyndicateXP: number;
DuringSingleMatch?: boolean;
}[],
day: number,
id: number,
buildVersion: number
): void => {
if (buildVersion < gameToBuildVersionInt["18.0.2"]) {
PVPMode = PVPMode.replace("PVPMODE_", "");
}
const conclaveDayStart = EPOCH + day * unixTimesInMs.day + 5 * unixTimesInMs.hour + 30 * unixTimesInMs.minute;
const conclaveDayEnd = conclaveDayStart + unixTimesInMs.day;
const challengeId = day * 8 + id;
const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
let challenge: {
key: string;
ScriptParamValue: number;
PVPModeAllowed?: string[];
SyndicateXP?: number;
DuringSingleMatch?: boolean;
};
do {
challenge = rng.randomElement(pool)!;
} while (
activeChallenges.some(x => x.challengeTypeRefID == challenge.key) &&
activeChallenges.some(x => x.PVPMode == PVPMode)
);
activeChallenges.push({
_id: toOid2("689ec5d985b55902" + challengeId.toString().padStart(8, "0"), buildVersion),
challengeTypeRefID: challenge.key,
startDate: toMongoDate2(conclaveDayStart, buildVersion),
endDate: toMongoDate2(conclaveDayEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: challenge.ScriptParamValue }],
isGenerated: true,
PVPMode,
subChallenges: [],
Category: "PVPChallengeTypeCategory_DAILY"
});
};
const pushConclaveDailys = (activeChallenges: IPVPChallengeInstance[], day: number, buildVersion: number): void => {
const modes = ["PVPMODE_CAPTURETHEFLAG", "PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"];
// closest known version to Update: Lunaro
if (buildVersion > gameToBuildVersionInt["18.13.3"]) {
modes.push("PVPMODE_SPEEDBALL");
}
const challengesMap: Record<
string,
{
key: string;
ScriptParamValue: number;
PVPModeAllowed: string[];
SyndicateXP: number;
DuringSingleMatch?: boolean;
}[]
> = {};
for (const mode of modes) {
challengesMap[mode] = Object.entries(pvpChallenges)
.filter(([_, challenge]) => challenge.PVPModeAllowed.includes(mode))
.map(([key, challenge]) => ({ key, ...challenge }));
}
modes.forEach((mode, index) => {
pushConclaveDaily(activeChallenges, mode, challengesMap[mode], day, index * 2, buildVersion);
pushConclaveDaily(activeChallenges, mode, challengesMap[mode], day, index * 2 + 1, buildVersion);
});
};
const pushConclaveWeekly = (activeChallenges: IPVPChallengeInstance[], week: number, buildVersion: number): void => {
const weekStart = EPOCH + week * unixTimesInMs.week;
const conclaveWeekStart = weekStart + 40 * unixTimesInMs.minute - 2 * unixTimesInMs.day;
const conclaveWeekEnd = conclaveWeekStart + unixTimesInMs.week;
const conclaveIdStart = ((conclaveWeekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0").padEnd(23, "0");
activeChallenges.push(
{
_id: toOid2(conclaveIdStart + "1", buildVersion),
challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeGameModeWins",
startDate: toMongoDate2(conclaveWeekStart, buildVersion),
endDate: toMongoDate2(conclaveWeekEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: 6 }],
isGenerated: true,
PVPMode: "PVPMODE_ALL",
subChallenges: [],
Category: "PVPChallengeTypeCategory_WEEKLY"
},
{
_id: toOid2(conclaveIdStart + "2", buildVersion),
challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeGameModeComplete",
startDate: toMongoDate2(conclaveWeekStart, buildVersion),
endDate: toMongoDate2(conclaveWeekEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: 20 }],
isGenerated: true,
PVPMode: "PVPMODE_ALL",
subChallenges: [],
Category: "PVPChallengeTypeCategory_WEEKLY"
},
{
_id: toOid2(conclaveIdStart + "3", buildVersion),
challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeOtherChallengeCompleteANY",
startDate: toMongoDate2(conclaveWeekStart, buildVersion),
endDate: toMongoDate2(conclaveWeekEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: 10 }],
isGenerated: true,
PVPMode: "PVPMODE_ALL",
subChallenges: [],
Category: "PVPChallengeTypeCategory_WEEKLY"
},
{
_id: toOid2(conclaveIdStart + "4", buildVersion),
challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeWeeklyStandardSet",
startDate: toMongoDate2(conclaveWeekStart, buildVersion),
endDate: toMongoDate2(conclaveWeekEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: 0 }],
isGenerated: true,
PVPMode: "PVPMODE_NONE",
subChallenges: [
toOid2(conclaveIdStart + "1", buildVersion),
toOid2(conclaveIdStart + "2", buildVersion),
toOid2(conclaveIdStart + "3", buildVersion)
],
Category: "PVPChallengeTypeCategory_WEEKLY_ROOT"
}
);
};
const pushGoalAlerts = (ws: IWorldState, tag: string, buildVersion: number): void => {
const genMeta = alertGeneratorConfig[tag];
const neededAlerts = genMeta.alertLength / genMeta.alertInterval;
const currentAlertIndex = Math.floor((ws.Time * 1000 - EPOCH) / genMeta.alertInterval);
for (let i = 0; i <= neededAlerts; i++) {
ws.Alerts.push(generateGoalAlert(tag, currentAlertIndex - i, ws.Alerts, buildVersion));
}
};
const generateGoalAlert = (tag: string, alertIdx: number, wsAlerts: IAlert[], buildVersion: number): IAlert => {
const genMeta = alertGeneratorConfig[tag];
const defenseWavesPerRotation = buildVersion < gameToBuildVersionInt["38.5.0"] ? 5 : 3;
const activation = EPOCH + alertIdx * genMeta.alertInterval;
const expiry = activation + genMeta.alertLength;
const tagHash = catBreadHash(tag);
const rng = new SRng(alertIdx ^ tagHash);
const allowedNodes = Object.fromEntries(
Object.entries(ExportRegions).filter(
([nodeTag, node]) =>
genMeta.allowedSystemIndexes.includes(node.systemIndex) &&
node.nodeType != 3 && // not hub
node.nodeType != 7 && // not junction
!["MT_ASSASSINATION", "MT_LANDSCAPE", "MT_RAILJACK", "MT_PVPVE", "MT_PVP", "MT_ARENA"].includes(
node.missionType
) &&
node.name.indexOf("1999NodeI") == -1 && // not stage defense
node.name.indexOf("1999NodeJ") == -1 && // not lich bounty
node.name.indexOf("SolarMapEntratiNode") == -1 && // not albrecht lab mission
!isArchwingMission(node) &&
!wsAlerts.map(a => a.MissionInfo.location).includes(nodeTag)
)
);
let nodeTag: string;
let node: IRegion;
do {
[nodeTag, node] = rng.randomElement(Object.entries(allowedNodes))!;
if (!node.tileset) logger.debug(`node ${nodeTag} without tileset - reroll it`);
else if (!node.faction) logger.debug(`node ${nodeTag} without faction - reroll it`);
} while (!node.tileset || !node.faction);
let missionType: string;
let mission: ITilesetMission;
do {
[missionType, mission] = rng.randomElement(Object.entries(ExportTilesets[node.tileset].missions))!;
} while (
["MT_ASSASSINATION", "MT_LANDSCAPE", "MT_RAILJACK", "MT_PVPVE", "MT_PVP", "MT_ARENA"].includes(missionType)
);
const oid =
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
tagHash.toString(16).padEnd(8, "0") +
(alertIdx & 0xffffffff).toString(16).padStart(8, "0");
const alert: IAlert = {
_id: { $oid: oid },
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
MissionInfo: {
location: nodeTag,
missionType: missionType as TMissionType,
faction: node.faction,
difficulty: 1,
seed: alertIdx,
levelOverride: mission.procLevel,
...genMeta.baseMissionInfo
},
...genMeta.baseAlert
};
if (mission.enemySpecs) alert.MissionInfo.enemySpec = rng.randomElement(mission.enemySpecs);
if (mission.extraEnemySpecs) alert.MissionInfo.extraEnemySpec = rng.randomElement(mission.extraEnemySpecs);
if (mission.vipAgent) alert.MissionInfo.vipAgent = mission.vipAgent;
if (mission.advancedSpawners) alert.MissionInfo.customAdvancedSpawners = mission.advancedSpawners;
if (["MT_INTEL", "MT_TERRITORY", "MT_EXCAVATE"].includes(missionType)) {
alert.MissionInfo.maxWaveNum = 2;
} else if (missionType == "MT_SURVIVAL") {
alert.MissionInfo.maxWaveNum = 5 * genMeta.baseMissionInfo.maxRotations!;
} else if (missionType == "MT_DEFENSE") {
alert.MissionInfo.maxWaveNum = defenseWavesPerRotation * genMeta.baseMissionInfo.maxRotations!;
}
delete alert.MissionInfo.maxRotations;
return alert;
};
export const populateFeaturedGuilds = async (worldState: IWorldState): Promise<void> => {
const guilds = await Guild.find({ Featured: true }, "Name Tier AllianceId Emblem");
for (const guild of guilds) {
worldState.FeaturedGuilds.push({
_id: toOid(guild._id),
Name: guild.Name,
Tier: guild.Tier,
AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined,
Emblem: guild.Emblem
});
}
};
const pushFlashSales = (
ws: IWorldState,
storeItems: IFlashSaleData[],
startDate: number | Date,
endDate: number | Date,
category: string,
buildVersion: number
): void => {
const filteredItems = storeItems.filter(
item => !item.minBuildVersionInt || buildVersion >= item.minBuildVersionInt
);
ws.FlashSales.push(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
...filteredItems.map(({ minBuildVersionInt, ...item }) => ({
...item,
StartDate: toMongoDate2(startDate, buildVersion),
EndDate: toMongoDate2(endDate, buildVersion),
ProductExpiryOverride: toMongoDate2(endDate, buildVersion),
// Defaulting all of this is probably not needed, but might be needed for Market 2.0 (pre-U25):
ShowInMarket: item.ShowInMarket ?? true,
HideFromMarket: item.HideFromMarket ?? false,
SupporterPack: item.SupporterPack ?? false,
Discount: item.Discount ?? 0,
BogoBuy: item.BogoBuy ?? 0,
BogoGet: item.BogoGet ?? 0,
RegularOverride: item.RegularOverride ?? 0,
PremiumOverride: item.PremiumOverride ?? 0,
Featured: item.Featured ?? category != "POPULAR",
Popular: item.Popular ?? category == "POPULAR",
BannerIndex: item.BannerIndex ?? 10
}))
);
const seasonalCategory = ws.InGameMarket.LandingPage.Categories.find(c => c.CategoryName == category);
if (!seasonalCategory) {
throw new Error(`No market category ${category} in static worldState data`);
}
seasonalCategory.Items!.push(...filteredItems.map(x => x.TypeName));
};
import baro, { type IBaroDataTradeOffer } from "../constants/baro.ts";
import varzia from "../constants/varzia.ts";
import fissureMissions from "../../static/fixed_responses/worldState/fissureMissions.json" with { type: "json" };
import sortieTilesets from "../../static/fixed_responses/worldState/sortieTilesets.json" with { type: "json" };
import sortieTilesetMissions from "../../static/fixed_responses/worldState/sortieTilesetMissions.json" with { type: "json" };
import syndicateMissions from "../../static/fixed_responses/worldState/syndicateMissions.json" with { type: "json" };
import darvoDeals from "../constants/darvoDeals.ts";
import invasionNodes from "../../static/fixed_responses/worldState/invasionNodes.json" with { type: "json" };
import invasionRewards from "../../static/fixed_responses/worldState/invasionRewards.json" with { type: "json" };
import pvpChallenges from "../../static/fixed_responses/worldState/pvpChallenges.json" with { type: "json" };
import { EPOCH, unixTimesInMs } from "../constants/timeConstants.ts";
import { config } from "./configService.ts";
import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "./rngService.ts";
import type { IMissionReward, IRegion, ITilesetMission, TFaction, TMissionType } from "warframe-public-export-plus";
import { ExportRegions, ExportSyndicates, ExportTilesets, ExportRecipes } from "warframe-public-export-plus";
import type {
ICalendarDay,
ICalendarEvent,
ICalendarSeason,
IAlert,
IGoal,
IInvasion,
ILiteSortie,
IPrimeVaultTrader,
IPrimeVaultTraderOffer,
IPVPChallengeInstance,
ISeasonChallenge,
ISortie,
ISortieMission,
ISyndicateMissionInfo,
ITmp,
IVoidStorm,
IVoidTrader,
IWorldState,
TCircuitGameMode,
IFlashSale,
IAlertMissionInfo,
IEndlessXpChoice,
IGoalV9
} from "../types/worldStateTypes.ts";
import { toMongoDate2, toOid, toOid2 } from "../helpers/inventoryHelpers.ts";
import { logger } from "../utils/logger.ts";
import { DailyDeal, Fissure } from "../models/worldStateModel.ts";
import { toStoreItem, fromStoreItem, getRegions } from "./itemDataService.ts";
import { factionToInt, getConquest, getMissionTypeForLegacyOverride } from "./conquestService.ts";
import { getDescent } from "./descentService.ts";
import { catBreadHash } from "../helpers/stringHelpers.ts";
import { Guild } from "../models/guildModel.ts";
import { libraryTargetToAvatar } from "../constants/synthesis.ts";
import { BL_LATEST, BV_LATEST } from "../constants/gameVersions.ts";
import { isRegionAvailableIn } from "./itemDataService.ts";
import gameToBuildVersionInt from "../constants/gameToBuildVersionInt.ts";
import { buildVersionToInt } from "../helpers/versionHelper.ts";
import {
dogDaysFlashSales,
naberusNightsFlashSales,
prideMonthFlashSales,
qqtcFlashSales,
saintPatrickDayFlashSales,
tennobaumFlashSales,
type IFlashSaleData
} from "../constants/flashSales.ts";
const sortieBosses = [
"SORTIE_BOSS_HYENA",
"SORTIE_BOSS_KELA",
"SORTIE_BOSS_VOR",
"SORTIE_BOSS_RUK",
"SORTIE_BOSS_HEK",
"SORTIE_BOSS_KRIL",
"SORTIE_BOSS_TYL",
"SORTIE_BOSS_JACKAL",
"SORTIE_BOSS_ALAD",
"SORTIE_BOSS_AMBULAS",
"SORTIE_BOSS_NEF",
"SORTIE_BOSS_RAPTOR",
"SORTIE_BOSS_PHORID",
"SORTIE_BOSS_LEPHANTIS",
"SORTIE_BOSS_INFALAD",
"SORTIE_BOSS_CORRUPTED_VOR"
] as const;
type TSortieBoss = (typeof sortieBosses)[number];
const sortieBossToFaction: Record<TSortieBoss, TFaction> = {
SORTIE_BOSS_HYENA: "FC_CORPUS",
SORTIE_BOSS_KELA: "FC_GRINEER",
SORTIE_BOSS_VOR: "FC_GRINEER",
SORTIE_BOSS_RUK: "FC_GRINEER",
SORTIE_BOSS_HEK: "FC_GRINEER",
SORTIE_BOSS_KRIL: "FC_GRINEER",
SORTIE_BOSS_TYL: "FC_GRINEER",
SORTIE_BOSS_JACKAL: "FC_CORPUS",
SORTIE_BOSS_ALAD: "FC_CORPUS",
SORTIE_BOSS_AMBULAS: "FC_CORPUS",
SORTIE_BOSS_NEF: "FC_CORPUS",
SORTIE_BOSS_RAPTOR: "FC_CORPUS",
SORTIE_BOSS_PHORID: "FC_INFESTATION",
SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION",
SORTIE_BOSS_INFALAD: "FC_INFESTATION",
SORTIE_BOSS_CORRUPTED_VOR: "FC_OROKIN"
};
const sortieFactionToSystemIndexes: Record<string, number[]> = {
FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 17, 18],
FC_CORPUS: [1, 4, 7, 8, 12, 15, 17],
FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16],
FC_OROKIN: [14]
};
const sortieFactionToFactions: Record<string, TFaction[]> = {
FC_GRINEER: ["FC_GRINEER"],
FC_CORPUS: ["FC_CORPUS"],
FC_INFESTATION: ["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION"],
FC_OROKIN: ["FC_OROKIN"]
};
const sortieBossNode: Record<Exclude<TSortieBoss, "SORTIE_BOSS_CORRUPTED_VOR">, string> = {
SORTIE_BOSS_ALAD: "SolNode53",
SORTIE_BOSS_AMBULAS: "SolNode51",
SORTIE_BOSS_HEK: "SolNode24",
SORTIE_BOSS_HYENA: "SolNode127",
SORTIE_BOSS_INFALAD: "SolNode166",
SORTIE_BOSS_JACKAL: "SolNode104",
SORTIE_BOSS_KELA: "SolNode193",
SORTIE_BOSS_KRIL: "SolNode99",
SORTIE_BOSS_LEPHANTIS: "SolNode712",
SORTIE_BOSS_NEF: "SettlementNode20",
SORTIE_BOSS_PHORID: "SolNode171",
SORTIE_BOSS_RAPTOR: "SolNode210",
SORTIE_BOSS_RUK: "SolNode32",
SORTIE_BOSS_TYL: "SolNode105",
SORTIE_BOSS_VOR: "SolNode108"
};
const sortieAssassinationOnlyNodes: string[] = ["SolNode193", "SolNode105", "SolNode108"];
const configAlerts: Record<string, IAlert & { minBuildVersion?: number }> = {
voidCorruption2025Week1: {
_id: { $oid: "677d452e2f324ee7b90f8ccf" },
Activation: { $date: { $numberLong: "1736524800000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode61",
missionType: "MT_SABOTAGE",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 30000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedFireRateDamagePistol"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipCoreSabotage",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipSquadA",
extraEnemySpec: "/Lotus/Types/Game/EnemySpecs/GamemodeExtraEnemySpecs/CorpusSabotageTiersA",
minEnemyLevel: 10,
maxEnemyLevel: 15
}
},
voidCorruption2025Week2: {
_id: { $oid: "677d45811daeae9de40e8c0f" },
Activation: { $date: { $numberLong: "1737129600000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SettlementNode11",
missionType: "MT_DEFENSE",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 30000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritChanceFireRatePistol"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipDefense",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipSquadDefenseB",
minEnemyLevel: 20,
maxEnemyLevel: 25,
maxRotations: 2
}
},
voidCorruption2025Week3: {
_id: { $oid: "677d45a494ad716c90006b9a" },
Activation: { $date: { $numberLong: "1737734400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode118",
missionType: "MT_ARTIFACT",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 30000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritDamagePistol"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipDisruption",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipSurvivalA",
extraEnemySpec: "/Lotus/Types/Game/EnemySpecs/SpecialMissionSpecs/DisruptionCorpusShip",
customAdvancedSpawners: ["/Lotus/Types/Enemies/AdvancedSpawners/ErrantSpecterInvasion"],
minEnemyLevel: 30,
maxEnemyLevel: 35
}
},
voidCorruption2025Week4: {
_id: { $oid: "677d4700682d173abb0e19fe" },
Activation: { $date: { $numberLong: "1738339200000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode4",
missionType: "MT_EXTERMINATION",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 30000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedDamageRecoilPistol"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipExterminate",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipExterminateMixed",
minEnemyLevel: 40,
maxEnemyLevel: 45
}
},
dagathAlerts2026Week1: {
_id: { $oid: "6949862e3f3f35491e02232c" },
Activation: { $date: { $numberLong: "1769022000000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode147",
missionType: "MT_TERRITORY",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Recipes/WarframeRecipes/DagathChassisComponent"]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerShipyardsInterception",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerShipyardsDefenseA",
minEnemyLevel: 25,
maxEnemyLevel: 35,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
dagathAlerts2026Week2: {
_id: { $oid: "6949863a548d83b650011de8" },
Activation: { $date: { $numberLong: "1769626800000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode404",
missionType: "MT_SABOTAGE",
faction: "FC_OROKIN",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Recipes/WarframeRecipes/DagathHelmetComponent"]
},
levelOverride: "/Lotus/Levels/Proc/Orokin/OrokinTowerSabotageForest",
enemySpec: "/Lotus/Types/Game/EnemySpecs/OrokinSquadOne",
extraEnemySpec: "/Lotus/Types/Game/EnemySpecs/OrokinSabotageGrineerForest",
minEnemyLevel: 25,
maxEnemyLevel: 35,
vipAgent: "/Lotus/Types/Enemies/Grineer/Vip/JetpackSisters/JetpackSisters",
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
dagathAlerts2026Week3: {
_id: { $oid: "69498642a0836c98e10f5e34" },
Activation: { $date: { $numberLong: "1770231600000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode100",
missionType: "MT_SURVIVAL",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Recipes/WarframeRecipes/DagathSystemsComponent"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusGasCitySurvival",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusGasSurvival",
minEnemyLevel: 25,
maxEnemyLevel: 35,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
dagathAlerts2026Week4: {
_id: { $oid: "69498623a7342ab1500cc99b" },
Activation: { $date: { $numberLong: "1768417200000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode103",
missionType: "MT_EXTERMINATION",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Recipes/WarframeRecipes/DagathBlueprint"]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerAsteroidExterminate",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerExterminateBlades",
minEnemyLevel: 25,
maxEnemyLevel: 35,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
starDaysAlerts2026Week1: {
_id: { $oid: "696935998429008d89100af9" },
Activation: { $date: { $numberLong: "1770393600000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode22",
missionType: "MT_DEFENSE",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 25000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageStarDaysStarDaysMarieGlyph"],
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/OrokinCatalyst", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusOutpostDefense",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusSquadDefenseB",
minEnemyLevel: 25,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/LotusGiftDesc",
maxRotations: 1
},
Tag: "LotusGift",
ForceUnlock: true
},
starDaysAlerts2026Week2: {
_id: { $oid: "696935b47918af0b1c0c4c2f" },
Activation: { $date: { $numberLong: "1770998400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode11",
missionType: "MT_MOBILE_DEFENSE",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 25000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageStarDaysLyonGlyph"],
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/OrokinReactor", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerSettlementMobileDefense",
enemySpec: "/Lotus/Types/Game/EnemySpecs/DesertGrineerSquadOne",
minEnemyLevel: 25,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
starDaysAlerts2026Week3: {
_id: { $oid: "696935d518288b03bb0224ae" },
Activation: { $date: { $numberLong: "1771603200000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode121",
missionType: "MT_EXTERMINATION",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 25000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageStarDaysRoatheGlyph"],
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/Forma", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusGasCityExterminate",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusGasExterminateMixed",
minEnemyLevel: 25,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
starDaysAlerts2026Week4: {
_id: { $oid: "696935ed8429008d89100cea" },
Activation: { $date: { $numberLong: "1772208000000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode82",
missionType: "MT_SABOTAGE",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 25000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Props/Seasonal/StarDays2026HeartPictureFrame"],
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/UtilityUnlocker", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerGalleonCoreSabotage",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerSquadSix",
extraEnemySpec: "/Lotus/Types/Game/EnemySpecs/GamemodeExtraEnemySpecs/GrineerSabotageTiersTwo",
minEnemyLevel: 25,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/LotusGiftDesc"
},
Tag: "LotusGift",
ForceUnlock: true
},
destiny2TributeAlert: {
_id: { $oid: "6a29b21c614c39ecbb0aa925" },
Activation: { $date: { $numberLong: "1781708400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
MissionInfo: {
location: "SolNode20",
missionType: "MT_EXTERMINATION",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
countedItems: [{ ItemType: "/Lotus/Types/Items/Titles/GuardianTitle", ItemCount: 1 }]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerGalleonExterminate",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerExterminateSimple",
minEnemyLevel: 10,
maxEnemyLevel: 15,
descText: "/Lotus/Language/Alerts/LotusGiftDesc",
maxWaveNum: 167
},
Tag: "LotusGift",
ForceUnlock: true,
minBuildVersion: gameToBuildVersionInt["43.0.0"]
}
};
const alertGeneratorConfig: Record<
string,
{
alertInterval: number;
alertLength: number;
allowedSystemIndexes: number[];
baseAlert: Partial<IAlert>;
baseMissionInfo: Partial<IAlertMissionInfo>;
}
> = {
"12MinWarEvent": {
alertInterval: unixTimesInMs.hour / 2,
alertLength: unixTimesInMs.hour * 1.5,
allowedSystemIndexes: [14],
baseAlert: {
Tag: "12MinWarEvent",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/BloodOfPeritaEventBadge.png"
},
baseMissionInfo: {
minEnemyLevel: 65,
maxEnemyLevel: 70,
maxRotations: 1,
missionReward: {
credits: 8900, // should be random
countedItems: [
{
ItemType: "/Lotus/Types/Gameplay/Tau/Resources/TwelveResourceCurrencyItem",
ItemCount: 20
}
]
},
descText: "/Lotus/Language/TauPrequel/TauPrequelFinal/TauPrequelEventAlertTitle",
questReq: "/Lotus/Types/Keys/TauPrequel/TauPrequelQuestKeyChain",
leadersAlwaysAllowed: true
}
},
JadeShadows: {
alertInterval: unixTimesInMs.hour / 2,
alertLength: unixTimesInMs.hour * 2,
allowedSystemIndexes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18],
baseAlert: {
Tag: "JadeShadows",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/JadeShadowsEventBadge.png"
},
baseMissionInfo: {
minEnemyLevel: 50,
maxEnemyLevel: 50,
maxRotations: 1,
missionReward: {
credits: 8900, // should be random
countedItems: [
{
ItemType: "/Lotus/Types/Gameplay/JadeShadows/Resources/AscensionEventResourceItem",
ItemCount: 10
}
]
},
descText: "/Lotus/Language/JadeShadows/EventAlertTitle",
questReq: "/Lotus/Types/Keys/JadeShadows/JadeShadowQuestKeyChain",
leadersAlwaysAllowed: true
}
}
};
const eidolonJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap",
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt",
"/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap",
"/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft",
"/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache",
"/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne",
"/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo",
"/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab",
"/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc"
];
const eidolonNarmerJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft",
"/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",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy"
];
const venusNarmerJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate",
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate",
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense",
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation"
];
const microplanetJobs: readonly string[] = [
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty"
];
const microplanetEndlessJobs: readonly string[] = [
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty",
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
];
const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => {
return nowMs + 300_000 > thenMs;
};
const getSortieTime = (day: number): number => {
const dayStart = EPOCH + day * 86400000;
const date = new Date(dayStart);
date.setUTCHours(12);
const isDst = new Intl.DateTimeFormat("en-US", {
timeZone: "America/Toronto",
timeZoneName: "short"
})
.formatToParts(date)
.find(part => part.type === "timeZoneName")!
.value.includes("DT");
return dayStart + (isDst ? 16 : 17) * 3600000;
};
const pushSyndicateMissions = (
worldState: IWorldState,
day: number,
seed: number,
idSuffix: string,
syndicateTag: string
): void => {
const nodeOptions: string[] = [...syndicateMissions];
const rng = new SRng(seed);
const nodes: string[] = [];
for (let i = 0; i != 6; ++i) {
const index = rng.randomInt(0, nodeOptions.length - 1);
nodes.push(nodeOptions[index]);
nodeOptions.splice(index, 1);
}
const dayStart = getSortieTime(day);
const dayEnd = getSortieTime(day + 1);
worldState.SyndicateMissions.push({
_id: toOid2(((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + idSuffix, worldState.BuildLabel),
Activation: toMongoDate2(dayStart, worldState.BuildLabel),
Expiry: toMongoDate2(dayEnd, worldState.BuildLabel),
Tag: syndicateTag,
Seed: seed,
Nodes: nodes
});
};
type TSortieTileset = keyof typeof sortieTilesetMissions;
const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): void => {
switch (tileset) {
case "GrineerForestTileset":
modifiers.push("SORTIE_MODIFIER_HAZARD_FOG");
break;
case "CorpusShipTileset":
case "GrineerGalleonTileset":
case "InfestedCorpusShipTileset":
modifiers.push("SORTIE_MODIFIER_HAZARD_MAGNETIC");
modifiers.push("SORTIE_MODIFIER_HAZARD_FIRE");
modifiers.push("SORTIE_MODIFIER_HAZARD_ICE");
break;
case "CorpusIcePlanetTileset":
case "CorpusIcePlanetTilesetCaves":
modifiers.push("SORTIE_MODIFIER_HAZARD_COLD");
break;
}
};
export const getSortie = (day: number, buildVersion: number): ISortie => {
// Seeds featuring CorpusIcePlanet to test seed validation: 14197, 23418, 71281
const seed = new SRng(day).randomInt(0, 100_000);
logger.trace(`sortie seed: ${seed}`);
const rng = new SRng(seed);
const boss = rng.randomElement(sortieBosses)!;
const enemyFaction = sortieBossToFaction[boss];
const nodes: string[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
key in sortieTilesets &&
(key != "SolNode228" || enemyFaction == "FC_GRINEER") && // PoE only works for grineer enemies
sortieFactionToSystemIndexes[enemyFaction].includes(value.systemIndex) &&
sortieFactionToFactions[enemyFaction].includes(value.faction!) &&
isRegionAvailableIn(key, value, buildVersion)
) {
nodes.push(key);
}
}
const willHaveAssassination = boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2;
if (willHaveAssassination) {
const index = nodes.indexOf(sortieBossNode[boss]);
if (index != -1) {
nodes.splice(index, 1);
}
}
const selectedNodes: ISortieMission[] = [];
const missionTypes = new Set();
if (enemyFaction == "FC_INFESTATION") {
// MT_RETRIEVAL may not be chosen for infested enemies (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2907)
missionTypes.add("MT_RETRIEVAL");
}
for (let i = 0; i < 3; i++) {
let randomIndex, node;
do {
randomIndex = rng.randomInt(0, nodes.length - 1);
node = nodes[randomIndex];
} while (sortieAssassinationOnlyNodes.indexOf(node) != -1);
const modifiers = [
"SORTIE_MODIFIER_LOW_ENERGY",
"SORTIE_MODIFIER_IMPACT",
"SORTIE_MODIFIER_SLASH",
"SORTIE_MODIFIER_PUNCTURE",
"SORTIE_MODIFIER_EXIMUS",
"SORTIE_MODIFIER_MAGNETIC",
"SORTIE_MODIFIER_CORROSIVE",
"SORTIE_MODIFIER_VIRAL",
"SORTIE_MODIFIER_ELECTRICITY",
"SORTIE_MODIFIER_RADIATION",
"SORTIE_MODIFIER_FIRE",
"SORTIE_MODIFIER_EXPLOSION",
"SORTIE_MODIFIER_FREEZE",
"SORTIE_MODIFIER_POISON",
"SORTIE_MODIFIER_SECONDARY_ONLY",
"SORTIE_MODIFIER_SHOTGUN_ONLY",
"SORTIE_MODIFIER_SNIPER_ONLY",
"SORTIE_MODIFIER_RIFLE_ONLY",
"SORTIE_MODIFIER_BOW_ONLY"
];
if (i == 2 && willHaveAssassination) {
const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] as TSortieTileset;
pushTilesetModifiers(modifiers, tileset);
const modifierType = rng.randomElement(modifiers)!;
selectedNodes.push({
missionType: "MT_ASSASSINATION",
modifierType,
node: sortieBossNode[boss],
tileset
});
continue;
}
const tileset = sortieTilesets[node as keyof typeof sortieTilesets] as TSortieTileset;
pushTilesetModifiers(modifiers, tileset);
const missionType = rng.randomElement(sortieTilesetMissions[tileset])! as TMissionType;
if (missionTypes.has(missionType) || missionType == "MT_ASSASSINATION") {
i--;
continue;
}
modifiers.push("SORTIE_MODIFIER_MELEE_ONLY"); // not an assassination mission, can now push this
if (missionType != "MT_TERRITORY") {
modifiers.push("SORTIE_MODIFIER_HAZARD_RADIATION");
}
modifiers.push(enemyFaction == "FC_CORPUS" ? "SORTIE_MODIFIER_SHIELDS" : "SORTIE_MODIFIER_ARMOR");
const modifierType = rng.randomElement(modifiers)!;
selectedNodes.push({
missionType,
modifierType,
node,
tileset
});
nodes.splice(randomIndex, 1);
missionTypes.add(missionType);
}
let clientSeed = rng.randomInt(0, 100_000);
while (!validateSortieSeed(clientSeed, enemyFaction, selectedNodes)) {
clientSeed = rng.randomInt(0, 100_000);
}
const dayStart = getSortieTime(day);
const dayEnd = getSortieTime(day + 1);
return {
_id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "d4d932c97c0a3acd" },
Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards",
Seed: clientSeed,
Boss: boss,
Variants: selectedNodes
};
};
const SORTIE_FALLBACK_MISSION_TYPES: TMissionType[] = [
"MT_EXTERMINATION",
"MT_SURVIVAL",
"MT_RESCUE",
"MT_SABOTAGE",
//"MT_CAPTURE",
"MT_INTEL",
"MT_DEFENSE",
"MT_MOBILE_DEFENSE",
"MT_TERRITORY",
"MT_RETRIEVAL",
"MT_HIVE",
"MT_EXCAVATE",
"MT_ARTIFACT"
];
const validateSortieSeed = (seed: number, enemyFaction: TFaction, variants: ISortieMission[]): boolean => {
const rng = new SRng(seed);
for (const variant of variants) {
// Gather variant data as the client would
let missionType = variant.missionType;
const tileset = ExportTilesets[variant.tileset];
if (missionType != "MT_ASSASSINATION") {
let missionPermutation = tileset.missions[variant.missionType];
if (!missionPermutation) {
const options = SORTIE_FALLBACK_MISSION_TYPES.filter(missionType => missionType in tileset.missions);
missionType = options[rng.randomInt(0, options.length - 1)];
missionPermutation = tileset.missions[missionType]!;
logger.trace(
`${variant.missionType} not supported by ${variant.tileset}; picking ${missionType} instead`
);
}
if (missionPermutation.enemySpecs?.length) {
rng.randomInt(1, missionPermutation.enemySpecs.length);
}
if (missionPermutation.extraEnemySpecs?.length) {
rng.randomInt(1, missionPermutation.extraEnemySpecs.length);
}
rng.randomFloat(); // difficulty
}
if (enemyFaction == "FC_INFESTATION" && tileset.faction != "FC_INFESTATION") {
// infested enemySpec
if (missionType == "MT_DEFENSE" || missionType == "MT_TERRITORY" || missionType == "MT_SURVIVAL") {
rng.randomInt(0, 1);
} else if (missionType != "MT_EXCAVATE") {
rng.randomInt(0, 3);
}
}
const locationTextureIndex =
missionType == "MT_ARENA" || missionType == "MT_JUNCTION" ? 0 : rng.randomInt(0, 1);
// Reject this seed if the data looks bad
if (missionType == "MT_RETRIEVAL" && enemyFaction == "FC_INFESTATION") {
return false;
}
if (
(variant.tileset == "CorpusIcePlanetTileset" || variant.tileset == "CorpusIcePlanetTilesetCaves") &&
locationTextureIndex != 0
) {
return false; // For the corpus ice planet tileset, index 1 is an infested corpus ship image, which we don't want.
}
}
return true;
};
interface IRotatingSeasonChallengePools {
daily: readonly string[];
weekly: string[];
hardWeekly: string[];
weeklyPermanent: string[];
}
export const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => {
const syndicate = ExportSyndicates[syndicateTag];
if (!syndicate.dailyChallenges || !syndicate.weeklyChallenges) {
throw new Error(`invalid syndicate tag for nightwave: ${syndicateTag}`);
}
return {
daily: syndicate.dailyChallenges,
weekly: syndicate.weeklyChallenges.filter(
x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
!x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
),
hardWeekly: syndicate.weeklyChallenges.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")),
weeklyPermanent: syndicate.weeklyChallenges.filter(x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
)
};
};
const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => {
const dayStart = EPOCH + day * 86400000;
const dayEnd = EPOCH + (day + 3) * 86400000;
return {
_id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") },
Daily: true,
Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } },
Challenge: sequentiallyUniqueRandomElement(pools.daily, day, 2, 605732938)!
};
};
const pushSeasonWeeklyChallenge = (
activeChallenges: ISeasonChallenge[],
pool: string[],
nightwaveSeason: number,
week: number,
id: number
): void => {
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id;
const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
let challenge: string;
do {
challenge = rng.randomElement(pool)!;
} while (activeChallenges.some(x => x.Challenge == challenge));
activeChallenges.push({
_id: {
$oid:
(nightwaveSeason + 1).toString().padStart(4, "0") +
"bb2d9d00cb47" +
challengeId.toString().padStart(8, "0")
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: challenge
});
};
export const pushWeeklyActs = (
activeChallenges: ISeasonChallenge[],
pools: IRotatingSeasonChallengePools,
week: number,
nightwaveStartTimestamp: number,
nightwaveSeason: number
): void => {
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 0);
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 1);
if (pools.weeklyPermanent.length > 0) {
const weekStart = EPOCH + week * unixTimesInMs.week;
const weekEnd = weekStart + unixTimesInMs.week;
const nightwaveWeekStart = ((): number => {
let ts = nightwaveStartTimestamp - EPOCH;
ts -= ts % unixTimesInMs.week;
return EPOCH + ts;
})();
const nightwaveWeek = Math.trunc((weekStart - nightwaveWeekStart) / unixTimesInMs.week);
const weeklyPermanentIndex = (nightwaveWeek * 3) % pools.weeklyPermanent.length;
for (let i = 0; i < 3; i++) {
activeChallenges.push({
_id: {
$oid:
(nightwaveSeason + 1).toString().padStart(4, "0") +
"b96e9d00cb47" +
(week * 7 + 2 + i).toString().padStart(8, "0")
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: pools.weeklyPermanent[weeklyPermanentIndex + i]
});
}
} else {
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 2);
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 3);
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 4);
}
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 5);
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 6);
};
const eidolonEpoch = 1391990400;
const timeOfDayRate = 0.0026670000515878;
const eidolonCycleDuration = 24 / timeOfDayRate;
const eidolonHourDuration = eidolonCycleDuration / 24;
const bountyEpoch = eidolonEpoch + 5 * eidolonHourDuration;
const generateXpAmounts = (rng: SRng, stageCount: number, minXp: number, maxXp: number): number[] => {
const step = minXp < 1000 ? 1 : 10;
const totalDeciXp = rng.randomInt(minXp / step, maxXp / step);
const xpAmounts: number[] = [];
if (stageCount < 4) {
const perStage = Math.ceil(totalDeciXp / stageCount) * step;
for (let i = 0; i != stageCount; ++i) {
xpAmounts.push(perStage);
}
} else {
const perStage = Math.ceil(Math.round(totalDeciXp * 0.667) / (stageCount - 1)) * step;
for (let i = 0; i != stageCount - 1; ++i) {
xpAmounts.push(perStage);
}
xpAmounts.push(Math.ceil(totalDeciXp * 0.332) * step);
}
return xpAmounts;
};
// Test vectors:
//console.log(generateXpAmounts(new SRng(1337n), 5, 5000, 5000)); // [840, 840, 840, 840, 1660]
//console.log(generateXpAmounts(new SRng(1337n), 3, 40, 40)); // [14, 14, 14]
//console.log(generateXpAmounts(new SRng(1337n), 5, 150, 150)); // [25, 25, 25, 25, 50]
//console.log(generateXpAmounts(new SRng(1337n), 4, 10, 10)); // [2, 2, 2, 4]
//console.log(generateXpAmounts(new SRng(1337n), 4, 15, 15)); // [4, 4, 4, 5]
//console.log(generateXpAmounts(new SRng(1337n), 4, 20, 20)); // [5, 5, 5, 7]
export const pushClassicBounties = (
syndicateMissions: ISyndicateMissionInfo[],
bountyCycle: number,
buildVersion: number
): void => {
const table = String.fromCharCode(65 + (bountyCycle % 3));
const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3));
const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2));
const seed = new SRng(bountyCycle).randomInt(0, 100_000);
const bountyCycleStart = Math.trunc((bountyEpoch + bountyCycle * eidolonCycleDuration) * 1000);
const bountyCycleEnd = Math.trunc(bountyCycleStart + eidolonCycleDuration * 1000);
if (buildVersion >= gameToBuildVersionInt["22.0.0"]) {
const rng = new SRng(seed);
const pool = [...eidolonJobs];
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "CetusSyndicate",
Seed: seed,
Nodes: [],
Jobs: [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`,
// Should be U22.10
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 0
}),
minEnemyLevel: 5,
maxEnemyLevel: 15,
xpAmounts: generateXpAmounts(rng, 3, 1000, 1500)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`,
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 1
}),
minEnemyLevel: 10,
maxEnemyLevel: 30,
xpAmounts: generateXpAmounts(rng, 3, 1750, 2250)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`,
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 2
}),
minEnemyLevel: 20,
maxEnemyLevel: 40,
xpAmounts: generateXpAmounts(rng, 4, 2500, 3000)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`,
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 3
}),
minEnemyLevel: 30,
maxEnemyLevel: 50,
xpAmounts: generateXpAmounts(rng, 5, 3250, 3750)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`,
...(buildVersion >= gameToBuildVersionInt["22.13.4"] && {
masteryReq: 4
}),
minEnemyLevel: 40,
maxEnemyLevel: 60,
xpAmounts: generateXpAmounts(rng, 5, 4000, 4500)
},
// U28.1
...(buildVersion >= gameToBuildVersionInt["28.3.0"]
? [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`,
masteryReq: 10,
minEnemyLevel: 100,
maxEnemyLevel: 100,
xpAmounts: [840, 840, 840, 840, 1660]
}
]
: []),
...(buildVersion >= gameToBuildVersionInt["31.0.0"]
? [
{
jobType: rng.randomElement(eidolonNarmerJobs),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable${table}Rewards`,
masteryReq: 0,
minEnemyLevel: 50,
maxEnemyLevel: 70,
xpAmounts: generateXpAmounts(rng, 5, 4500, 5000)
}
]
: [])
]
});
}
if (buildVersion >= gameToBuildVersionInt["24.0.0"]) {
const rng = new SRng(seed);
const pool = [...venusJobs];
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "SolarisSyndicate",
Seed: seed,
Nodes: [],
Jobs: [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`,
masteryReq: 0,
minEnemyLevel: 5,
maxEnemyLevel: 15,
xpAmounts: generateXpAmounts(rng, 3, 1000, 1500)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`,
masteryReq: 1,
minEnemyLevel: 10,
maxEnemyLevel: 30,
xpAmounts: generateXpAmounts(rng, 3, 1750, 2250)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`,
masteryReq: 2,
minEnemyLevel: 20,
maxEnemyLevel: 40,
xpAmounts: generateXpAmounts(rng, 4, 2500, 3000)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`,
masteryReq: 3,
minEnemyLevel: 30,
maxEnemyLevel: 50,
xpAmounts: generateXpAmounts(rng, 5, 3250, 3750)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`,
masteryReq: 5,
minEnemyLevel: 40,
maxEnemyLevel: 60,
xpAmounts: generateXpAmounts(rng, 5, 4000, 4500)
},
// U28.1
...(buildVersion >= gameToBuildVersionInt["28.3.0"]
? [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`,
masteryReq: 10,
minEnemyLevel: 100,
maxEnemyLevel: 100,
xpAmounts: [840, 840, 840, 840, 1660]
}
]
: []),
...(buildVersion >= gameToBuildVersionInt["31.0.0"]
? [
{
jobType: rng.randomElement(venusNarmerJobs),
rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards",
masteryReq: 0,
minEnemyLevel: 50,
maxEnemyLevel: 70,
xpAmounts: generateXpAmounts(rng, 5, 4500, 5000)
}
]
: [])
]
});
}
if (buildVersion >= gameToBuildVersionInt["29.0.0"]) {
const rng = new SRng(seed);
const pool = [...microplanetJobs];
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "EntratiSyndicate",
Seed: seed,
Nodes: [],
Jobs: [
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`,
masteryReq: 0,
minEnemyLevel: 5,
maxEnemyLevel: 15,
xpAmounts: generateXpAmounts(rng, 3, 12, 18)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`,
masteryReq: 1,
minEnemyLevel: 15,
maxEnemyLevel: 25,
xpAmounts: generateXpAmounts(rng, 3, 24, 36)
},
{
jobType: rng.randomElement(microplanetEndlessJobs),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTable${table}Rewards`,
masteryReq: 5,
minEnemyLevel: 25,
maxEnemyLevel: 30,
endless: true,
xpAmounts: [14, 14, 14]
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`,
masteryReq: 2,
minEnemyLevel: 30,
maxEnemyLevel: 40,
xpAmounts: generateXpAmounts(rng, 4, 72, 88)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`,
masteryReq: 3,
minEnemyLevel: 40,
maxEnemyLevel: 60,
xpAmounts: generateXpAmounts(rng, 5, 115, 135)
},
{
jobType: rng.randomElementPop(pool),
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`,
masteryReq: 10,
minEnemyLevel: 100,
maxEnemyLevel: 100,
xpAmounts: [25, 25, 25, 25, 50]
},
{
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATable${vaultTable}Rewards`,
masteryReq: 5,
minEnemyLevel: 30,
maxEnemyLevel: 40,
xpAmounts: [2, 2, 2, 4],
locationTag: "ChamberB",
isVault: true
},
{
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTable${vaultTable}Rewards`,
masteryReq: 5,
minEnemyLevel: 40,
maxEnemyLevel: 50,
xpAmounts: [4, 4, 4, 5],
locationTag: "ChamberA",
isVault: true
},
{
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTable${vaultTable}Rewards`,
masteryReq: 5,
minEnemyLevel: 50,
maxEnemyLevel: 60,
xpAmounts: [5, 5, 5, 7],
locationTag: "ChamberC",
isVault: true
}
]
});
}
};
const birthdays: number[] = [
1, // Kaya
45, // Lettie
74, // Minerva (MinervaVelemirDialogue_rom.dialogue)
143, // Amir
166, // Flare
191, // Aoi
306, // Eleanor
307, // Arthur
338, // Quincy
355 // Velimir (MinervaVelemirDialogue_rom.dialogue)
];
const getCalendarSeason = (week: number): ICalendarSeason => {
const seasonIndex = week % 4;
const seasonDay1 = [1, 91, 182, 274][seasonIndex];
const seasonDay91 = seasonDay1 + 90;
const eventDays: ICalendarDay[] = [];
for (const day of birthdays) {
if (day < seasonDay1) {
continue;
}
if (day >= seasonDay91) {
break;
}
logger.trace(`birthday on day ${day}`);
eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0
}
const rng = new SRng(new SRng(week).randomInt(0, 100_000));
const challenges = [
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTankHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesHard",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeMedium",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeHard"
];
const rewardRanges: number[] = [];
const upgradeRanges: number[] = [];
for (let i = 0; i != 6; ++i) {
const chunkDay1 = seasonDay1 + i * 15;
const chunkDay13 = chunkDay1 - 1 + 13;
let challengeDay: number;
do {
challengeDay = rng.randomInt(chunkDay1, chunkDay13);
} while (birthdays.indexOf(challengeDay) != -1);
let challengeIndex;
let challenge;
do {
challengeIndex = rng.randomInt(0, challenges.length - 1);
challenge = challenges[challengeIndex];
} while (i < 2 && !challenge.endsWith("Easy")); // First 2 challenges should be easy
challenges.splice(challengeIndex, 1);
logger.trace(`challenge on day ${challengeDay}`);
eventDays.push({
day: challengeDay,
events: [{ type: "CET_CHALLENGE", challenge }]
});
rewardRanges.push(challengeDay);
if (i == 0 || i == 3 || i == 5) {
upgradeRanges.push(challengeDay);
}
}
rewardRanges.push(seasonDay91);
upgradeRanges.push(seasonDay91);
const rewards = [
"/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker",
"/Lotus/StoreItems/Types/Recipes/Components/FormaAuraBlueprint",
"/Lotus/StoreItems/Types/Recipes/Components/FormaBlueprint",
"/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker",
"/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle",
"/Lotus/StoreItems/Types/BoosterPacks/CalendarRivenPack",
"/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall",
"/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleLarge",
"/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack",
"/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack",
"/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem",
"/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem",
"/Lotus/Types/StoreItems/Boosters/ResourceDropChance3DayStoreItem",
"/Lotus/StoreItems/Types/Items/MiscItems/Forma",
"/Lotus/StoreItems/Types/Recipes/Components/OrokinCatalystBlueprint",
"/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker",
"/Lotus/Types/StoreItems/Packages/Calendar/CalendarVosforPack",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalOrange",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalGreen",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar",
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalViolet"
];
for (let i = 0; i != rewardRanges.length - 1; ++i) {
const events: ICalendarEvent[] = [];
for (let j = 0; j != 2; ++j) {
const rewardIndex = rng.randomInt(0, rewards.length - 1);
events.push({ type: "CET_REWARD", reward: rewards[rewardIndex] });
rewards.splice(rewardIndex, 1);
}
logger.trace(`trying to fit rewards between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`);
let day: number;
do {
day = rng.randomInt(rewardRanges[i] + 1, rewardRanges[i + 1] - 1);
} while (eventDays.find(x => x.day == day));
eventDays.push({ day, events });
}
const upgradesByHexMember = [
[
"/Lotus/Upgrades/Calendar/AttackAndMovementSpeedOnCritMelee",
"/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump",
"/Lotus/Upgrades/Calendar/ElectricDamagePerDistance",
"/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance",
"/Lotus/Upgrades/Calendar/OvershieldCap",
"/Lotus/Upgrades/Calendar/SpeedBuffsWhenAirborne"
],
[
"/Lotus/Upgrades/Calendar/AbilityStrength",
"/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange",
"/Lotus/Upgrades/Calendar/MagnetStatusPull",
"/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts",
"/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent",
"/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts"
],
[
"/Lotus/Upgrades/Calendar/EnergyWavesOnCombo",
"/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier",
"/Lotus/Upgrades/Calendar/MeleeAttackSpeed",
"/Lotus/Upgrades/Calendar/MeleeCritChance",
"/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit",
"/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy"
],
[
"/Lotus/Upgrades/Calendar/Armor",
"/Lotus/Upgrades/Calendar/CloneActiveCompanionForEnergySpent",
"/Lotus/Upgrades/Calendar/CompanionDamage",
"/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer",
"/Lotus/Upgrades/Calendar/CompanionsRadiationChance",
"/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage",
"/Lotus/Upgrades/Calendar/ReviveEnemyAsSpectreOnKill"
],
[
"/Lotus/Upgrades/Calendar/EnergyOrbsGrantShield",
"/Lotus/Upgrades/Calendar/EnergyRestoration",
"/Lotus/Upgrades/Calendar/ExplodingHealthOrbs",
"/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill",
"/Lotus/Upgrades/Calendar/HealingEffects",
"/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup"
],
[
"/Lotus/Upgrades/Calendar/BlastEveryXShots",
"/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary",
"/Lotus/Upgrades/Calendar/GuidingMissilesChance",
"/Lotus/Upgrades/Calendar/MagazineCapacity",
"/Lotus/Upgrades/Calendar/PunchToPrimary",
"/Lotus/Upgrades/Calendar/RefundBulletOnStatusProc",
"/Lotus/Upgrades/Calendar/StatusChancePerAmmoSpent"
]
];
for (let i = 0; i != upgradeRanges.length - 1; ++i) {
// Pick 3 unique hex members
const hexMembersPickedForThisDay: number[] = [];
for (let j = 0; j != 3; ++j) {
let hexMemberIndex: number;
do {
hexMemberIndex = rng.randomInt(0, upgradesByHexMember.length - 1);
} while (hexMembersPickedForThisDay.indexOf(hexMemberIndex) != -1);
hexMembersPickedForThisDay.push(hexMemberIndex);
}
hexMembersPickedForThisDay.sort(); // Always present them in the same order
// For each hex member, pick an upgrade that was not yet picked this season.
const events: ICalendarEvent[] = [];
for (const hexMemberIndex of hexMembersPickedForThisDay) {
const upgrades = upgradesByHexMember[hexMemberIndex];
const upgradeIndex = rng.randomInt(0, upgrades.length - 1);
events.push({ type: "CET_UPGRADE", upgrade: upgrades[upgradeIndex] });
upgrades.splice(upgradeIndex, 1);
}
logger.trace(`trying to fit upgrades between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`);
let day: number;
do {
day = rng.randomInt(upgradeRanges[i] + 1, upgradeRanges[i + 1] - 1);
} while (eventDays.find(x => x.day == day));
eventDays.push({ day, events });
}
eventDays.sort((a, b) => a.day - b.day);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
return {
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Days: eventDays,
Season: (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[seasonIndex],
YearIteration: Math.trunc(week / 4),
Version: 19,
UpgradeAvaliabilityRequirements: ["/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement"]
};
};
// Not very faithful, but to avoid the same node coming up back-to-back (which is not valid), I've split these into 2 arrays which we're alternating between.
const voidStormMissions = {
VoidT1: [
"CrewBattleNode519",
"CrewBattleNode518",
"CrewBattleNode515",
"CrewBattleNode503",
"CrewBattleNode509",
"CrewBattleNode522",
"CrewBattleNode511",
"CrewBattleNode512"
],
VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530", "CrewBattleNode535", "CrewBattleNode533"],
VoidT3: ["CrewBattleNode521", "CrewBattleNode516", "CrewBattleNode524", "CrewBattleNode525"],
VoidT4: [
"CrewBattleNode555",
"CrewBattleNode553",
"CrewBattleNode554",
"CrewBattleNode539",
"CrewBattleNode531",
"CrewBattleNode527",
"CrewBattleNode542",
"CrewBattleNode538",
"CrewBattleNode543",
"CrewBattleNode536",
"CrewBattleNode550",
"CrewBattleNode529"
]
} as const;
const voidStormLookbehind = {
VoidT1: 3,
VoidT2: 1,
VoidT3: 1,
VoidT4: 3
} as const;
const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
const expiry = activation + 90 * unixTimesInMs.minute;
let accum = 0;
const tierIdx = { VoidT1: hour * 2, VoidT2: hour, VoidT3: hour, VoidT4: hour * 2 };
for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) {
arr.push({
_id: {
$oid:
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"0321e89b" +
(accum++).toString().padStart(8, "0")
},
Node: sequentiallyUniqueRandomElement(
voidStormMissions[tier],
tierIdx[tier]++,
voidStormLookbehind[tier],
2051969264
)!,
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
ActiveMissionTier: tier
});
}
};
interface ITimeConstraint {
name: string;
isValidTime: (timeSecs: number) => boolean;
getIdealTimeBefore: (timeSecs: number) => number;
}
const eidolonDayConstraint: ITimeConstraint = {
name: "eidolon day",
isValidTime: (timeSecs: number): boolean => {
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / eidolonCycleDuration);
const eidolonCycleStart = eidolonEpoch + eidolonCycle * eidolonCycleDuration;
const hour = 24 * (((timeSecs - eidolonEpoch) % eidolonCycleDuration) / eidolonCycleDuration);
// const isDay = hour > 5 && hour < 21.9;
if (hour > 5) {
const eidolonCycleNightStart = eidolonCycleStart + 21.9 * eidolonHourDuration;
return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000);
}
return false;
},
getIdealTimeBefore: (timeSecs: number): number => {
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / eidolonCycleDuration);
const eidolonCycleStart = eidolonEpoch + eidolonCycle * eidolonCycleDuration;
const hour = 24 * (((timeSecs - eidolonEpoch) % eidolonCycleDuration) / eidolonCycleDuration);
if (hour > 5) {
return Math.trunc(eidolonCycleStart + 5 * eidolonHourDuration); // Today's morning
} else {
return Math.trunc(eidolonCycleStart - eidolonCycleDuration + 5 * eidolonHourDuration); // Yesterday's morning
}
}
};
const eidolonNightConstraint: ITimeConstraint = {
name: "eidolon night",
isValidTime: (timeSecs: number): boolean => {
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / eidolonCycleDuration);
const eidolonCycleStart = eidolonEpoch + eidolonCycle * eidolonCycleDuration;
const hour = 24 * (((timeSecs - eidolonEpoch) % eidolonCycleDuration) / eidolonCycleDuration);
if (hour > 5) {
const eidolonCycleNightStart = eidolonCycleStart + 21.9 * eidolonHourDuration;
return timeSecs >= eidolonCycleNightStart;
} else {
const eidolonCycleDayStart = eidolonCycleStart + 5 * eidolonHourDuration;
return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleDayStart * 1000);
}
},
getIdealTimeBefore: (timeSecs: number): number => {
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / eidolonCycleDuration);
const eidolonCycleStart = eidolonEpoch + eidolonCycle * eidolonCycleDuration;
const eidolonCycleNightStart = eidolonCycleStart + 21.9 * eidolonHourDuration;
if (eidolonCycleNightStart > timeSecs) {
// Night hasn't started yet, but we need to return a time in the past.
return Math.trunc(eidolonCycleNightStart - eidolonCycleDuration);
}
return Math.trunc(eidolonCycleNightStart);
}
};
const venusColdConstraint: ITimeConstraint = {
name: "venus cold",
isValidTime: (timeSecs: number): boolean => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
const vallisCycleEnd = vallisCycleStart + 1600;
const vallisCycleColdStart = vallisCycleStart + 400;
return (
timeSecs >= vallisCycleColdStart &&
!isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000)
);
},
getIdealTimeBefore: (timeSecs: number): number => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
const vallisCycleColdStart = vallisCycleStart + 400;
if (vallisCycleColdStart > timeSecs) {
// Cold hasn't started yet, but we need to return a time in the past.
return vallisCycleColdStart - 1600;
}
return vallisCycleColdStart;
}
};
const venusWarmConstraint: ITimeConstraint = {
name: "venus warm",
isValidTime: (timeSecs: number): boolean => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
const vallisCycleColdStart = vallisCycleStart + 400;
return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000);
},
getIdealTimeBefore: (timeSecs: number): number => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
return vallisCycleStart;
}
};
const getIdealTimeSatsifyingConstraints = (constraints: ITimeConstraint[]): number => {
let timeSecs = Math.trunc(Date.now() / 1000);
let allGood;
do {
allGood = true;
for (const constraint of constraints) {
if (!constraint.isValidTime(timeSecs)) {
logger.trace(`${constraint.name} is not happy with ${timeSecs}`);
const prevTimeSecs = timeSecs;
const suggestion = constraint.getIdealTimeBefore(timeSecs);
timeSecs = suggestion;
do {
timeSecs += 60;
if (timeSecs >= prevTimeSecs || !constraint.isValidTime(timeSecs)) {
timeSecs = suggestion; // Can't find a compromise; just take the suggestion and try to compromise on another constraint.
break;
}
} while (!constraints.every(constraint => constraint.isValidTime(timeSecs)));
allGood = false;
break;
}
}
} while (!allGood);
return timeSecs;
};
const fullyStockBaro = (vt: IVoidTrader, buildVersion: number): void => {
const tempManifest: IBaroDataTradeOffer[] = [];
for (const item of baro.evilBaro) {
tempManifest.push(item);
}
for (const armorSet of baro.armorSets) {
if (armorSet.bundle) {
tempManifest.push(armorSet.bundle);
}
tempManifest.push(...armorSet.items);
}
for (const item of baro.rest) {
tempManifest.push(item);
}
for (const item of baro.evergreen) {
tempManifest.push(item);
}
vt.Manifest.push(
...tempManifest
.filter(({ minBuildVersionInt }) => buildVersion >= minBuildVersionInt)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ minBuildVersionInt, ...offer }) => offer)
);
};
const getVarziaRotation = (week: number, buildVersion: number): string => {
const seed = new SRng(week).randomInt(0, 100_000);
const rng = new SRng(seed);
const [itemType, rotation] = rng.randomElement(Object.entries(varzia.primeDualPacks))!;
if (buildVersion < rotation.minBuildVersionInt) {
return itemType;
} else {
return "/Lotus/StoreItems/Types/StoreItems/Packages/MegaPrimeVault/LastChanceItemC";
}
};
const getVarziaManifest = (dualPack: string, buildVersion: number): IPrimeVaultTraderOffer[] => {
const rotationManifest = varzia.primeDualPacks[dualPack];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!rotationManifest || buildVersion < rotationManifest.minBuildVersionInt) {
return [];
}
const mainPack = [{ ItemType: dualPack, PrimePrice: 10 }];
const singlePacks: IPrimeVaultTraderOffer[] = [];
const vanguardRelics: IPrimeVaultTraderOffer[] = [];
const items: IPrimeVaultTraderOffer[] = [];
const bobbleHeads: IPrimeVaultTraderOffer[] = [];
if (config.worldState?.vanguardVaultRelics && buildVersion >= gameToBuildVersionInt["41.0.0"]) {
vanguardRelics.push(...varzia.vanguardVaultRelics);
}
for (const singlePackType of rotationManifest.SinglePacks) {
singlePacks.push({ ItemType: singlePackType, PrimePrice: 6 });
const sp = varzia.primeSinglePacks[singlePackType];
items.push(...sp.Items);
sp.BobbleHeads.forEach(bobbleHead => {
bobbleHeads.push({ ItemType: bobbleHead, PrimePrice: 1 });
});
}
const relics = rotationManifest.Relics.map(relic => ({ ItemType: relic, RegularPrice: 1 }));
return [singlePacks[0], ...mainPack, singlePacks[1], ...vanguardRelics, ...items, ...bobbleHeads, ...relics];
};
const getAllVarziaManifests = (buildVersion: number): IPrimeVaultTraderOffer[] => {
const dualPacks: IPrimeVaultTraderOffer[] = [];
const singlePacks: IPrimeVaultTraderOffer[] = [];
const vanguardRelics: IPrimeVaultTraderOffer[] = [];
const items: IPrimeVaultTraderOffer[] = [];
const bobbleHeads: IPrimeVaultTraderOffer[] = [];
const relics: IPrimeVaultTraderOffer[] = [];
const singlePackSet = new Set<string>();
const itemsSet = new Set<string>();
const bobbleHeadsSet = new Set<string>();
if (config.worldState?.vanguardVaultRelics && buildVersion >= gameToBuildVersionInt["41.0.0"]) {
vanguardRelics.push(...varzia.vanguardVaultRelics);
}
Object.entries(varzia.primeDualPacks)
.filter(([_, dualPack]) => buildVersion >= dualPack.minBuildVersionInt)
.forEach(([dualPackItemType, dualPack]) => {
dualPacks.push({ ItemType: dualPackItemType, PrimePrice: 10 });
dualPack.SinglePacks.forEach(singlePackKey => {
if (!singlePackSet.has(singlePackKey)) {
singlePackSet.add(singlePackKey);
singlePacks.push({ ItemType: singlePackKey, PrimePrice: 6 });
}
const sp = varzia.primeSinglePacks[singlePackKey];
sp.Items.forEach(item => {
if (!itemsSet.has(item.ItemType)) {
itemsSet.add(item.ItemType);
items.push(item);
}
});
sp.BobbleHeads.forEach(bobbleHead => {
if (!bobbleHeadsSet.has(bobbleHead)) {
bobbleHeadsSet.add(bobbleHead);
bobbleHeads.push({ ItemType: bobbleHead, PrimePrice: 1 });
}
});
});
relics.push(...dualPack.Relics.map(relic => ({ ItemType: relic, RegularPrice: 1 })));
});
return [...dualPacks, ...vanguardRelics, ...singlePacks, ...items, ...bobbleHeads, ...relics];
};
const createInvasion = (day: number, idx: number, buildVersion: number): IInvasion => {
const id = day * 3 + idx;
const defender = (["FC_GRINEER", "FC_CORPUS", day % 2 ? "FC_GRINEER" : "FC_CORPUS"] as const)[idx];
const rng = new SRng(new SRng(id).randomInt(0, 1_000_000));
const isInfestationOutbreak = rng.randomInt(0, 1) == 0;
const attacker = isInfestationOutbreak ? "FC_INFESTATION" : defender == "FC_GRINEER" ? "FC_CORPUS" : "FC_GRINEER";
const startMs = EPOCH + day * 86400_000;
const oid =
((startMs / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"fd148cb8" +
(idx & 0xffffffff).toString(16).padStart(8, "0");
const node = sequentiallyUniqueRandomElement(invasionNodes[defender], id, 5, 690175)!; // Can't repeat the other 2 on this day nor the last 3
const progress = (Date.now() - startMs) / 86400_000;
const countMultiplier = isInfestationOutbreak || rng.randomInt(0, 1) ? -1 : 1; // if defender is winning, count is negative
const fiftyPercent = rng.randomInt(1000, 29000); // introduce some 'yitter' for the percentages
const rewardFloat = rng.randomFloat();
const rewardTier = rewardFloat < 0.201 ? "RARE" : rewardFloat < 0.7788 ? "COMMON" : "UNCOMMON";
const attackerReward: IMissionReward = {};
const defenderReward: IMissionReward = {};
if (isInfestationOutbreak) {
defenderReward.countedItems = [
rng.randomElement(invasionRewards[rng.randomInt(0, 1) ? "FC_INFESTATION" : defender][rewardTier])!
];
} else {
attackerReward.countedItems = [rng.randomElement(invasionRewards[attacker][rewardTier])!];
defenderReward.countedItems = [rng.randomElement(invasionRewards[defender][rewardTier])!];
}
return {
_id: toOid2(oid, buildVersion),
Faction: attacker,
DefenderFaction: defender,
Node: node,
Count: Math.round(
(progress < 0.5 ? progress * 2 * fiftyPercent : fiftyPercent + (30_000 - fiftyPercent) * (progress - 0.5)) *
countMultiplier
),
Goal: 30000, // Value seems to range from 30000 to 98000 in intervals of 1000. Higher values are increasingly rare. I don't think this is relevant for the frontend besides dividing count by it.
LocTag: isInfestationOutbreak
? ExportRegions[node].missionType == "MT_ASSASSINATION"
? "/Lotus/Language/Menu/InfestedInvasionBoss"
: "/Lotus/Language/Menu/InfestedInvasionGeneric"
: attacker == "FC_CORPUS"
? "/Lotus/Language/Menu/CorpusInvasionGeneric"
: "/Lotus/Language/Menu/GrineerInvasionGeneric",
Completed: startMs + 86400_000 < Date.now(), // Sorta unfaithful. Invasions on live are (at least in part) in fluenced by people completing them. And otherwise also probably not hardcoded to last 24 hours.
ChainID: { $oid: oid },
AttackerReward: attackerReward,
AttackerMissionInfo: {
seed: rng.randomInt(0, 1_000_000),
faction: defender
},
DefenderReward: defenderReward,
DefenderMissionInfo: {
seed: rng.randomInt(0, 1_000_000),
faction: attacker
},
Activation: toMongoDate2(startMs, buildVersion)
};
};
export const getInvasionByOid = (oid: string): IInvasion | undefined => {
const arr = oid.split("fd148cb8");
if (arr.length == 2 && arr[0].length == 8 && arr[1].length == 8) {
return createInvasion(idToDay(oid), parseInt(arr[1], 16), BV_LATEST);
}
return undefined;
};
export const getWorldState = (
buildLabel: string = BL_LATEST,
convertGoals: boolean = true,
changeLegacyTags: boolean = config.unfaithfulBugFixes?.useAnniversaryTagForOldGoals || false
): IWorldState => {
const buildVersion = buildVersionToInt(buildLabel);
const constraints: ITimeConstraint[] = [];
if (config.worldState?.eidolonOverride) {
constraints.push(config.worldState.eidolonOverride == "day" ? eidolonDayConstraint : eidolonNightConstraint);
}
if (config.worldState?.vallisOverride) {
constraints.push(config.worldState.vallisOverride == "cold" ? venusColdConstraint : venusWarmConstraint);
}
if (config.worldState?.duviriOverride) {
const duviriMoods = ["sorrow", "fear", "joy", "anger", "envy"];
const desiredMood = duviriMoods.indexOf(config.worldState.duviriOverride);
if (desiredMood == -1) {
logger.warn(`ignoring invalid config value for worldState.duviriOverride`, {
value: config.worldState.duviriOverride,
valid_values: duviriMoods
});
} else {
constraints.push({
name: `duviri ${config.worldState.duviriOverride}`,
isValidTime: (timeSecs: number): boolean => {
const moodIndex = Math.trunc(timeSecs / 7200);
return moodIndex % 5 == desiredMood;
},
getIdealTimeBefore: (timeSecs: number): number => {
let moodIndex = Math.trunc(timeSecs / 7200);
moodIndex -= ((moodIndex % 5) - desiredMood + 5) % 5; // while (moodIndex % 5 != desiredMood) --moodIndex;
const moodStart = moodIndex * 7200;
return moodStart;
}
});
}
}
const timeSecs = getIdealTimeSatsifyingConstraints(constraints);
if (constraints.length != 0) {
const delta = Math.trunc(Date.now() / 1000) - timeSecs;
if (delta > 1) {
logger.debug(
`reported time is ${delta} seconds behind real time to satisfy selected constraints (${constraints.map(x => x.name).join(", ")})`
);
}
}
const timeMs = timeSecs * 1000;
const day = Math.trunc((timeMs - EPOCH) / 86400000);
const week = Math.trunc(day / 7);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const date = new Date(timeMs);
const defenseWavesPerRotation = buildVersion < gameToBuildVersionInt["38.5.0"] ? 5 : 3;
const worldState: IWorldState = {
Version: 10,
BuildLabel: buildLabel,
Time: timeSecs,
Goals: [],
Events: [],
Alerts: [],
Sorties: [],
LiteSorties: [],
ActiveMissions: [],
FlashSales: [],
GlobalUpgrades: [],
Invasions: [],
VoidTraders: [],
PrimeVaultTraders: [],
VoidStorms: [],
DailyDeals: [],
//EndlessXpChoices: [],
//EndlessXpSchedule: [],
KnownCalendarSeasons: [],
PVPChallengeInstances: [],
FeaturedGuilds: [],
NodeOverrides: [],
LibraryInfo: { LastCompletedTargetType: "/Lotus/Types/Game/Library/Targets/Research7Target" },
PrimeVaultAvailabilities: [false, false, false, false, false],
PrimeTokenAvailability: true,
PrimeAccessAvailability: { State: "PRIME1" },
PersistentEnemies: [],
PVPAlternativeModes: [],
PVPActiveTournaments: [],
ConstructionProjects: [],
ProjectPct: [0, 0, 0],
TwitchPromos: [],
ForceLogoutVersion: 0,
ExperimentRecommended: [],
SyndicateMissions: [],
InGameMarket: {
LandingPage: {
Categories: [
{
CategoryName: "NEW_PLAYER",
Name: "/Lotus/Language/Store/NewPlayerCategoryTitle",
Icon: "newplayer",
AddToMenu: true,
Items: [
"/Lotus/Types/StoreItems/Packages/2024Bundles/WeaponStarterPack",
"/Lotus/StoreItems/Powersuits/MonkeyKing/MonkeyKing",
"/Lotus/StoreItems/Weapons/Tenno/Melee/SwordsAndBoards/MeleeContestWinnerOne/TennoSwordShield",
"/Lotus/StoreItems/Upgrades/Skins/Effects/WerewolfEphemera",
"/Lotus/StoreItems/Types/StoreItems/SlotItems/TwoWeaponSlotItem",
"/Lotus/StoreItems/Powersuits/Wisp/Wisp",
"/Lotus/StoreItems/Weapons/Tenno/Shotgun/Shotgun",
"/Lotus/StoreItems/Powersuits/Rhino/Rhino",
"/Lotus/StoreItems/Weapons/Corpus/Pistols/CrpAirPistol/CrpAirPistolArray",
"/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem"
]
},
{
CategoryName: "NEW",
Name: "/Lotus/Language/Menu/Store_New",
Icon: "new",
Items: []
},
{
CategoryName: "POPULAR",
Name: "/Lotus/Language/Menu/StorePopular",
Icon: "popular",
AddToMenu: true,
Items: [
"/Lotus/Types/StoreItems/Packages/2025Bundles/TC2025DigitalPack",
"/Lotus/Types/StoreItems/Packages/2025Bundles/EncoreCompSupPack",
"/Lotus/Types/StoreItems/Packages/2025Bundles/EncoreGeminiSupPack",
"/Lotus/Types/StoreItems/Packages/WarframeBundles/TempleItemsBundle",
"/Lotus/Types/StoreItems/Packages/FormaPack",
"/Lotus/StoreItems/Upgrades/Skins/Saryn/WF1999SarynSkin",
"/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DaxDuviriKatana/DaxDuviriKatanaWeapon",
"/Lotus/StoreItems/Upgrades/Skins/Jade/WF1999NyxSkin",
"/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/NinjaColourPickerItem",
"/Lotus/StoreItems/Upgrades/Skins/Mag/WF1999MagSkin",
"/Lotus/StoreItems/Upgrades/Skins/Frost/WF1999FrostSkin",
"/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DaxDuviriTwoHandedKatana/DaxDuviriTwoHandedKatanaWeapon",
"/Lotus/StoreItems/Upgrades/Skins/Harlequin/MirageDeluxeSkin",
"/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/DaxDuviriHammer/DaxDuviriHammerWeapon"
]
},
{
CategoryName: "SEASONAL",
Name: "/Lotus/Language/Store/SeasonalCategoryTitle",
Icon: "seasonal",
AddToMenu: true,
Items: []
},
{
CategoryName: "COMMUNITY",
Name: "/Lotus/Language/Store/CommunityCategoryTitle",
Icon: "community",
AddToMenu: true,
Items: []
},
{
CategoryName: "HEIRLOOM",
Name: "/Lotus/Language/Store/HeirloomCategoryTitle",
Icon: "heirloom",
AddToMenu: true,
Items: [
"/Lotus/StoreItems/Upgrades/Skins/Berserker/ValkyrHeirloomSkin",
"/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomValkyrCrown",
"/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerValkyrHeirloom",
"/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardValkyrHeirloom",
"/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomValkyrGlyph",
"/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomValkyrSigil",
"/Lotus/Types/StoreItems/Packages/HeirloomPackRhino",
"/Lotus/StoreItems/Upgrades/Skins/Rhino/RhinoHeirloomSkin",
"/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomRhinoCrown",
"/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerRhinoHeirloom",
"/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardRhinoHeirloom",
"/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomRhinoGlyph",
"/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomRhinoSigil",
"/Lotus/Types/StoreItems/Packages/HeirloomPackEmber",
"/Lotus/StoreItems/Upgrades/Skins/Ember/EmberHeirloomSkin",
"/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomEmberCrown",
"/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerEmberHeirloom",
"/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardEmberHeirloom",
"/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomEmberGlyph",
"/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomEmberSigil"
]
},
{
CategoryName: "TENNOGEN",
Name: "/Lotus/Language/Menu/Store_Tennogen",
Icon: "tennogen",
AddToMenu: true,
Items: [
"/Lotus/StoreItems/Upgrades/Skins/Armor/SWEndocitosShoulderArmor/SWEndocitosShoulderArmorA",
"/Lotus/StoreItems/Upgrades/Skins/Scarves/SWLunariusSyandana",
"/Lotus/StoreItems/Upgrades/Skins/Scarves/SWRauSyandana",
"/Lotus/StoreItems/Upgrades/Skins/Hoplite/SWStyanaxHuzarrSkin",
"/Lotus/StoreItems/Upgrades/Skins/Werewolf/VorunaDemionnaSkin"
]
},
{
CategoryName: "SALE",
Name: "/Lotus/Language/Menu/Store_Sale",
Icon: "sale",
AddToMenu: true,
Items: []
},
{
CategoryName: "WISH_LIST",
Name: "/Lotus/Language/Menu/Store_Wishlist",
Icon: "wishlist",
Items: []
},
{
CategoryName: "QUICK_BUY",
Name: "/Lotus/Language/Store/TopSeller_Title",
Icon: "quickbuy",
Items: [
"/Lotus/Types/StoreItems/Packages/FormaPack",
"/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst",
"/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker"
]
}
]
}
}
};
worldState.Events.push({
Msg: "Join the OpenWF Discord!",
Messages: [
{ LanguageCode: "fr", Message: "Rejoignez le Discord OpenWF!" },
{ LanguageCode: "it", Message: "Unisciti al Discord di OpenWF!" },
{ LanguageCode: "de", Message: "Trete dem OpenWF Discord bei!" },
{ LanguageCode: "es", Message: "Únete al Discord de OpenWF!" },
{ LanguageCode: "pt", Message: "Junte-se ao Discord do OpenWF!" },
{ LanguageCode: "ru", Message: "Присоединяйтесь к OpenWF Discord!" },
{ LanguageCode: "pl", Message: "Dołącz do Discord OpenWF!" },
{ LanguageCode: "uk", Message: "Приєднуйтесь до OpenWF Discord!" },
{ LanguageCode: "tr", Message: "OpenWF Discord'a katıl!" },
{ LanguageCode: "ja", Message: "OpenWFのDiscordに参加しよう!" },
{ LanguageCode: "zh", Message: "加入OpenWF Discord!" },
{ LanguageCode: "ko", Message: "OpenWF Discord에 가입하세요!" },
{ LanguageCode: "tc", Message: "加入OpenWF Discord!" }
],
Prop: "https://discord.gg/PNNZ3asUuY",
Icon: "/Lotus/Interface/Icons/DiscordIconNoBacker.png"
});
if (config.worldState?.tennoLiveRelay) {
worldState.Goals.push({
_id: toOid2("687bf9400000000000000000", buildVersion),
Activation: toMongoDate2(1752955200000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Locations/RelayStationTennoConB",
ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDescB",
Icon: "/Lotus/Interface/Icons/Categories/IconTennoLive.png",
Tag: "TennoConRelayB",
Node: "TennoConBHUB6"
});
}
if (config.worldState?.baroTennoConRelay) {
worldState.Goals.push({
_id: toOid2("687bb2f00000000000000000", buildVersion),
Activation: toMongoDate2(1752937200000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
//"Faction": "FC_GRINEER",
Desc: "/Lotus/Language/Locations/RelayStationTennoCon",
ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDesc",
Icon:
buildVersion >= gameToBuildVersionInt["43.0.0"]
? "/Lotus/Interface/Icons/Categories/IconTennoConBaroVip.png"
: "/Lotus/Interface/Icons/Categories/IconTennoConSigil.png",
Tag: "TennoConRelay",
Node: "TennoConHUB2"
});
const vt: IVoidTrader = {
_id: toOid2("687809030379266d790495c6", buildVersion),
Activation: toMongoDate2(1752937200000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Character: "Baro'Ki Teel",
Node: "TennoConHUB2",
Manifest: []
};
worldState.VoidTraders.push(vt);
fullyStockBaro(vt, buildVersion);
}
if (config.worldState) {
for (const [key, alert] of Object.entries(configAlerts)) {
if (config.worldState[key as keyof typeof config.worldState]) {
const { minBuildVersion, ...wsAlert } = alert;
if (!minBuildVersion || buildVersion >= minBuildVersion) {
if (wsAlert.MissionInfo.missionType == "MT_DEFENSE") {
wsAlert.MissionInfo.maxWaveNum =
defenseWavesPerRotation * (wsAlert.MissionInfo.maxRotations ?? 1);
wsAlert.MissionInfo.maxRotations = undefined;
}
worldState.Alerts.push(wsAlert);
}
}
}
}
if (config.worldState?.qtccAlerts) {
const activation = 1759327200000;
const expiry = 2000000000000;
worldState.Alerts.push(
{
_id: toOid2("68dc23c42e9d3acfa708ff3b", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
MissionInfo: {
location: "SolNode123",
missionType: "MT_SURVIVAL",
faction: "FC_CORPUS",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/Plushy2021QTCC"]
},
levelOverride: "/Lotus/Levels/Proc/Corpus/CorpusShipSurvivalRaid",
enemySpec: "/Lotus/Types/Game/EnemySpecs/CorpusShipEnemySpecs/CorpusShipSurvivalA",
minEnemyLevel: 20,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/TennoUnitedAlert",
maxWaveNum: 5
},
Tag: "LotusGift",
ForceUnlock: true
},
{
_id: toOid2("68dc2466e298b4f04206687a", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
MissionInfo: {
location: "SolNode149",
missionType: "MT_DEFENSE",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/Plushy2022QTCC"]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerShipyardsDefense",
enemySpec: "/Lotus/Types/Game/EnemySpecs/GrineerShipyardsDefenseA",
minEnemyLevel: 20,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/TennoUnitedAlert",
maxWaveNum: defenseWavesPerRotation * 1
},
Tag: "LotusGift",
ForceUnlock: true
},
{
_id: toOid2("68dc26865e7cb56b820b4252", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
MissionInfo: {
location: "SolNode39",
missionType: "MT_EXCAVATE",
faction: "FC_GRINEER",
difficulty: 1,
missionReward: {
credits: 10000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyVirminkQTCC"]
},
levelOverride: "/Lotus/Levels/Proc/Grineer/GrineerForestExcavation",
enemySpec: "/Lotus/Types/Game/EnemySpecs/ForestGrineerExcavationA",
minEnemyLevel: 20,
maxEnemyLevel: 30,
descText: "/Lotus/Language/Alerts/TennoUnitedAlert",
maxWaveNum: 5
},
Tag: "LotusGift",
ForceUnlock: true
}
);
pushFlashSales(worldState, qqtcFlashSales, activation, expiry, "COMMUNITY", buildVersion);
}
const isFebruary = date.getUTCMonth() == 1;
if ((config.worldState?.starDaysOverride ?? isFebruary) && buildVersion >= gameToBuildVersionInt["29.10.0"]) {
worldState.Goals.push({
_id: toOid2("67a4dcce2a198564d62e1647", buildVersion),
Activation: {
$date: {
$numberLong: config.worldState?.starDaysOverride
? "1738868400000"
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1).toString()
}
},
Expiry: {
$date: {
$numberLong: config.worldState?.starDaysOverride
? "2000000000000"
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1).toString()
}
},
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Events/ValentinesFortunaName",
ToolTip: "/Lotus/Language/Events/ValentinesFortunaName",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/ValentinesEventIcon.png",
Tag: "FortunaValentines",
Node: "SolarisUnitedHub1"
});
}
if (buildVersion >= gameToBuildVersionInt["38.6.0"]) {
// The client gets kinda confused when multiple goals have the same tag, so considering these mutually exclusive.
if (config.worldState?.galleonOfGhouls == 1) {
worldState.Goals.push({
_id: toOid2("6814ddf00000000000000000", buildVersion),
Activation: toMongoDate2(1746198000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode19",
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlert",
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
Tag: "GalleonRobbery",
Reward: {
items: [
"/Lotus/StoreItems/Types/Recipes/Weapons/GrnChainSawTonfaBlueprint",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
} else if (config.worldState?.galleonOfGhouls == 2) {
worldState.Goals.push({
_id: toOid2("681e18700000000000000000", buildVersion),
Activation: toMongoDate2(1746802800000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode28", // Incompatible with Wolf Hunt, Orphix Venom, Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertB",
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
Tag: "GalleonRobbery",
Reward: {
items: [
"/Lotus/StoreItems/Types/Recipes/Weapons/MortiforShieldAndSwordBlueprint",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
} else if (config.worldState?.galleonOfGhouls == 3) {
worldState.Goals.push({
_id: toOid2("682752f00000000000000000", buildVersion),
Activation: toMongoDate2(1747407600000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode19",
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertC",
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
Tag: "GalleonRobbery",
Reward: {
items: [
"/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
}
}
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) &&
buildVersion >= gameToBuildVersionInt["22.7.0"]
) {
worldState.Goals.push({
_id: toOid2("654a5058c757487cdb11824f", buildVersion),
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"
});
}
const firstAugustWeekday = new Date(Date.UTC(date.getUTCFullYear(), 7, 1)).getUTCDay();
const firstAugustWednesdayOffset = (3 - firstAugustWeekday + 7) % 7;
const dogDaysStart = 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 dogDaysEnd = Date.UTC(date.getUTCFullYear(), 8, 1 + firstSeptemberWednesdayOffset, 15);
const isDogDaysActive = timeMs >= dogDaysStart && timeMs < dogDaysEnd;
if ((config.worldState?.dogDaysOverride ?? isDogDaysActive) && buildVersion >= gameToBuildVersionInt["25.7.0"]) {
const activation = config.worldState?.dogDaysOverride ? 1699372800000 : dogDaysStart;
const expiry = config.worldState?.dogDaysOverride ? 2000000000000 : dogDaysEnd;
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"], // 2023.07.26.16.38
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 25
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyBeachKavat"], // 2024.02.16.17.13
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 50
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyRucksackKubrow"], // 2024.02.16.17.13
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 75
}
]
},
{
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneBeachcomber"], // 2023.07.26.16.38
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 100
}
]
}
],
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageDogDays2024Glyph"], // 2024.07.17.17.07
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 25
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/DogDays2024Poster"], // 2024.07.17.17.07
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 50
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/DogDaysKubrowBadgeItem"], // 2024.07.17.17.07
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 75
}
]
},
{
items: ["/Lotus/StoreItems/Types/Items/ShipDecos/DogDays2024LisetPropCleaningDroneBeachcomber"], // 2024.07.17.17.07
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 100
}
]
}
],
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysHydroidGlyph"], // 2025.06.23.11.39
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 25
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysLokiGlyph"], // 2025.06.23.11.39
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 50
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysNovaGlyph"], // 2025.06.23.11.39
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 75
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysValkyrGlyph"], // 2025.06.23.11.39
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 100
}
]
}
],
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysKelaGlyph"], // 2026.07.09.15.36
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 25
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysRusalkaGlyph"], // 2026.07.09.15.36
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 50
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysErraGlyph"], // 2026.07.09.15.36
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 75
}
]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysGareshGlyph"], // 2026.07.09.15.36
countedItems: [
{
ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks",
ItemCount: 100
}
]
}
]
];
const fallbackYear =
buildVersion >= gameToBuildVersionInt["43.0.0"]
? 4
: buildVersion >= gameToBuildVersionInt["39.0.0"]
? 3
: buildVersion >= gameToBuildVersionInt["36.1.2"]
? 2
: buildVersion >= gameToBuildVersionInt["33.6.0"]
? 1
: 0;
const year = config.worldState?.dogDaysRewardsOverride ?? fallbackYear;
worldState.Goals.push({
_id: {
$oid:
((dogDaysStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"c57487c3768936d" +
year.toString(16)
},
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
Count: 0,
Goal: 100,
InterimGoals: [25, 50],
BonusGoal: 200,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode25", // Incompatible with Hallowed Flame, Hallowed Nightmares, Warframe Anniversary
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/TacAlertKeyWaterFightB",
"/Lotus/Types/Keys/TacAlertKeyWaterFightC",
"/Lotus/Types/Keys/TacAlertKeyWaterFightD"
],
ConcurrentNodeReqs: [25, 50, 100],
ConcurrentNodes: ["EventNode24", "EventNode34", "EventNode35"], // Incompatible with Hallowed Flame, Hallowed Nightmares, Warframe Anniversary
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"
});
pushFlashSales(worldState, dogDaysFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
// there no strict start day so lets pick first march monday
const firstMarchWeekday = new Date(Date.UTC(date.getUTCFullYear(), 2, 1)).getUTCDay();
const firstMarchMondayOffset = (8 - firstMarchWeekday) % 7;
const saintPatrickStart = Date.UTC(date.getUTCFullYear(), 2, firstMarchMondayOffset + 1, 16);
const saintPatrickEnd = Date.UTC(date.getUTCFullYear(), 2, firstMarchMondayOffset + 22, 16);
const isSaintPatrickActive = timeMs >= saintPatrickStart && timeMs < saintPatrickEnd;
if (
(config.worldState?.saintPatrickOverride ?? isSaintPatrickActive) &&
buildVersion >= gameToBuildVersionInt["13.0.0"]
) {
const activation = config.worldState?.saintPatrickOverride ? 1772467200000 : saintPatrickStart;
const expiry = config.worldState?.saintPatrickOverride ? 2000000000000 : saintPatrickEnd;
pushFlashSales(worldState, saintPatrickDayFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
// Pride Month - June
const isJune = date.getUTCMonth() == 5;
if ((config.worldState?.prideOverride ?? isJune) && buildVersion >= gameToBuildVersionInt["29.5.0"]) {
const activation = config.worldState?.prideOverride ? 1772467200000 : Date.UTC(date.getUTCFullYear(), 5, 1);
const expiry = config.worldState?.prideOverride ? 2000000000000 : Date.UTC(date.getUTCFullYear(), 6, 1);
pushFlashSales(worldState, prideMonthFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
const xmasStart = Date.UTC(date.getUTCFullYear(), 11, 1);
const xmasEnd = Date.UTC(date.getUTCFullYear() + 1, 1, 1);
const isXmas = timeMs >= xmasStart && timeMs < xmasEnd;
if ((config.worldState?.xmasOverride ?? isXmas) && buildVersion >= gameToBuildVersionInt["13.0.0"]) {
const activation = config.worldState?.xmasOverride ? 1772467200000 : xmasStart;
const expiry = config.worldState?.xmasOverride ? 2000000000000 : xmasEnd;
pushFlashSales(worldState, tennobaumFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
if (config.worldState?.anniversary != undefined) {
// Incompatible with: Use Tag from Warframe Anniversary for old Events, Wolf Hunt, Galleon Of Ghouls, Hallowed Flame, Hallowed Nightmares, Dog Days, Proxy Rebellion, Long Shadow
const goalsByWeek: Partial<IGoal>[][] = [
[
{
Node: "EventNode28",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2019E",
Tag: "Anniversary2019TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Excalibur/ExcaliburDexSkin"] }
},
{
Node: "EventNode26",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020F",
Tag: "Anniversary2020TacAlert",
Reward: { items: ["/Lotus/StoreItems/Types/Items/ShipDecos/ExcaliburDexBobbleHead"] }
},
{
Node: "EventNode19",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeA",
Tag: "Anniversary2024TacAlertCMA",
Reward: { items: ["/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker"] }
}
],
[
{
Node: "EventNode24",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017C",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Weapons/Tenno/LongGuns/DexTheThird/DexTheThird"] }
},
{
Node: "EventNode18",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020H",
Tag: "Anniversary2020TacAlert",
Reward: { items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageDexAnniversary"] }
}
],
[
{
Node: "EventNode18",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2022J",
Tag: "Anniversary2022TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Rhino/RhinoDexSkin"] }
},
{
Node: "EventNode38",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025D",
Tag: "Anniversary2020TacAlert",
Reward: { items: ["/Lotus/StoreItems/Types/Items/ShipDecos/RhinoDexBobbleHead"] }
},
{
Node: "EventNode27",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeA",
Tag: "Anniversary2024TacAlertCMA",
Reward: { items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"] }
}
],
[
{
Node: "EventNode2",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020G",
Tag: "Anniversary2020TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Liset/DexLisetSkin"] }
},
{
Node: "EventNode17",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017B",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DexTheSecond/DexTheSecond"] }
}
],
[
{
Node: "EventNode18",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017A",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Weapons/Tenno/Pistols/DexFuris/DexFuris"] }
},
{
Node: "EventNode26",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2023K",
Tag: "Anniversary2025TacAlert",
Reward: { items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageCommunityClemComic"] }
},
{
Node: "EventNode12",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeB",
Tag: "Anniversary2025TacAlertCMB",
Reward: { items: ["/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker"] }
}
],
[
{
Node: "EventNode17",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025A",
Tag: "Anniversary2025TacAlert",
Reward: {
items: [
"/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/KatanaAndWakizashi/Dex2023Nikana/Dex2023Nikana"
]
}
},
{
Node: "EventNode27",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2018D",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Scarves/DexScarf"] }
}
],
[
{
Node: "EventNode38",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025C",
Tag: "Anniversary2018TacAlert",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Wisp/DexWispSkin"] }
},
{
Node: "EventNode12",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024L",
Tag: "Anniversary2024TacAlert",
Reward: { items: ["/Lotus/Types/StoreItems/Packages/OperatorDrifterDexBundle"] }
},
{
Node: "EventNode26",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeB",
Tag: "Anniversary2024TacAlertCMB",
Reward: { items: ["/Lotus/StoreItems/Types/Recipes/Components/UmbraFormaBlueprint"] }
}
],
[
{
Node: "EventNode37",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2021I",
Tag: "Anniversary2021TacAlert",
Reward: {
items: [
"/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorAArmor",
"/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorCArmor",
"/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorLArmor",
"/Lotus/StoreItems/Types/Game/CatbrowPet/CatbrowGeneticSignature"
],
countedItems: [
{ ItemType: "/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", ItemCount: 10 }
]
}
},
{
Node: "EventNode9",
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025B",
Tag: "Anniversary2025TacAlert",
Reward: {
items: ["/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerAnniversaryEleven"]
}
}
]
];
goalsByWeek[config.worldState.anniversary].forEach((goal, i) => {
worldState.Goals.push({
_id: {
$oid:
"67c6d8e725b23feb" +
config.worldState?.anniversary!.toString(16).padStart(4, "0") +
i.toString(16).padStart(4, "0")
},
Activation: toMongoDate2(1745593200000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1,
Success: 0,
Personal: true,
ClampNodeScores: true,
Node: goal.Node,
MissionKeyName: goal.MissionKeyName,
Desc: goal.Tag!.endsWith("CMB")
? "/Lotus/Language/Events/Anniversary2024ChallengeMode"
: "/Lotus/Language/G1Quests/Anniversary2017MissionTitle",
Icon: "/Lotus/Interface/Icons/Player/GlyphLotus12Anniversary.png",
Tag: goal.Tag!,
Reward: goal.Reward
});
});
}
if (config.worldState?.wolfHunt != undefined && buildVersion >= gameToBuildVersionInt["25.0.0"]) {
if (config.worldState.wolfHunt == 0) {
worldState.Goals.push({
_id: toOid2("67ed7672798d6466172e3b9c", buildVersion),
Activation: { $date: { $numberLong: "1743616800000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
Count: 0,
Goal: 1,
BonusGoal: 2,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode29",
ConcurrentMissionKeyNames: ["/Lotus/Types/Keys/WolfTacAlertB"],
ConcurrentNodeReqs: [1],
ConcurrentNodes: ["EventNode28"], // Incompatible with Galleon Of Ghouls, Orphix Venom, Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/WolfTacAlertA",
Faction: "FC_GRINEER",
Desc: "/Lotus/Language/Alerts/WolfAlert",
Icon: "/Lotus/Interface/Icons/Npcs/Seasonal/WolfStalker.png",
Tag: "WolfHuntRedux", // unfaithful
Reward: {
countedItems: [{ ItemType: "/Lotus/Types/Items/MiscItems/Alertium", ItemCount: 10 }]
},
BonusReward: {
items: [
"/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
} else if (config.worldState.wolfHunt == 1) {
worldState.Goals.push({
_id: toOid2("67ed7672798d6466172e3b9d", buildVersion),
Activation: { $date: { $numberLong: "1743616800000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
Count: 0,
Goal: 3,
InterimGoals: [1, 2],
BonusGoal: 4,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode29",
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/WolfTacAlertReduxB",
"/Lotus/Types/Keys/WolfTacAlertReduxC",
"/Lotus/Types/Keys/WolfTacAlertReduxD"
],
ConcurrentNodeReqs: [1, 2, 3],
ConcurrentNodes: ["EventNode28", "EventNode39", "EventNode40"], // Incompatible with Galleon Of Ghouls, Orphix Venom, Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/WolfTacAlertReduxA",
Faction: "FC_GRINEER",
Desc: "/Lotus/Language/Alerts/WolfAlert",
Icon: "/Lotus/Interface/Icons/Npcs/Seasonal/WolfStalker.png",
Tag: "WolfHuntRedux",
InterimRewards: [
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerHandle"]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerHead"]
}
],
Reward: {
credits: 50000,
items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerMotor"]
},
BonusReward: {
credits: 50000,
items: [
"/Lotus/StoreItems/Types/Recipes/Weapons/ThrowingHammerBlueprint",
"/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
});
}
}
const tagsForOlderGoals: string[] = [
"Anniversary2018TacAlert",
"Anniversary2019TacAlert",
"Anniversary2020TacAlert",
"Anniversary2021TacAlert",
"Anniversary2022TacAlert",
"Anniversary2024TacAlert",
"Anniversary2024TacAlertCMA",
"Anniversary2025TacAlert",
"Anniversary2025TacAlertCMB"
];
if (config.worldState?.hallowedFlame && buildVersion >= gameToBuildVersionInt["26.0.0"]) {
worldState.Goals.push(
{
_id: toOid2("5db305403d34b5158873519a", buildVersion),
Activation: toMongoDate2(1699372800000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 3,
InterimGoals: [1, 2],
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode24", // Incompatible with Hallowed Nightmares, Dog Days
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/LanternEndlessEventKeyB",
"/Lotus/Types/Keys/LanternEndlessEventKeyC"
],
ConcurrentNodeReqs: [1, 2],
ConcurrentNodes: ["EventNode25", "EventNode34"], // Incompatible with Hallowed Nightmares, Dog Days
MissionKeyName: "/Lotus/Types/Keys/LanternEndlessEventKeyA",
Faction: "FC_INFESTATION",
Desc: "/Lotus/Language/Events/TacAlertHalloweenLantern",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
Tag: changeLegacyTags ? tagsForOlderGoals[0] : "Halloween19",
InterimRewards: [
{ items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"] },
{ items: ["/Lotus/StoreItems/Types/Items/MiscItems/Forma"] }
],
Reward: {
items: ["/Lotus/StoreItems/Types/Items/MiscItems/FormaAura"]
}
},
{
_id: toOid2("5db3054a3d34b5158873519c", buildVersion),
Activation: toMongoDate2(1699372800000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 900,
Success: 0,
Personal: true,
Bounty: true,
Best: true,
ClampNodeScores: true,
Node: "EventNode35",
MissionKeyName: "/Lotus/Types/Keys/LanternEndlessEventKeyD",
Faction: "FC_INFESTATION",
Desc: "/Lotus/Language/Events/TacAlertHalloweenLanternEndless",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
Tag: "Halloween19Endless",
PrereqGoalTags: [changeLegacyTags ? tagsForOlderGoals[0] : "Halloween19"],
Reward: {
items: [
"/Lotus/StoreItems/Upgrades/Skins/Effects/BatsEphemera",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
},
ScoreVar: "EndlessMissionTimeElapsed",
ScoreMaxTag: "Halloween19ScoreMax"
}
);
}
if (config.worldState?.hallowedNightmares && buildVersion >= gameToBuildVersionInt["18.0.2"]) {
const rewards = [
// 2018
[
{
items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/DotD2016Sigil"]
},
{
items: ["/Lotus/StoreItems/Upgrades/Skins/Halloween/HalloweenDread"]
},
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor"]
}
],
// 2016
[
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"]
},
{
items: [
"/Lotus/StoreItems/Upgrades/Skins/Sigils/DotD2016Sigil",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
},
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor"]
}
],
// 2015
[
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"]
},
{
items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"]
}
]
];
const year = config.worldState.hallowedNightmaresRewardsOverride ?? 0;
worldState.Goals.push({
_id: { $oid: "5bc98f00000000000000000" + year.toString(16) },
Activation: toMongoDate2(1539972000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
InterimGoals: [1],
Goal: 2,
Success: 0,
Personal: true,
Bounty: true,
Tag: changeLegacyTags ? tagsForOlderGoals[0] : "Halloween",
Faction: "FC_INFESTATION",
Desc: "/Lotus/Language/G1Quests/TacAlertHalloweenTitle",
ToolTip: "/Lotus/Language/G1Quests/TacAlertHalloweenToolTip",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
ClampNodeScores: true,
Node: "EventNode2", // Incompatible with Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyHalloween",
ConcurrentMissionKeyNames: ["/Lotus/Types/Keys/TacAlertKeyHalloweenBonus"],
ConcurrentNodeReqs: [1],
ConcurrentNodes: ["EventNode24"], // Incompatible with Hallowed Flame, Dog Days, Warframe Anniversary
InterimRewards: [rewards[year][0]],
Reward: rewards[year][1]
});
if (year != 2) {
worldState.Goals.push({
_id: { $oid: "5bc98f01000000000000000" + year.toString(16) },
Activation: toMongoDate2(1539972000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 666,
Success: 0,
Personal: true,
Bounty: true,
Best: true,
Tag: "Halloween",
PrereqGoalTags: [changeLegacyTags ? tagsForOlderGoals[0] : "Halloween"],
Faction: "FC_INFESTATION",
Desc: "Hallowed Nightmares - Time Attack",
ToolTip: "/Lotus/Language/G1Quests/TacAlertHalloweenToolTip",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
ClampNodeScores: true,
Node: "EventNode25", // Incompatible with Hallowed Flame, Dog Days
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyHalloweenTimeAttack",
ScoreVar: "TimeAttackScore",
ScoreMaxTag: "Halloween16",
Reward: rewards[year][2]
});
}
}
if (config.worldState?.proxyRebellion && buildVersion >= gameToBuildVersionInt["23.2.0"]) {
const rewards = [
// 2019
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod"]
},
{
credits: 50000,
items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"]
},
{
items: [
"/Lotus/StoreItems/Upgrades/Skins/Scarves/HornSkullScarf",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
],
// 2018
[
{
credits: 50000,
items: ["/Lotus/StoreItems/Upgrades/Mods/FusionBundles/NightwatchFusionBundle"]
},
{
credits: 50000,
items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"]
},
{
credits: 50000,
items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"]
},
{
items: [
"/Lotus/StoreItems/Upgrades/Skins/Sigils/EnergySigilA",
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
]
}
]
];
const year = config.worldState.proxyRebellionRewardsOverride ?? 0;
worldState.Goals.push({
_id: { $oid: "5b5b5da0000000000000000" + year.toString(16) },
Activation: toMongoDate2(1532714400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 3,
InterimGoals: [1, 2],
BonusGoal: 4,
Success: 0,
Personal: true,
Bounty: true,
ClampNodeScores: true,
Node: "EventNode18", // Incompatible with Warframe Anniversary
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionTwo",
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionThree",
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionFour"
],
ConcurrentNodeReqs: [1, 2, 3],
ConcurrentNodes: ["EventNode7", "EventNode4", "EventNode17"], // Incompatible with Orphix venom, Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyProxyRebellionOne",
Faction: "FC_CORPUS",
Desc: "/Lotus/Language/Alerts/TacAlertProxyRebellion",
Icon: "/Lotus/Materials/Emblems/BountyBadge_e.png",
Tag: changeLegacyTags ? tagsForOlderGoals[1] : "ProxyRebellion",
InterimRewards: rewards[year].slice(0, 2),
Reward: rewards[year][2],
BonusReward: rewards[year][3]
});
}
if (config.worldState?.longShadow && buildVersion >= gameToBuildVersionInt["18.22.1"]) {
worldState.Goals.push({
_id: toOid2("5bc9e8f7272d5d184c8398c9", buildVersion),
Activation: toMongoDate2(1539972000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
InterimGoals: [1, 2],
Goal: 3,
BonusGoal: 4,
Success: 0,
Personal: true,
Bounty: true,
Tag: changeLegacyTags ? tagsForOlderGoals[2] : "NightwatchTacAlert",
Faction: "FC_GRINEER",
Desc: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertTitle",
Icon: "/Lotus/Materials/Emblems/BountyBadge_e.png",
ClampNodeScores: true,
Node: "EventNode9", // Incompatible with Warframe Anniversary
MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchEasy",
ConcurrentMissionKeyNames: [
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatch",
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatchHard",
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatchBonus"
],
ConcurrentNodeReqs: [1, 2, 3],
ConcurrentNodes: ["SolNode136", "EventNode3", "EventNode0"],
InterimRewards: [
{
credits: 50000,
countedItems: [
{ ItemType: "/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle", ItemCount: 10 } // Not sure about that
]
},
{
items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"]
}
],
Reward: {
items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"]
},
BonusReward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"] }
});
}
const isOctober = date.getUTCMonth() == 9; // October = month index 9
if ((config.worldState?.naberusNightsOverride ?? isOctober) && buildVersion >= gameToBuildVersionInt["29.3.1"]) {
const activation = config.worldState?.naberusNightsOverride
? 1727881200000
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
const expiry = config.worldState?.naberusNightsOverride
? 2000000000000
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
worldState.Goals.push({
_id: toOid2("66fd602de1778d583419e8e7", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Events/HalloweenNaberusName",
ToolTip: "/Lotus/Language/Events/HalloweenNaberusDesc",
Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
Tag: "DeimosHalloween",
Node: "DeimosHub"
});
pushFlashSales(worldState, naberusNightsFlashSales, activation, expiry, "SEASONAL", buildVersion);
}
if (config.worldState?.bellyOfTheBeast && buildVersion >= gameToBuildVersionInt["36.0.0"]) {
worldState.Goals.push({
_id: toOid2("67a5035c2a198564d62e165e", buildVersion),
Activation: toMongoDate2(1738868400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
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"
});
pushGoalAlerts(worldState, "JadeShadows", buildVersion);
}
if (config.worldState?.operationAtramentum && buildVersion >= gameToBuildVersionInt["42.0.0"]) {
worldState.Goals.push({
_id: toOid2("69ce8b780000000000000000", buildVersion),
Activation: toMongoDate2(1738868400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: config.worldState.operationAtramentumProgressOverride ?? 0,
HealthPct: (config.worldState.operationAtramentumProgressOverride ?? 0) / 100,
Goal: 0,
Personal: true,
Community: true,
ClanGoal: [72, 216, 648, 1944, 5832],
Tag: "ShadowgrapherEvent",
Faction: "FC_TENNO",
Desc: "/Lotus/Language/Shadowgrapher/ShadowgrapherEventName",
ToolTip: "/Lotus/Language/Shadowgrapher/ShadowgrapherShortEventDesc",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/OperationAntramentumIcon.png",
ScoreLocTag: "/Lotus/Language/Shadowgrapher/ShadowgrapherEventScore",
Node: "SolNode239",
MissionKeyName: "/Lotus/Types/Keys/ShadowgrapherEventMission",
ItemType: "/Lotus/Types/Gameplay/Shadowgrapher/Resources/ShadowgrapherEventResource"
});
}
// <U39
if (config.worldState?.eightClaw && buildVersion >= gameToBuildVersionInt["39.0.0"]) {
worldState.Goals.push({
_id: toOid2("685c15f80000000000000000", buildVersion),
Activation: toMongoDate2(1750865400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
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"
});
}
if (config.worldState?.scarletSpear && buildVersion >= gameToBuildVersionInt["27.3.0"]) {
worldState.Goals.push({
_id: toOid2("5e7a3e2389e3090b0c6a998b", buildVersion),
Activation: { $date: { $numberLong: "1585070400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
Node: "ScenarioEventHub5",
Desc: "/Lotus/Language/G1Quests/FlotillaOperation",
Icon: "/Lotus/Interface/Graphics/ScarletSpear/ScarletSpearIcon.png",
Tag: "SquadLinkEvent",
ScoreVar: "ScenarioScore",
Personal: true,
Metadata:
'{"progressReq":100,"duration":180,"cooldown":10,"groundTiers":[1000,3000,5000],"spaceTiers":[1000,3000,5000]}',
CompletionBonus: [0, 2000, 6000, 10000],
InterimGoals: [10000, 30000],
InterimRewards: [
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/ScarletSpear/ScarletSpearOperationIEmblem"] },
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/ScarletSpear/ScarletSpearOperationIIEmblem"] }
],
Goal: 50000,
Reward: {
items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/ScarletSpear/ScarletSpearOperationIIIEmblem"]
},
AltExpiry: { $date: { $numberLong: "2000000000000" } },
AltActivation: { $date: { $numberLong: "2000000000000" } },
EpochNum: 114,
NextAltActivation: { $date: { $numberLong: "2000000000000" } },
NextAltExpiry: { $date: { $numberLong: "2000000000000" } },
PauseAutoScheduling: true
});
}
if (config.worldState?.orphixVenom && buildVersion >= gameToBuildVersionInt["29.6.8"]) {
worldState.Goals.push(
{
_id: toOid2("5fdcccb875d5ad500dc477d0", buildVersion),
Activation: toMongoDate2(1608320400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 500,
Success: 0,
Personal: true,
Best: true,
Node: "EventNode17", // Incompatible with Proxy Rebellion
MissionKeyName: "/Lotus/Types/Keys/MechSurvivalCorpusShip",
Faction: "FC_SENTIENT",
Desc: "/Lotus/Language/Events/MechEventMissionTier1",
Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png",
Tag: "MechSurvivalA",
ScoreVar: "MechSurvivalScore",
Reward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/MechEventEmblemItem"] }
},
{
_id: toOid2("5fdcccb875d5ad500dc477d1", buildVersion),
Activation: toMongoDate2(1608320400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 1000,
Success: 0,
Personal: true,
Best: true,
Node: "EventNode28", // Incompatible with Galleon Of Ghouls, Wolf Hunt
MissionKeyName: "/Lotus/Types/Keys/MechSurvivalGrineerGalleon",
Faction: "FC_SENTIENT",
Desc: "/Lotus/Language/Events/MechEventMissionTier2",
Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png",
Tag: "MechSurvivalB",
PrereqGoalTags: ["MechSurvivalA"],
ScoreVar: "MechSurvivalScore",
Reward: { items: ["/Lotus/StoreItems/Types/Items/FusionTreasures/OroFusexJ"] }
},
{
_id: toOid2("5fdcccb875d5ad500dc477d2", buildVersion),
Activation: toMongoDate2(1608320400000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Count: 0,
Goal: 2000,
Success: 0,
Personal: true,
Best: true,
Node: "EventNode32",
MissionKeyName: "/Lotus/Types/Keys/MechSurvivalGasCity",
MissionKeyRotation: [
"/Lotus/Types/Keys/MechSurvivalGasCity",
"/Lotus/Types/Keys/MechSurvivalCorpusShipEndurance",
"/Lotus/Types/Keys/MechSurvivalGrineerGalleonEndurance"
],
MissionKeyRotationInterval: 3600, // 1 hour
Faction: "FC_SENTIENT",
Desc: "/Lotus/Language/Events/MechEventMissionTier3",
Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png",
Tag: "MechSurvival",
PrereqGoalTags: ["MechSurvivalA", "MechSurvivalB"],
ScoreVar: "MechSurvivalScore",
ScoreMaxTag: "MechSurvivalScoreMax",
Reward: {
items: [
"/Lotus/StoreItems/Types/Items/MiscItems/FormaAura",
"/Lotus/StoreItems/Upgrades/Skins/Necramech/MechWeapon/MechEventMausolonSkin"
]
}
}
);
}
if (config.worldState?.bloodOfPerita && buildVersion >= gameToBuildVersionInt["41.0.0"]) {
worldState.Goals.push({
_id: toOid2("694189080000000000000000", buildVersion),
Activation: toMongoDate2(1765902600000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
GracePeriod: { $date: { $numberLong: "2000000000000" } },
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/TauPrequel/TauPrequelFinal/TauPrequelEventName",
ToolTip: "/Lotus/Language/TauPrequel/TauPrequelFinal/BloodOfPeritaDetails",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/BloodOfPeritaEventBadgeSmall.png",
Tag: "12MinWarEvent",
Node: "SolNode251"
});
pushGoalAlerts(worldState, "12MinWarEvent", buildVersion);
}
if (config.worldState?.lunarNewYear) {
const reapeingItems: Record<string, (Partial<IFlashSale> & { TypeName: string })[]> = {
"2022": [
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyTiger", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2022HeavyBladeSkin",
PremiumOverride: 45
},
{ TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/LNYCarpSugatra", PremiumOverride: 15 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2022IgnisSkin",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2022Zarr", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/Kubrows/Armor/Lunar2022KubrowArmor", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/Effects/PeachBlossomsEphemera", PremiumOverride: 60 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/CNY2021Poster", RegularOverride: 1 },
{ TypeName: "/Lotus/Types/StoreItems/AvatarImages/ChineseNewYear2021Glyph", RegularOverride: 1 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/WeGame/LuckyKavat", PremiumOverride: 35 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/WeGame/LuckyKavatWhite", PremiumOverride: 35 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/WeGame/LuckyKavatGold", PremiumOverride: 35 },
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2020AcceltraSkin", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2020OrthosSkin", PremiumOverride: 20 },
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2020PyranaSkin", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/Promo/WeGame/WeGameMacheteSkin", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/WegameChinaKnotDangle", PremiumOverride: 15 },
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2020PyranaSkin", PremiumOverride: 25 }
],
"2023": [
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyLNY2023Rabbit", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2023CedoSkin",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Upgrades/Skins/Sigils/WeGameNewYearTigerSigil", PremiumOverride: 40 },
{ TypeName: "/Lotus/Upgrades/Skins/Sigils/WeGameNewYearRabbitSigil", PremiumOverride: 40 },
{ TypeName: "/Lotus/Interface/Graphics/CustomUI/LunarNewYearStyle", PremiumOverride: 50 },
{
TypeName: "/Lotus/Interface/Graphics/CustomUI/Backgrounds/SpringFestivalBackground",
PremiumOverride: 50
},
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2023NagantakaSkin",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Upgrades/Skins/Effects/LunarEphemera", PremiumOverride: 60 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/Lunar2023CernosSkin",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Types/Items/Emotes/LNY2023Emote", PremiumOverride: 25 }
],
"2024": [
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyLNY2024Dragon", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2024Nukor",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2024DragonSigil", PremiumOverride: 40 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNYHookSword",
PremiumOverride: 75
},
{
TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/LNYBirdSugatra",
PremiumOverride: 15
},
{ TypeName: "/Lotus/Upgrades/Skins/Effects/LNYDragonEphemera", PremiumOverride: 60 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2024Ogris",
PremiumOverride: 25
},
{ TypeName: "/Lotus/Types/Items/Emotes/LNY2024DragonEmote", PremiumOverride: 25 },
{ TypeName: "/Lotus/Upgrades/Skins/Sentinels/Skins/LNYDragonSentinelSkin", PremiumOverride: 85 },
{ TypeName: "/Lotus/Upgrades/Skins/Sentinels/Masks/LNYDragonMask", PremiumOverride: 30 },
{ TypeName: "/Lotus/Upgrades/Skins/Sentinels/Wings/LNYDragonWings", PremiumOverride: 15 },
{ TypeName: "/Lotus/Upgrades/Skins/Sentinels/Tails/LNYDragonTail", PremiumOverride: 15 }
],
"2025": [
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushySnake", PremiumOverride: 35 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyLNYMirage", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/CNY2025ScytheSkin",
PremiumOverride: 25
},
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2025BoltorSkin",
PremiumOverride: 20
},
{
TypeName: "/Lotus/StoreItems/Upgrades/Skins/Catbrows/Armor/LNYKavatBoltorArmor",
PremiumOverride: 90
},
{ TypeName: "/Lotus/Upgrades/Skins/Effects/LNYStonesEphemera", PremiumOverride: 60 },
{ TypeName: "/Lotus/Upgrades/Skins/Sigils/WeGameNewYearSnakeSigil", PremiumOverride: 40 },
{
TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/LNYSnakeMeleeDangle",
PremiumOverride: 15
},
{ TypeName: "/Lotus/Types/Items/Emotes/LNY2025SnakeEmote", PremiumOverride: 15 }
],
"2026": [
{ TypeName: "/Lotus/Upgrades/Skins/Horse/DagathDeluxeLNYHorseBodySkin", PremiumOverride: 220 },
{ TypeName: "/Lotus/Upgrades/Skins/Horse/DagathDeluxeLNYHorseTail", PremiumOverride: 30 },
{ TypeName: "/Lotus/Types/Items/ShipDecos/Plushies/PlushyLNYKaithe", PremiumOverride: 35 },
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/CNY2025ScytheSkin",
PremiumOverride: 25
},
{
TypeName: "/Lotus/Upgrades/Skins/LunarNewYear/LNY2025BoltorSkin",
PremiumOverride: 20
},
{ TypeName: "/Lotus/Upgrades/Skins/Effects/LNYKaitheDagathEphemera", PremiumOverride: 60 },
{ TypeName: "/Lotus/Upgrades/Skins/Sigils/WeGameNewYearHorseSigil", PremiumOverride: 40 },
{
TypeName: "/Lotus/Upgrades/Skins/MeleeDangles/LNYFireSugatra",
PremiumOverride: 15
},
{ TypeName: "/Lotus/Types/Items/Emotes/LNY2026HorseEmote", PremiumOverride: 15 }
]
};
const storeItems: Record<string, (Partial<IFlashSale> & { TypeName: string })[]> = {
"2019": [{ TypeName: "/Lotus/Types/Items/ShipLayerCNY", RegularOverride: 1 }],
"2020": [{ TypeName: "/Lotus/Types/StoreItems/AvatarImages/ImageGengzi", RegularOverride: 1 }],
"2021": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2021BlessingsBundle", PremiumOverride: 130 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2021AnewBundle", PremiumOverride: 185 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2021LuminousBundle", PremiumOverride: 470 }
],
"2022": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2022BundleA", PremiumOverride: 135 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2022BundleB", PremiumOverride: 275 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2022BundleC", PremiumOverride: 515 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2022BundleD", PremiumOverride: 105 },
...[...reapeingItems["2022"]].sort((a, b) => a.TypeName.localeCompare(b.TypeName))
],
"2023": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2023BundleA", PremiumOverride: 135 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2023BundleB", PremiumOverride: 265 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2023BundleC", PremiumOverride: 500 },
{ TypeName: "/Lotus/Types/StoreItems/AvatarImages/CNY2023RabbitGlyph", RegularOverride: 1 },
...[...reapeingItems["2022"], ...reapeingItems["2023"]].sort((a, b) =>
a.TypeName.localeCompare(b.TypeName)
)
],
"2024": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2024BundleA", PremiumOverride: 135 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2024BundleB", PremiumOverride: 295 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2024BundleC", PremiumOverride: 575 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2024SentinelSkinBundle", PremiumOverride: 95 },
{
TypeName: "/Lotus/Types/StoreItems/AvatarImages/Seasonal/AvatarImageYearOfTheDragonGlyph",
RegularOverride: 1
},
...[...reapeingItems["2022"], ...reapeingItems["2023"], ...reapeingItems["2024"]].sort((a, b) =>
a.TypeName.localeCompare(b.TypeName)
)
],
"2025": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2025BundleA", PremiumOverride: 215 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2025BundleB", PremiumOverride: 590 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2025BundleC", PremiumOverride: 805 },
{
TypeName: "/Lotus/Types/StoreItems/AvatarImages/Seasonal/AvatarImageCNY2025SnakeGlyphB",
RegularOverride: 1
},
{ TypeName: "/Lotus/Upgrades/Skins/Clan/CNY2025SnakeEmblem", RegularOverride: 1 },
...[
...reapeingItems["2022"],
...reapeingItems["2023"],
...reapeingItems["2024"],
...reapeingItems["2025"]
].sort((a, b) => a.TypeName.localeCompare(b.TypeName))
],
"2026": [
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2026BundleA", PremiumOverride: 295 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2026BundleB", PremiumOverride: 480 },
{ TypeName: "/Lotus/Types/StoreItems/Packages/LNY2026BundleC", PremiumOverride: 700 },
{
TypeName: "/Lotus/Types/StoreItems/AvatarImages/LNY2026HorseGlyph",
RegularOverride: 1
},
{ TypeName: "/Lotus/Upgrades/Skins/Clan/LNY2026HorseGlyph", RegularOverride: 1 },
...[
...reapeingItems["2022"],
...reapeingItems["2023"],
...reapeingItems["2024"],
...reapeingItems["2025"],
...reapeingItems["2026"]
].sort((a, b) => a.TypeName.localeCompare(b.TypeName))
]
};
const selectedStoreItems: (Partial<IFlashSale> & { TypeName: string })[] = [];
if (config.worldState.lunarNewYear == "all") {
const addedTypeNames = new Set<string>();
const packageItems = [];
const otherItems = [];
for (const items of Object.values(storeItems)) {
for (const item of items) {
if (!addedTypeNames.has(item.TypeName)) {
addedTypeNames.add(item.TypeName);
if (item.TypeName.includes("/Lotus/Types/StoreItems/Packages/")) {
packageItems.push(item);
} else {
otherItems.push(item);
}
}
}
}
selectedStoreItems.push(
...packageItems.sort((a, b) => a.TypeName.localeCompare(b.TypeName)),
...otherItems.sort((a, b) => a.TypeName.localeCompare(b.TypeName))
);
} else if (config.worldState.lunarNewYear in storeItems) {
selectedStoreItems.push(...storeItems[config.worldState.lunarNewYear]);
}
pushFlashSales(worldState, selectedStoreItems, 1750865400000, 2000000000000, "SEASONAL", buildVersion);
}
// Thermia Fractures activates for 14 days, with alternating 4 and 3-day breaks
const thermiaFracturesCycleDay = day % 35;
const isThermiaFracturesActive =
thermiaFracturesCycleDay < 14 || (thermiaFracturesCycleDay >= 18 && thermiaFracturesCycleDay < 32);
const activeThermiaFracturesCycleDay =
thermiaFracturesCycleDay - (thermiaFracturesCycleDay < 14 ? 0 : thermiaFracturesCycleDay < 18 ? 14 : 32);
if (
(config.worldState?.thermiaFracturesOverride ?? isThermiaFracturesActive) &&
buildVersion >= gameToBuildVersionInt["24.5.1"]
) {
const activeStartDay = day - activeThermiaFracturesCycleDay;
const count = config.worldState?.thermiaFracturesProgressOverride ?? 0;
const activation = config.worldState?.thermiaFracturesOverride ? 1740416400000 : getSortieTime(activeStartDay);
const expiry = config.worldState?.thermiaFracturesOverride ? 2000000000000 : getSortieTime(activeStartDay + 14);
// If we push it, the game may show the event even tho it's not activated yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2721)
if (timeMs >= activation) {
worldState.Goals.push({
_id: toOid2("5c7cb0d00000000000000000", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
Node: "SolNode129",
ScoreVar: "FissuresClosed",
ScoreLocTag: "/Lotus/Language/G1Quests/HeatFissuresEventScore",
Count: count,
HealthPct: count / 100,
Regions: [1],
Desc: "/Lotus/Language/G1Quests/HeatFissuresEventName",
ToolTip: "/Lotus/Language/G1Quests/HeatFissuresEventDesc",
OptionalInMission: true,
Tag: "HeatFissure",
UpgradeIds: [{ $oid: "5c81cefa4c4566791728eaa7" }, { $oid: "5c81cefa4c4566791728eaa6" }],
Personal: true,
Community: true,
Goal: 100,
Reward: {
items: ["/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpBFG/Vandal/VandalCrpBFG"]
},
InterimGoals: [5, 25, 50, 75],
InterimRewards: [
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/OrbBadgeItem"] },
{
items: [
"/Lotus/StoreItems/Upgrades/Mods/DualSource/Shotgun/ShotgunMedicMod",
"/Lotus/StoreItems/Upgrades/Mods/DualSource/Rifle/SerratedRushMod"
]
},
{
items: [
"/Lotus/StoreItems/Upgrades/Mods/DualSource/Pistol/MultishotDodgeMod",
"/Lotus/StoreItems/Upgrades/Mods/DualSource/Melee/CritDamageChargeSpeedMod"
]
},
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/OrbSigil"] }
]
});
worldState.NodeOverrides.push({
_id: toOid2("5c7cb0d00000000000000000", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
Node: "SolNode129",
Faction: "FC_CORPUS",
CustomNpcEncounters: ["/Lotus/Types/Gameplay/Venus/Encounters/Heists/ExploiterHeistFissure"]
});
if (count >= 35) {
worldState.GlobalUpgrades.push({
_id: toOid2("5c81cefa4c4566791728eaa6", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
ExpiryDate: { $date: { $numberLong: expiry.toString() } },
UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
Nodes: ["SolNode129"]
});
}
// Not sure about that
if (count == 100) {
worldState.GlobalUpgrades.push({
_id: toOid2("5c81cefa4c4566791728eaa7", buildVersion),
Activation: { $date: { $numberLong: activation.toString() } },
ExpiryDate: { $date: { $numberLong: expiry.toString() } },
UpgradeType: "GAMEPLAY_PICKUP_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
Nodes: ["SolNode129"]
});
}
}
}
// Nightwave Challenges
const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildVersion);
if (nightwaveSyndicateTag) {
const nightwaveStartTimestamp = nightwaveTagToActivation[nightwaveSyndicateTag] ?? 1747851300000;
const nightwaveSeason = nightwaveTagToSeason[nightwaveSyndicateTag];
const nightwaveEpisode = ([0, 2, 4].includes(nightwaveSeason) && config.worldState?.nightwaveEpisode) || 1;
worldState.SeasonInfo = {
Activation: { $date: { $numberLong: nightwaveStartTimestamp.toString() } },
Expiry: { $date: { $numberLong: "2000000000000" } },
AffiliationTag: nightwaveSyndicateTag,
Season: nightwaveSeason,
Phase: nightwaveEpisode - 1,
Params: "",
ActiveChallenges: []
};
const pools = getSeasonChallengePools(nightwaveSyndicateTag);
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0));
if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) {
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1));
}
pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week, nightwaveStartTimestamp, nightwaveSeason);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
pushWeeklyActs(
worldState.SeasonInfo.ActiveChallenges,
pools,
week + 1,
nightwaveStartTimestamp,
nightwaveSeason
);
}
}
{
worldState.NodeOverrides.push(
{ _id: toOid2("549b18e9b029cef5991d6aec", buildVersion), Node: "EuropaHUB", Hide: true },
{ _id: toOid2("54a1737aeb658f6cbccf70ff", buildVersion), Node: "ErisHUB", Hide: true },
{ _id: toOid2("54a736ddec12f80bd6e9e326", buildVersion), Node: "VenusHUB", Hide: true }
);
if (buildVersion >= gameToBuildVersionInt["22.18.0"]) {
worldState.NodeOverrides.push({
_id: toOid2("5ad9f9bb6df82a56eabf3d44", buildVersion),
Node: "SolNode802",
// Elite Sanctuary Onslaught cycling every week
Seed: new SRng(week).randomInt(0, 0xff_ffff)
});
}
if (buildVersion >= gameToBuildVersionInt["25.7.0"]) {
worldState.NodeOverrides.push({
_id: toOid2("5d24d1f674491d51f8d44473", buildVersion),
Node: "MercuryHUB",
Hide: true,
LevelOverride: "/Lotus/Levels/Proc/Hub/RelayStationHubHydroid",
Activation: toMongoDate2(1563030000000, buildVersion)
});
}
if (buildVersion >= gameToBuildVersionInt["21.0.0"]) {
worldState.NodeOverrides.push({
_id: toOid2("5b8817c2bd4f253264d6aa91", buildVersion),
Node: "EarthHUB",
Hide: false,
LevelOverride: "/Lotus/Levels/Proc/Hub/RelayStationHubTwoB",
Activation: toMongoDate2(1535646600000, buildVersion)
});
}
}
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
let bountyCycle = Math.trunc((timeSecs - bountyEpoch) / eidolonCycleDuration);
let bountyCycleEnd: number | undefined;
do {
const bountyCycleStart = Math.trunc((bountyEpoch + bountyCycle * eidolonCycleDuration) * 1000);
bountyCycleEnd = Math.trunc(bountyCycleStart + eidolonCycleDuration * 1000);
if (buildVersion >= gameToBuildVersionInt["29.0.0"]) {
worldState.SyndicateMissions.push({
_id: toOid2(
((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000004",
buildVersion
),
Activation: toMongoDate2(bountyCycleStart, buildVersion),
Expiry: toMongoDate2(bountyCycleEnd, buildVersion),
Tag: "EntratiLabSyndicate",
Seed: bountyCycle,
Nodes: []
});
}
if (buildVersion >= gameToBuildVersionInt["31.5.0"]) {
worldState.SyndicateMissions.push({
_id: toOid2(
((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000029",
buildVersion
),
Activation: toMongoDate2(bountyCycleStart, buildVersion),
Expiry: toMongoDate2(bountyCycleEnd, buildVersion),
Tag: "ZarimanSyndicate",
Seed: bountyCycle,
Nodes: []
});
}
if (buildVersion >= gameToBuildVersionInt["38.0.0"]) {
worldState.SyndicateMissions.push({
_id: toOid2(
((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000006",
buildVersion
),
Activation: toMongoDate2(bountyCycleStart, buildVersion),
Expiry: toMongoDate2(bountyCycleEnd, buildVersion),
Tag: "HexSyndicate",
Seed: bountyCycle,
Nodes: []
});
}
pushClassicBounties(worldState.SyndicateMissions, bountyCycle, buildVersion);
} 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) &&
buildVersion >= gameToBuildVersionInt["22.8.2"]
) {
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) / unixTimesInMs.day;
const progress = (dayWithFraction - activeStartDay) / (activeEndDay - activeStartDay);
const healthPct = 1 - Math.min(Math.max(progress, 0), 1);
const bountyCycleStartSecs = Math.trunc(bountyEpoch + bountyCycle * eidolonCycleDuration);
worldState.Goals.push({
_id: toOid2("687ebbe6d1d17841c9c59f38", buildVersion),
Activation: {
$date: {
$numberLong: config.worldState?.ghoulEmergenceOverride
? "1753204900185"
: (EPOCH + activeStartDay * unixTimesInMs.day).toString()
}
},
Expiry: {
$date: {
$numberLong: config.worldState?.ghoulEmergenceOverride
? "2000000000000"
: (EPOCH + activeEndDay * unixTimesInMs.day).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: (bountyCycleStartSecs & 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?.breedingGrounds && buildVersion >= gameToBuildVersionInt["14.0.0"]) {
worldState.Goals.push({
_id: toOid2("53a35a043f9a5b16e0308ea7", buildVersion),
Activation: toMongoDate2(1753204900185, buildVersion),
Count: 0,
Desc: "/Lotus/Language/G1Quests/InfestedCorpusHiveEventName",
Expiry: toMongoDate2(2000000000000, buildVersion),
Goal: 48,
InterimGoals: [6, 17],
MissionKeyName: "/Lotus/Types/Keys/InfestedCorpusHiveEventKey",
Node: "EventNode10",
Success: 0,
ToolTip: "/Lotus/Language/G1Quests/InfestedCorpusHiveEventToolTip",
Personal: true,
Best: !config.unfaithfulBugFixes?.giveBreedingGroundsRewardsAtSum,
Tag: changeLegacyTags ? tagsForOlderGoals[3] : "HiveEvent", // Madeup tag
InterimRewards: [
{ items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/HiveSabotageEventBadgeItem"] },
{
items: [
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/FireEventRifleMod",
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/FireEventShotgunMod",
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/FireEventPistolMod",
"/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/FireEventMeleeMod"
]
}
],
Reward: { items: ["/Lotus/StoreItems/Weapons/ClanTech/Energy/VandalElectroProd"] },
ScoreSumTag: "HiveEventScoreSum",
ScoreMaxTag: "HiveEvent"
});
}
if (config.worldState?.creditBoostMultiplier) {
worldState.GlobalUpgrades.push({
_id: toOid2("5b23106f283a555109666672", buildVersion),
Activation: toMongoDate2(1740164400000, buildVersion),
ExpiryDate: toMongoDate2(2000000000000, buildVersion),
UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT",
OperationType: "MULTIPLY",
Value: config.worldState.creditBoostMultiplier,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
if (config.worldState?.affinityBoostMultiplier) {
worldState.GlobalUpgrades.push({
_id: toOid2("5b23106f283a555109666673", buildVersion),
Activation: toMongoDate2(1740164400000, buildVersion),
ExpiryDate: toMongoDate2(2000000000000, buildVersion),
UpgradeType: "GAMEPLAY_KILL_XP_AMOUNT",
OperationType: "MULTIPLY",
Value: config.worldState.affinityBoostMultiplier,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
if (config.worldState?.resourceBoostMultiplier) {
worldState.GlobalUpgrades.push({
_id: toOid2("5b23106f283a555109666674", buildVersion),
Activation: toMongoDate2(1740164400000, buildVersion),
ExpiryDate: toMongoDate2(2000000000000, buildVersion),
UpgradeType: "GAMEPLAY_PICKUP_AMOUNT",
OperationType: "MULTIPLY",
Value: config.worldState.resourceBoostMultiplier,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
// Sheev parts were added in 19.6.3, so versions prior to that may take too kindly to seeing invasions.
if (buildVersion > gameToBuildVersionInt["19.5.3"]) {
// Rough outline of dynamic invasions.
// TODO: Invasions chains, e.g. an infestation mission would soon lead to other nodes on that planet also having an infestation invasion.
// TODO: Grineer/Corpus to fund their death stars with each invasion win.
{
worldState.Invasions.push(createInvasion(day, 0, buildVersion));
worldState.Invasions.push(createInvasion(day, 1, buildVersion));
worldState.Invasions.push(createInvasion(day, 2, buildVersion));
// Completed invasions stay for up to 24 hours as the winner 'occupies' that node
worldState.Invasions.push(createInvasion(day - 1, 0, buildVersion));
worldState.Invasions.push(createInvasion(day - 1, 1, buildVersion));
worldState.Invasions.push(createInvasion(day - 1, 2, buildVersion));
}
}
// Baro
// Introdused in U15.6
if (buildVersion > gameToBuildVersionInt["15.5.0"]) {
const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));
const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000;
const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12);
const baroEnd = baroStart + unixTimesInMs.day * 14;
const baroRelayOverride = config.worldState?.baroRelayOverride;
const baroNodeIndex = baroRelayOverride && baroRelayOverride > 0 ? baroRelayOverride - 1 : baroIndex % 4;
let baroNode = ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroNodeIndex];
if (baroNode == "MercuryHUB" && buildVersion < gameToBuildVersionInt["18.18.0"]) {
// This Pre-Star Chart 3.0 client won't know Larunda Relay, so move Baro elsewhere.
baroNode = "EarthHUB";
}
const evilBaroStage =
buildVersion >= gameToBuildVersionInt["40.0.0"] ? (config.worldState?.evilBaroStage ?? 0) : 0;
const baroCharacter = ["Baro'Ki Teel", "EvilBaroWeek1", "EvilBaroWeek2", "EvilBaroWeek3", "EvilBaroWeek4"][
evilBaroStage
];
const vt: IVoidTrader = {
_id: toOid2(
((baroStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "493c96d6067610bc",
buildVersion
),
Activation: toMongoDate2(baroActualStart, buildVersion),
Expiry: toMongoDate2(baroEnd, buildVersion),
Character: baroCharacter,
Node: baroNode,
Manifest: []
};
worldState.VoidTraders.push(vt);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, baroActualStart)) {
if (config.worldState?.baroFullyStocked) {
fullyStockBaro(vt, buildVersion);
} else {
const tempManifest: IBaroDataTradeOffer[] = [];
const rng = new SRng(new SRng(baroIndex).randomInt(0, 100_000));
// TOVERIFY: Constraint for upgrades amount?
// TOVERIFY: Constraint for weapon amount?
// TOVERIFY: Constraint for relics amount?
const armorSet = rng.randomElement(baro.armorSets)!;
const armorItems = armorSet.bundle && rng.randomInt(0, 1) == 0 ? [armorSet.bundle] : armorSet.items;
while (tempManifest.length + armorItems.length < 31) {
const item = rng.randomElement(baro.rest)!;
if (tempManifest.indexOf(item) == -1) {
const set = baro.allIfAny.find(set => set.indexOf(item.ItemType) != -1);
if (set) {
for (const itemType of set) {
tempManifest.push(baro.rest.find(x => x.ItemType == itemType)!);
}
} else {
tempManifest.push(item);
}
}
}
const overflow = 31 - (tempManifest.length + armorItems.length);
if (overflow > 0) {
tempManifest.splice(0, overflow);
}
tempManifest.push(...armorItems);
{
const evilBaroStock: string[] = [];
if (evilBaroStage >= 1) evilBaroStock.push("EvilBaroArcaArmorA", "BaroEvilEphemera");
if (evilBaroStage >= 2) evilBaroStock.push("GrimoireEvilBaroSkin", "EvilBaroArcaArmorC");
if (evilBaroStage >= 3) evilBaroStock.push("EvilBaroArcaArmorL", "EvilBaroSilvaAndAegis");
if (evilBaroStage >= 4) {
evilBaroStock.push(
"EvilBaroNecraloidSigil",
"DissolveEnemyMod",
"EvilBaroFloatingCandlesShipDeco"
);
}
tempManifest.unshift(
...baro.evilBaro.filter(item => evilBaroStock.some(end => item.ItemType.endsWith(end)))
);
}
for (const item of baro.evergreen) {
tempManifest.push(item);
}
vt.Manifest.push(
...tempManifest
.filter(({ minBuildVersionInt }) => buildVersion >= minBuildVersionInt)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ minBuildVersionInt, ...offer }) => offer)
);
}
}
}
// Varzia
// introduced in 30.9.4
if (buildVersion >= gameToBuildVersionInt["31.0.0"]) {
const pt: IPrimeVaultTrader = {
_id: { $oid: ((weekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "c36af423770eaa97" },
Activation: { $date: { $numberLong: weekStart.toString() } },
InitialStartDate: { $date: { $numberLong: "1662738144266" } },
Node: "TradeHUB1",
Manifest: [],
Expiry: { $date: { $numberLong: weekEnd.toString() } },
EvergreenManifest: varzia.evergreen,
ScheduleInfo: []
};
worldState.PrimeVaultTraders.push(pt);
const rotation = config.worldState?.varziaOverride || getVarziaRotation(week, buildVersion);
pt.Manifest = config.worldState?.varziaFullyStocked
? getAllVarziaManifests(buildVersion)
: getVarziaManifest(rotation, buildVersion);
if (config.worldState?.varziaOverride || config.worldState?.varziaFullyStocked) {
pt.Expiry = { $date: { $numberLong: "2000000000000" } };
} else {
pt.ScheduleInfo.push({
Expiry: { $date: { $numberLong: (weekEnd + unixTimesInMs.week).toString() } },
FeaturedItem: getVarziaRotation(week + 1, buildVersion)
});
}
}
// Void Storms
if (buildVersion >= gameToBuildVersionInt["30.0.0"]) {
const hour = Math.trunc(timeMs / unixTimesInMs.hour);
const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
if (overLastHourStormExpiry > timeMs) {
pushVoidStorms(worldState.VoidStorms, hour - 2);
}
pushVoidStorms(worldState.VoidStorms, hour - 1);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
pushVoidStorms(worldState.VoidStorms, hour);
}
}
// Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST)
if (buildVersion >= gameToBuildVersionInt["15.0.0"]) {
const rollover = getSortieTime(day);
// Omit sorties for pre-Star Chart 3.0 clients to avoid breaking them.
if (buildVersion >= gameToBuildVersionInt["18.18.0"]) {
if (timeMs < rollover) {
worldState.Sorties.push(getSortie(day - 1, buildVersion));
if (
buildVersion >= gameToBuildVersionInt["25.7.0"] &&
isBeforeNextExpectedWorldStateRefresh(timeMs, rollover)
) {
worldState.Sorties.push(getSortie(day, buildVersion));
}
} else {
worldState.Sorties.push(getSortie(day, buildVersion));
}
}
// The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
const sdy = timeMs >= rollover ? day : day - 1;
const rng = new SRng(sdy);
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804e", "NewLokaSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48050", "PerrinSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4805e", "RedVeilSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48061", "SteelMeridianSyndicate");
}
if (config.worldState?.communitySynthesisTarget) {
const targetType = `/Lotus/Types/Game/Library/Targets/Research${config.worldState.communitySynthesisTarget}Target`;
worldState.LibraryInfo = {
CurrentTarget: {
StartTime: toMongoDate2(0, buildVersion),
TargetType: targetType,
EnemyType: libraryTargetToAvatar[targetType],
PersonalScansRequired: 10,
ProgressPercent: config.worldState.communitySynthesisProgress ?? 0
}
};
if (config.worldState.communitySynthesisTarget != 1) {
worldState.LibraryInfo.LastCompletedTargetType = `/Lotus/Types/Game/Library/Targets/Research${config.worldState.communitySynthesisTarget - 1}Target`;
}
}
if (config.worldState?.snowdayShowdown && buildVersion >= gameToBuildVersionInt["19.4.1"]) {
worldState.PVPAlternativeModes.push({
TargetMode: "PVPMODE_TEAMDEATHMATCH",
TitleLoc: "/Lotus/Language/G1Quests/TacAlertSnowballFightTitle",
DescriptionLoc: "/Lotus/Language/G1Quests/TacAlertSnowballFightToolTip",
DisableEnergyPickups: true,
DisableEnergySurge: true,
DisableAmmoPickups: true,
DisableWeaponSwitching: true,
DisableWeaponHud: false,
EnergyCapOverride: 0,
ForceChangeLoadoutOnDeath: false,
MatchTimeOverride: 600,
MaxPlayersOverride: 8,
MinPlayersPerTeamOverride: 2,
MaxTeamCountDifferenceOverride: 2,
WeaponOverrides: [
{ Override: false, UseFirstAsDefault: true, Resources: [], OriginalVersions: [] },
{
Override: true,
UseFirstAsDefault: true,
Resources: ["/Lotus/Weapons/Tenno/ThrowingWeapons/VariantSnowBalls"],
OriginalVersions: []
},
{ Override: true, UseFirstAsDefault: true, Resources: [], OriginalVersions: [] }
],
MeleeWeaponOverride: {
Override: true,
UseFirstAsDefault: true,
Resources: ["/Lotus/Weapons/Tenno/Melee/Scythe/ParisScythe/VariantXmasScythe"],
OriginalVersions: [],
IsModularMeleeWeapon: false,
BalancesPool: [],
HandlesPool: [],
TipsPool: []
}
});
}
if (buildVersion >= gameToBuildVersionInt["17.7.1"]) {
if (buildVersion >= gameToBuildVersionInt["18.0.2"]) {
const conclaveWeekStart = weekStart + 40 * unixTimesInMs.minute - 2 * unixTimesInMs.day;
const conclaveWeekEnd = conclaveWeekStart + unixTimesInMs.week;
pushConclaveWeekly(worldState.PVPChallengeInstances, week, buildVersion);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, conclaveWeekEnd)) {
pushConclaveWeekly(worldState.PVPChallengeInstances, week + 1, buildVersion);
}
}
{
const conclaveDayStart =
EPOCH + day * unixTimesInMs.day + 5 * unixTimesInMs.hour + 30 * unixTimesInMs.minute;
const conclaveDayEnd = conclaveDayStart + unixTimesInMs.day;
pushConclaveDailys(worldState.PVPChallengeInstances, day, buildVersion);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, conclaveDayEnd)) {
pushConclaveDailys(worldState.PVPChallengeInstances, day + 1, buildVersion);
}
}
}
// Archon Hunt cycling every week
if (buildVersion >= gameToBuildVersionInt["32.0.0"]) {
worldState.LiteSorties.push(getLiteSortie(week));
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
worldState.LiteSorties.push(getLiteSortie(week + 1));
}
}
// Circuit choices cycling every week
if (buildVersion >= gameToBuildVersionInt["42.0.0"]) {
worldState.EndlessXpSchedule = [
{
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
CategoryChoices: getEndlessXpChoices(week, buildVersion)
}
];
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
worldState.EndlessXpSchedule.push({
Activation: { $date: { $numberLong: (weekStart + 604800000).toString() } },
Expiry: { $date: { $numberLong: (weekEnd + 604800000).toString() } },
CategoryChoices: getEndlessXpChoices(week + 1, buildVersion)
});
}
} else if (buildVersion >= gameToBuildVersionInt["33.0.0"]) {
worldState.EndlessXpChoices = getEndlessXpChoices(week, buildVersion);
}
// 1999 Calendar Season cycling every week + YearIteration every 4 weeks
if (buildVersion >= gameToBuildVersionInt["38.0.0"]) {
worldState.KnownCalendarSeasons.push(getCalendarSeason(week));
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
}
}
const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[week % 4];
const labConquest = getConquest("CT_LAB", week, null);
const hexConquest = getConquest("CT_HEX", week, season);
if (buildVersion >= gameToBuildVersionInt["40.0.0"]) {
worldState.Conquests = [labConquest, hexConquest];
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[(week + 1) % 4];
worldState.Conquests.push(getConquest("CT_LAB", week, null));
worldState.Conquests.push(getConquest("CT_HEX", week, season));
}
}
if (buildVersion >= gameToBuildVersionInt["41.0.0"]) {
worldState.Descents = [getDescent(week)];
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
worldState.Descents.push(getDescent(week + 1));
}
}
// Sentient Anomaly + Xtra Cheese cycles
const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2));
const hourInSeconds = 3600;
const cheeseInterval = hourInSeconds * 8;
const cheeseDuration = hourInSeconds * 2;
const cheeseIndex = Math.trunc(timeSecs / cheeseInterval);
let cheeseStart = cheeseIndex * cheeseInterval;
let cheeseEnd = cheeseStart + cheeseDuration;
let cheeseNext = (cheeseIndex + 1) * cheeseInterval;
// Live servers only update the start time once it happens, which makes the
// client show a negative countdown during off-hours. Optionally adjust the
// times so the next activation is always in the future.
if (config.unfaithfulBugFixes?.fixXtraCheeseTimer && timeSecs >= cheeseEnd) {
cheeseStart = cheeseNext;
cheeseEnd = cheeseStart + cheeseDuration;
cheeseNext += cheeseInterval;
}
const tmp: ITmp = {
cavabegin: "1690761600",
PurchasePlatformLockEnabled: true,
pgr: {
ts: "1732572900",
en: "CUSTOM DECALS @ ZEVILA",
fr: "DECALS CUSTOM @ ZEVILA",
it: "DECALCOMANIE PERSONALIZZATE @ ZEVILA",
de: "AUFKLEBER NACH WUNSCH @ ZEVILA",
es: "CALCOMANÍAS PERSONALIZADAS @ ZEVILA",
pt: "DECALQUES PERSONALIZADOS NA ZEVILA",
ru: "ПОЛЬЗОВАТЕЛЬСКИЕ НАКЛЕЙКИ @ ЗеВиЛа",
pl: "NOWE NAKLEJKI @ ZEVILA",
uk: "КОРИСТУВАЦЬКІ ДЕКОЛІ @ ЗІВІЛА",
tr: "ÖZEL ÇIKARTMALAR @ ZEVILA",
ja: "カスタムデカール @ ゼビラ",
zh: "定制贴花认准泽威拉",
ko: "커스텀 데칼 @ ZEVILA",
tc: "自訂貼花 @ ZEVILA",
th: "รูปลอกสั่งทำที่ ZEVILA"
},
ennnd: true,
mbrt: true,
fbst: {
a: cheeseStart,
e: cheeseEnd,
n: cheeseNext
},
lqo: {
mt: labConquest.Missions.map(x => getMissionTypeForLegacyOverride(x.missionType, "CT_LAB")),
mv: labConquest.Missions.map(x => x.difficulties[1].deviation),
c: labConquest.Missions.map(x => x.difficulties[1].risks),
fv: labConquest.Variables
},
hqo: {
mt: hexConquest.Missions.map(x => getMissionTypeForLegacyOverride(x.missionType, "CT_HEX")),
mv: hexConquest.Missions.map(x => x.difficulties[1].deviation),
mf: hexConquest.Missions.map(x => factionToInt(x.faction)),
c: hexConquest.Missions.map(x => x.difficulties[1].risks),
fv: hexConquest.Variables
},
sfn: [550, 553, 554, 555][halfHour % 4],
tcend26: true,
tcaz: 3
};
if (Array.isArray(config.worldState?.circuitGameModes)) {
tmp.edg = config.worldState.circuitGameModes as TCircuitGameMode[];
}
worldState.Tmp = JSON.stringify(tmp);
if (convertGoals && buildVersion < gameToBuildVersionInt["20.4.0"]) {
worldState.Version = 9;
convertGoalsToV9(worldState.Goals);
}
// This must be the last field in these versions.
if (buildVersion >= gameToBuildVersionInt["18.18.0"] && buildVersion <= gameToBuildVersionInt["20.4.0"]) {
worldState.WorldSeed = "4763605";
}
return worldState;
};
const convertGoalsToV9 = (goals: IGoal[]): void => {
for (let i = 0; i < goals.length; i++) {
const g = goals[i];
goals[i] = {
_id: g._id,
Activation: g.Activation,
AltActivation: g.AltActivation,
Expiry: g.Expiry,
Count: g.Count,
CountAlt: g.CountAlt,
ParentCountClamp: g.ParentCountClamp,
HealthPct: g.HealthPct,
Icon: g.Icon,
Desc: g.Desc,
CommunityReqDesc: g.CommunityReqDesc,
ToolTip: g.ToolTip,
Faction: g.Faction,
Goal: g.Goal,
GoalInterim: g.InterimGoals && g.InterimGoals[0] ? g.InterimGoals[0] : undefined,
GoalInterim2: g.InterimGoals && g.InterimGoals[1] ? g.InterimGoals[1] : undefined,
BonusGoal: g.BonusGoal,
Success: g.Success,
Personal: g.Personal,
Best: g.Best,
Ongoing: g.Ongoing,
Bounty: g.Bounty,
Fomorian: g.Fomorian,
Invasion: g.Invasion,
ClampNodeScores: g.ClampNodeScores,
Roaming: g.Roaming,
PvpRep: g.PvpRep,
Transmission: g.Transmission,
ItemType: g.ItemType,
Tag: g.Tag,
ParentTag: g.ParentTag,
PrereqGoalTags: g.PrereqGoalTags,
Node: g.Node,
VictimNode: g.VictimNode,
InvasionNode: g.InvasionNode,
ConcurrentMissionKeyNames: g.ConcurrentMissionKeyNames,
ConcurrentMissionInfo: g.ConcurrentMissionInfo,
ConcurrentNodeReqs: g.ConcurrentNodeReqs,
ConcurrentNodes: g.ConcurrentNodes,
MissionKeyName: g.MissionKeyName,
KeyRequired: g.KeyRequired,
Reward: g.Reward,
RewardInterim: g.InterimRewards && g.InterimRewards[0] ? g.InterimRewards[0] : undefined,
RewardInterim2: g.InterimRewards && g.InterimRewards[1] ? g.InterimRewards[1] : undefined,
BonusReward: g.BonusReward,
ScoreVar: g.ScoreVar,
ScoreLocTag: g.ScoreLocTag,
ScoreSumTag: g.ScoreSumTag,
ScoreMaxTag: g.ScoreMaxTag,
ScoreMaxNode: g.ScoreMaxNode,
ArchiveTag: g.JobAffiliationTag,
MissionInfo: g.MissionInfo,
SuccessHubEvent: g.SuccessHubEvent,
FailureHubEvent: g.FailureHubEvent,
ContinuousHubEvent: g.ContinuousHubEvent,
Types: g.Types,
RewardRegion: g.RewardRegion,
RegionDrops: g.RegionDrops,
ArchwingDrops: g.ArchwingDrops,
Diorama: g.Diorama,
RadioSound: g.RadioSound,
MaxConclave: g.MaxConclave,
BonusMaxConclave: g.BonusMaxConclave,
BonusLevelModifier: g.BonusLevelModifier,
BonusWaveModifier: g.BonusWaveModifier
} as IGoalV9;
}
};
export const populateFissures = async (worldState: IWorldState): Promise<void> => {
const buildVersion = buildVersionToInt(worldState.BuildLabel);
if (buildVersion < gameToBuildVersionInt["18.16.0"]) return;
if (config.worldState?.allTheFissures) {
let i = 0;
for (const [tier, nodes] of Object.entries(fissureMissions)) {
for (const node of nodes) {
const meta = ExportRegions[node];
worldState.ActiveMissions.push({
_id: toOid2((i++).toString().padStart(8, "0") + "8e0c70ba050f1eb7", buildVersion),
Region: meta.systemIndex + 1,
Seed: 1337,
Activation: toMongoDate2(1000000000000, buildVersion),
Expiry: toMongoDate2(2000000000000, buildVersion),
Node: node,
MissionType: meta.missionType,
Modifier: tier,
Hard: config.worldState.allTheFissures == "hard"
});
}
}
} else {
const fissures = await Fissure.find({});
for (const fissure of fissures) {
const meta = ExportRegions[fissure.Node];
worldState.ActiveMissions.push({
_id: toOid2(fissure._id, buildVersion),
Region: meta.systemIndex + 1,
Seed: 1337,
Activation:
fissure.Activation.getTime() < Date.now() // Activation is in the past?
? toMongoDate2(1000000000000, buildVersion) // Let the client know 'explicitly' to avoid interference from time constraints.
: toMongoDate2(fissure.Activation, buildVersion),
Expiry: toMongoDate2(fissure.Expiry, buildVersion),
Node: fissure.Node,
MissionType: meta.missionType,
Modifier: fissure.Modifier,
Hard: fissure.Hard
});
}
}
worldState.ActiveMissions = worldState.ActiveMissions.filter(fissure => {
const meta = ExportRegions[fissure.Node];
if (
(meta.faction == "FC_MITW" || fissure.Modifier == "VoidT6") &&
buildVersion < gameToBuildVersionInt["35.5.0"]
) {
return false;
}
if (fissure.Hard && buildVersion < gameToBuildVersionInt["32.0.0"]) {
return false;
}
if (meta.systemIndex == 16 && buildVersion < gameToBuildVersionInt["29.0.0"]) {
return false;
}
if (fissure.Modifier == "VoidT5" && buildVersion < gameToBuildVersionInt["26.0.0"]) {
return false;
}
if (fissure.MissionType == "MT_ARTIFACT" && buildVersion < gameToBuildVersionInt["25.7.0"]) {
return false;
}
if (
meta.systemIndex == 17 &&
buildVersion < gameToBuildVersionInt["18.0.2"] // Should be 18.0.0
) {
return false;
}
return true;
});
if (buildVersion < gameToBuildVersionInt["32.0.0"]) {
for (const fissure of worldState.ActiveMissions) {
delete fissure.Hard;
}
}
};
export const populateDailyDeal = async (worldState: IWorldState): Promise<void> => {
const buildVersion = buildVersionToInt(worldState.BuildLabel);
const dailyDeals = await DailyDeal.find({});
for (const dailyDeal of dailyDeals) {
const meta = darvoDeals.find(d => d.StoreItem == dailyDeal.StoreItem);
if ((!meta || buildVersion >= meta.minBuildVersionInt) && dailyDeal.Expiry.getTime() > Date.now()) {
worldState.DailyDeals.push({
StoreItem: dailyDeal.StoreItem,
Activation: toMongoDate2(dailyDeal.Activation, buildVersion),
Expiry: toMongoDate2(dailyDeal.Expiry, buildVersion),
Discount: dailyDeal.Discount,
OriginalPrice: dailyDeal.OriginalPrice,
SalePrice: dailyDeal.SalePrice,
AmountTotal: Math.round(dailyDeal.AmountTotal * (config.worldState?.darvoStockMultiplier ?? 1)),
AmountSold: dailyDeal.AmountSold
});
}
}
};
export const idToBountyCycle = (id: string): number => {
return Math.round((parseInt(id.substring(0, 8), 16) - bountyEpoch) / eidolonCycleDuration);
};
export const idToDay = (id: string): number => {
return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 86400_000);
};
export const idToWeek = (id: string): number => {
return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 604800_000);
};
export const getLiteSortie = (week: number): ILiteSortie => {
const boss = (["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"] as const)[week % 3];
const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3];
const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth
const nodes: string[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex === systemIndex &&
(value.faction == "FC_GRINEER" || value.faction == "FC_CORPUS") &&
!isArchwingMission(value) &&
value.missionType != "MT_ASSASSINATION" &&
value.missionType != "MT_JUNCTION" &&
value.missionType != "MT_LANDSCAPE" &&
value.missionType != "MT_RAILJACK" &&
key != "SolNode63" && // This node uses GrineerForestTilesetCaves which only supports MT_CAPTURE, which is not valid for LiteSorties.
key != "SolNode15" // This node uses GrineerGalleonTileset which seems to not support LiteSorties
) {
nodes.push(key);
}
}
const seed = new SRng(week).randomInt(0, 100_000);
const rng = new SRng(seed);
const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
const firstNode = nodes[firstNodeIndex];
nodes.splice(firstNodeIndex, 1);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
return {
_id: {
$oid: ((weekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "5e23a244740a190c"
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards",
Seed: seed,
Boss: boss,
Missions: [
{
missionType: rng.randomElement([
"MT_INTEL",
"MT_MOBILE_DEFENSE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_RESCUE"
])!,
node: firstNode
},
{
missionType: rng.randomElement([
"MT_DEFENSE",
"MT_TERRITORY",
"MT_ARTIFACT",
"MT_EXCAVATE",
"MT_SURVIVAL"
])!,
node: rng.randomElement(nodes)!
},
{
missionType: "MT_ASSASSINATION",
node: showdownNode
}
]
};
};
const getEndlessXpChoices = (week: number, buildVersion: number): IEndlessXpChoice[] => {
const normalChoices = [
["Nidus", "Octavia", "Harrow"],
["Gara", "Khora", "Revenant"],
["Garuda", "Baruuk", "Hildryn"],
["Excalibur", "Trinity", "Ember"],
["Loki", "Mag", "Rhino"],
["Ash", "Frost", "Nyx"],
["Saryn", "Vauban", "Nova"],
["Nekros", "Valkyr", "Oberon"],
["Hydroid", "Mirage", "Limbo"],
["Mesa", "Chroma", "Atlas"],
["Ivara", "Inaros", "Titania"]
];
const hardChoices = [
["Boar", "Gammacor", "Angstrum", "Gorgon", "Anku"],
["Bo", "Latron", "Furis", "Furax", "Strun"],
["Lex", "Magistar", "Boltor", "Bronco", "CeramicDagger"],
["Torid", "DualToxocyst", "DualIchor", "Miter", "Atomos"],
["AckAndBrunt", "Soma", "Vasto", "NamiSolo", "Burston"],
["Zylok", "Sibear", "Dread", "Despair", "Hate"],
["Dera", "Sybaris", "Cestra", "Sicarus", "Okina"],
...(buildVersion >= gameToBuildVersionInt["43.0.0"]
? [["Vectis", "Stug", "Ballistica", "Destreza", "Obex"]]
: []),
["Braton", "Lato", "Skana", "Paris", "Kunai"]
];
return [
{
Category: "EXC_NORMAL",
Choices: normalChoices[week % normalChoices.length]
},
{
Category: "EXC_HARD",
Choices: hardChoices[week % hardChoices.length]
}
];
};
export const isArchwingMission = (node: IRegion): boolean => {
if (node.name.indexOf("Archwing") != -1) {
return true;
}
// SettlementNode10
if (node.missionType == "MT_RACE") {
return true;
}
return false;
};
export const getNightwaveSyndicateTag = (buildVersion: number = BV_LATEST): string | undefined => {
if (config.worldState?.nightwaveOverride) {
if (config.worldState.nightwaveOverride in nightwaveTagToSeason) {
return config.worldState.nightwaveOverride;
}
if (config.worldState.nightwaveOverride == "disable") {
return undefined;
}
logger.warn(`ignoring invalid config value for worldState.nightwaveOverride`, {
value: config.worldState.nightwaveOverride,
valid_values: Object.keys(nightwaveTagToSeason)
});
}
if (buildVersion >= gameToBuildVersionInt["42.0.6"]) {
return "RadioLegionIntermission15Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["40.0.0"]) {
return "RadioLegionIntermission14Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["38.6.0"]) {
return "RadioLegionIntermission13Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["38.0.8"]) {
return "RadioLegionIntermission12Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["36.1.2"]) {
return "RadioLegionIntermission11Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["35.5.9"]) {
return "RadioLegionIntermission10Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["34.0.8"]) {
return "RadioLegionIntermission9Syndicate";
}
// Actual version is U33.0.11
if (buildVersion > gameToBuildVersionInt["33.0.10"]) {
return "RadioLegionIntermission8Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["32.2.0"]) {
return "RadioLegionIntermission7Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["31.6.4"]) {
return "RadioLegionIntermission6Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["31.2.0"]) {
return "RadioLegionIntermission5Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["30.6.0"]) {
return "RadioLegionIntermission4Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["29.7.0"]) {
return "RadioLegionIntermission3Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["28.3.2"]) {
return "RadioLegion3Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["25.8.2"]) {
return "RadioLegionIntermission2Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["25.3.0"]) {
return "RadioLegion2Syndicate";
}
if (buildVersion >= gameToBuildVersionInt["25.1.2"]) {
return "RadioLegionIntermissionSyndicate";
}
if (buildVersion >= gameToBuildVersionInt["24.3.0"]) {
return "RadioLegionSyndicate";
}
return undefined;
};
export const nightwaveTagToSeason: Record<string, number> = {
RadioLegionIntermission15Syndicate: 17, // Nora's Mix: Time Tempests
RadioLegionIntermission14Syndicate: 16, // Nora's Mix: Dreams of the Dead
RadioLegionIntermission13Syndicate: 15, // Nora's Mix Vol. 9
RadioLegionIntermission12Syndicate: 14, // Nora's Mix Vol. 8
RadioLegionIntermission11Syndicate: 13, // Nora's Mix Vol. 7
RadioLegionIntermission10Syndicate: 12, // Nora's Mix Vol. 6
RadioLegionIntermission9Syndicate: 11, // Nora's Mix Vol. 5
RadioLegionIntermission8Syndicate: 10, // Nora's Mix Vol. 4
RadioLegionIntermission7Syndicate: 9, // Nora's Mix Vol. 3
RadioLegionIntermission6Syndicate: 8, // Nora's Mix Vol. 2
RadioLegionIntermission5Syndicate: 7, // Nora's Mix Vol. 1
RadioLegionIntermission4Syndicate: 6, // Nora's Choice
RadioLegionIntermission3Syndicate: 5, // Intermission III
RadioLegion3Syndicate: 4, // Glassmaker
RadioLegionIntermission2Syndicate: 3, // Intermission II
RadioLegion2Syndicate: 2, // The Emissary
RadioLegionIntermissionSyndicate: 1, // Intermission I
RadioLegionSyndicate: 0 // The Wolf of Saturn Six
};
export const nightwaveTagToSeasonName: Record<string, string> = {
RadioLegionIntermission15Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceTwelveSeasonTitle",
RadioLegionIntermission14Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceElevenSeasonTitle",
RadioLegionIntermission13Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceTenSeasonTitle",
RadioLegionIntermission12Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceNineSeasonTitle",
RadioLegionIntermission11Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceEightSeasonTitle",
RadioLegionIntermission10Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceSevenSeasonTitle",
RadioLegionIntermission9Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceSixSeasonTitle",
RadioLegionIntermission8Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceFiveSeasonTitle",
RadioLegionIntermission7Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceFourSeasonTitle",
RadioLegionIntermission6Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceThreeSeasonTitle",
RadioLegionIntermission5Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceTwoSeasonTitle",
RadioLegionIntermission4Syndicate: "/Lotus/Language/Syndicates/RadioLegionNorasChoiceSeasonTitle",
RadioLegionIntermission3Syndicate: "/Lotus/Language/Syndicates/RadioLegionShortSeasonTitle1",
RadioLegion3Syndicate: "/Lotus/Language/Syndicates/RadioLegionSeasonTitle3",
RadioLegionIntermission2Syndicate: "/Lotus/Language/Syndicates/RadioLegionShortSeasonTitle1",
RadioLegion2Syndicate: "/Lotus/Language/Syndicates/RadioLegionSeasonTitle2",
RadioLegionIntermissionSyndicate: "/Lotus/Language/Syndicates/RadioLegionShortSeasonTitle1",
RadioLegionSyndicate: "/Lotus/Language/Syndicates/RadioLegionSeasonTitle1"
};
const nightwaveTagToActivation: Record<string, number> = {
RadioLegionIntermission15Syndicate: 1775662200000,
RadioLegionIntermission14Syndicate: 1761589199000
};
const updateFissures = async (): Promise<void> => {
const fissures = await Fissure.find();
const activeNodes = new Set<string>();
const tierToFurthestExpiry: Record<string, number> = {
VoidT1: 0,
VoidT2: 0,
VoidT3: 0,
VoidT4: 0,
VoidT5: 0,
VoidT6: 0,
VoidT1Hard: 0,
VoidT2Hard: 0,
VoidT3Hard: 0,
VoidT4Hard: 0,
VoidT5Hard: 0,
VoidT6Hard: 0
};
for (const fissure of fissures) {
if (fissure.Expiry.getTime() > Date.now()) {
activeNodes.add(fissure.Node);
}
const key = fissure.Modifier + (fissure.Hard ? "Hard" : "");
tierToFurthestExpiry[key] = Math.max(tierToFurthestExpiry[key], fissure.Expiry.getTime());
}
const deadline = Date.now() - 6 * unixTimesInMs.minute;
for (const [tier, expiry] of Object.entries(tierToFurthestExpiry)) {
if (expiry < deadline) {
const numFissures = getRandomInt(1, 3);
logger.trace(`${tier} fissures expire soon, generating ${numFissures} new ones`);
for (let i = 0; i != numFissures; ++i) {
const modifier = tier.replace("Hard", "") as
| "VoidT1"
| "VoidT2"
| "VoidT3"
| "VoidT4"
| "VoidT5"
| "VoidT6";
let node: string = getRandomElement(fissureMissions[modifier])!;
while (activeNodes.has(node)) {
logger.trace(`tried to use ${node} for a fissure, but it's already in use`);
node = getRandomElement(fissureMissions[modifier])!;
}
activeNodes.add(node);
await Fissure.insertOne({
Activation: new Date(),
Expiry: new Date(Date.now() + getRandomInt(60, 120) * unixTimesInMs.minute),
Node: node,
Modifier: modifier,
Hard: tier.indexOf("Hard") != -1 ? true : undefined
});
}
}
}
};
const updateDailyDeal = async (): Promise<void> => {
let darvoIndex = Math.trunc((Date.now() - 25200000) / (26 * unixTimesInMs.hour));
let darvoEnd;
do {
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
darvoEnd = darvoStart + 26 * unixTimesInMs.hour;
const darvoOid = ((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
if (!(await DailyDeal.findById(darvoOid))) {
const seed = new SRng(darvoIndex).randomInt(0, 100_000);
const rng = new SRng(seed);
let deal;
do {
deal = rng.randomReward(darvoDeals)!; // Using an actual sampling collected over roughly a year because I can't extrapolate an algorithm from it with enough certainty.
//const [storeItem, meta] = rng.randomElement(Object.entries(darvoDeals))!;
//const discount = Math.min(rng.randomInt(1, 9) * 10, (meta as { MaxDiscount?: number }).MaxDiscount ?? 1);
} while (await DailyDeal.exists({ StoreItem: deal.StoreItem }));
await DailyDeal.insertOne({
_id: darvoOid,
StoreItem: deal.StoreItem,
Activation: new Date(darvoStart),
Expiry: new Date(darvoEnd),
Discount: deal.Discount,
OriginalPrice: deal.OriginalPrice,
SalePrice: deal.SalePrice, //Math.trunc(deal.OriginalPrice * (1 - discount))
AmountTotal: deal.AmountTotal,
AmountSold: 0
});
}
} while (darvoEnd < Date.now() + 6 * unixTimesInMs.minute && ++darvoIndex);
};
const alertStandardResources = [
{ path: "/Lotus/Types/Items/MiscItems/AlloyPlate", qty: 1500 },
{ path: "/Lotus/Types/Items/MiscItems/Circuits", qty: 1500 },
{ path: "/Lotus/Types/Items/MiscItems/ControlModule", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Ferrite", qty: 3000 },
{ path: "/Lotus/Types/Items/MiscItems/Gallium", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Morphic", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Nanospores", qty: 3000 },
{ path: "/Lotus/Types/Items/MiscItems/NeuralSensor", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Neurode", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/OrokinCell", qty: 1 },
{ path: "/Lotus/Types/Items/MiscItems/Plastids", qty: 300 },
{ path: "/Lotus/Types/Items/MiscItems/PolymerBundle", qty: 300 },
{ path: "/Lotus/Types/Items/MiscItems/Rubedo", qty: 450 },
{ path: "/Lotus/Types/Items/MiscItems/Salvage", qty: 300 }
];
const alertAuras = [
"/Lotus/Upgrades/Mods/Aura/PlayerEnemyRadarAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerEnergyRegenAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerHealthRegenAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerMeleeAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerPistolAmmoAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerRifleAmmoAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerRifleDamageAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerShellAmmoAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerSniperAmmoAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerHealthAuraMod",
"/Lotus/Upgrades/Mods/Aura/EnemyArmorReductionAuraMod",
"/Lotus/Upgrades/Mods/Aura/EnemyShieldReductionAuraMod",
"/Lotus/Upgrades/Mods/Aura/InfestationSpeedReductionAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerHolsterSpeedAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerSprintAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerSniperDamageAuraMod",
"/Lotus/Upgrades/Mods/Aura/PlayerLootRadarAuraMod",
"/Lotus/Upgrades/Mods/Aura/RobotPoorAimAuraMod"
];
const alertHelmets = [
"/Lotus/Types/Recipes/Helmets/StatlessAshAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessBansheeAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessEmberAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessExcaliburAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessFrostAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessLokiAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessMagAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessNyxAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessRhinoAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessSarynAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessTrinityAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessVoltAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ValkyrBastetHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/OberonAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ZephyrCierzoHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/HarlequinAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/LimboAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/MirageAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2AshAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2BansheeAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2EmberAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2ExcaliburAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2FrostAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2LokiAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2MagAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2NyxAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2RhinoAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2SarynAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2TrinityAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2VoltAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/OberonAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ValkyrAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/PirateAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/LimboAristeasHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ZephyrTenguHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/CowgirlAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/MesaAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/DragonAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/ChromaAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/VaubanHelmetSoldierBlueprint",
"/Lotus/Types/Recipes/Helmets/ExcaliburMordredHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/AnimaAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/RangerAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NezhaAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/SandmanAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/BrawlerAltTwoHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessVaubanAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessV2VaubanAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/StatlessNovaAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NekrosAraknidHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NovaQuantumHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NekrosShroudHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NovaSlipstreamHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/LokiEnigmaHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/PirateAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/BrawlerAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/WukongAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/FairyAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/NidusAltHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/SandmanAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/RangerAltBHelmetBlueprint",
"/Lotus/Types/Recipes/Helmets/BardAltHelmetBlueprint"
];
const alertVaubanParts = [
"/Lotus/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint",
"/Lotus/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint",
"/Lotus/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint"
];
const alertWeapons = [
"/Lotus/Types/Recipes/Weapons/CeramicDaggerBlueprint",
"/Lotus/Types/Recipes/Weapons/DarkDaggerBlueprint",
"/Lotus/Types/Recipes/Weapons/HeatDaggerBlueprint",
"/Lotus/Types/Recipes/Weapons/HeatSwordBlueprint",
"/Lotus/Types/Recipes/Weapons/JawBlueprint",
"/Lotus/Types/Recipes/Weapons/PangolinSwordBlueprint",
"/Lotus/Types/Recipes/Weapons/PlasmaSwordBlueprint",
"/Lotus/Types/Recipes/Weapons/GlaiveBlueprint",
"/Lotus/Types/Recipes/DarkSwordBlueprint",
"/Lotus/Types/Recipes/Weapons/Skins/DaggerAxeBlueprint",
"/Lotus/Types/Recipes/Weapons/Skins/DualDaggerAxeBlueprint",
"/Lotus/Types/Recipes/Weapons/Skins/GrnHammerBlueprint",
"/Lotus/Types/Recipes/Weapons/Skins/GrnAxeBlueprint"
];
const alertNightmareMods = [
"/Lotus/Upgrades/Mods/Pistol/DualStat/StunningSpeedMod",
"/Lotus/Upgrades/Mods/Shotgun/DualStat/BlazeMod",
"/Lotus/Upgrades/Mods/Rifle/DualStat/WildfireMod",
"/Lotus/Upgrades/Mods/Shotgun/DualStat/AcceleratedBlastMod",
"/Lotus/Upgrades/Mods/Warframe/DualStat/ConstitutionMod",
"/Lotus/Upgrades/Mods/Pistol/DualStat/IceStormMod",
"/Lotus/Upgrades/Mods/Warframe/DualStat/FortitudeMod",
"/Lotus/Upgrades/Mods/Rifle/DualStat/HammerShotMod",
"/Lotus/Upgrades/Mods/Melee/DualStat/FocusEnergyMod",
"/Lotus/Upgrades/Mods/Melee/DualStat/RendingStrikeMod",
"/Lotus/Upgrades/Mods/Rifle/DualStat/ShredMod",
"/Lotus/Upgrades/Mods/Pistol/DualStat/GrinderMod",
"/Lotus/Upgrades/Mods/Warframe/DualStat/VigorMod",
"/Lotus/Upgrades/Mods/Warframe/DualStat/RunSpeedArmorMod",
"/Lotus/Upgrades/Mods/Shotgun/DualStat/ReloadSpeedPunchThroughMod"
];
const alertOrokinBP = [
"/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
"/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
"/Lotus/Types/Recipes/Components/FormaBlueprint"
];
const alertDurationMultipliers = new Map<string, number>([
["/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint", 2],
["/Lotus/Types/Recipes/Components/OrokinReactorBlueprint", 2],
["/Lotus/Types/Recipes/Components/FormaBlueprint", 2],
["/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", 2],
["/Lotus/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint", 2],
["/Lotus/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint", 2],
["/Lotus/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint", 2],
["/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", 2],
["/Lotus/Types/Items/MiscItems/Eventium", 2]
]);
const getVersionAppropriateHelmet = (helmetPath: string, buildVersion: number): string => {
let isStore = false;
let typePath = helmetPath;
if (helmetPath.startsWith("/Lotus/StoreItems/")) {
isStore = true;
typePath = "/Lotus/" + helmetPath.substring("/Lotus/StoreItems/".length);
}
const isPreU13_2_3 = buildVersion < gameToBuildVersionInt["13.2.3"]; // arcane helmets were available until U13.2.3
let resultPath = typePath;
if (isPreU13_2_3) {
if (typePath.includes("Statless")) {
const arcanePath = typePath.replace("Statless", "");
if (arcanePath in ExportRecipes) {
resultPath = arcanePath;
}
}
} else {
if (!typePath.includes("Statless")) {
const baseName = typePath.replace("/Lotus/Types/Recipes/Helmets/", "");
const statlessPath = "/Lotus/Types/Recipes/Helmets/Statless" + baseName;
if (statlessPath in ExportRecipes) {
resultPath = statlessPath;
}
}
}
return isStore ? toStoreItem(resultPath) : resultPath;
};
const getEligibleAlertNodes = (regions: Record<string, IRegion>, buildVersion: number): string[] => {
const eligibleNodes: string[] = [];
const validMissionTypes = new Set([
"MT_SURVIVAL",
"MT_DEFENSE",
"MT_RESCUE",
"MT_CAPTURE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_MOBILE_DEFENSE",
"MT_EXCAVATE",
"MT_INTEL",
"MT_TERRITORY",
"MT_PURSUIT",
"MT_SABOTAGE",
"MT_RACE"
]);
const validFactions = new Set(["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION", "FC_CORRUPTED", "FC_OROKIN"]);
for (const [nodeId, nodeData] of Object.entries(regions)) {
if (!isRegionAvailableIn(nodeId, nodeData, buildVersion)) {
continue;
}
if (!nodeId.startsWith("SolNode") && !nodeId.startsWith("SettlementNode")) {
continue;
}
if (nodeData.nodeType !== 0) {
continue;
}
if (!validMissionTypes.has(nodeData.missionType)) {
continue;
}
if (!nodeData.faction || !validFactions.has(nodeData.faction)) {
continue;
}
if (
nodeData.systemIndex == 16 || // Deimos/Derelict
nodeData.systemIndex > 18 // Kuva fortress and more modern things
) {
continue;
}
eligibleNodes.push(nodeId);
}
return eligibleNodes;
};
const ALERT_INTERVAL_MS = 25 * 60 * 1000;
const ALERT_BASE_DURATION_MS = 40 * 60 * 1000;
const alertDescTexts: Record<string, string[] | undefined> = {
MT_ASSASSINATION: [
"/Lotus/Language/Alerts/AssassinationDesc1",
"/Lotus/Language/Alerts/AssassinationDesc9",
"/Lotus/Language/Alerts/AssassinationDesc10",
"/Lotus/Language/Alerts/AssassinationDesc11",
"/Lotus/Language/Alerts/AssassinationDesc14"
],
MT_EXTERMINATION: [
"/Lotus/Language/Alerts/ExterminationDesc3",
"/Lotus/Language/Alerts/ExterminationDesc4",
"/Lotus/Language/Alerts/ExterminationDesc5",
"/Lotus/Language/Alerts/ExterminationDesc6",
"/Lotus/Language/Alerts/ExterminationDesc7",
"/Lotus/Language/Alerts/ExterminationDesc8",
"/Lotus/Language/Alerts/ExterminationDesc9",
"/Lotus/Language/Alerts/ExterminationDesc11",
"/Lotus/Language/Alerts/ExterminationDesc13",
"/Lotus/Language/Alerts/ExterminationDesc14"
],
MT_SURVIVAL: [
"/Lotus/Language/Alerts/RaidDesc15",
"/Lotus/Language/Alerts/RaidDesc19",
"/Lotus/Language/Alerts/RaidDesc22",
"/Lotus/Language/Alerts/RaidDesc23",
"/Lotus/Language/Alerts/DefenseDesc9",
"/Lotus/Language/Alerts/DefenseDesc15"
],
MT_RESCUE: [
"/Lotus/Language/Alerts/RescueDesc1",
"/Lotus/Language/Alerts/RescueDesc2",
"/Lotus/Language/Alerts/RescueDesc3",
"/Lotus/Language/Alerts/RescueDesc4",
"/Lotus/Language/Alerts/RescueDesc5",
"/Lotus/Language/Alerts/RescueDesc6",
"/Lotus/Language/Alerts/RescueDesc7",
"/Lotus/Language/Alerts/RescueDesc8",
"/Lotus/Language/Alerts/RescueDesc9",
"/Lotus/Language/Alerts/RescueDesc10",
"/Lotus/Language/Alerts/RescueDesc11",
"/Lotus/Language/Alerts/RescueDesc12",
"/Lotus/Language/Alerts/RescueDesc14"
],
MT_SABOTAGE: [
"/Lotus/Language/Alerts/SabotageDesc2",
"/Lotus/Language/Alerts/SabotageDesc6",
"/Lotus/Language/Alerts/SabotageDesc7",
"/Lotus/Language/Alerts/SabotageDesc15",
"/Lotus/Language/Alerts/SabotageDesc16",
"/Lotus/Language/Alerts/SabotageDesc17",
"/Lotus/Language/Alerts/SabotageDesc18",
"/Lotus/Language/Alerts/SabotageDesc21",
"/Lotus/Language/Alerts/SabotageDesc23",
"/Lotus/Language/Alerts/SabotageDesc24",
"/Lotus/Language/Alerts/SabotageDesc26",
"/Lotus/Language/Alerts/SabotageDesc28"
],
MT_CAPTURE: [
"/Lotus/Language/Alerts/CaptureDesc1",
"/Lotus/Language/Alerts/CaptureDesc2",
"/Lotus/Language/Alerts/CaptureDesc3",
"/Lotus/Language/Alerts/CaptureDesc4",
"/Lotus/Language/Alerts/CaptureDesc5",
"/Lotus/Language/Alerts/CaptureDesc9",
"/Lotus/Language/Alerts/CaptureDesc10",
"/Lotus/Language/Alerts/CaptureDesc12",
"/Lotus/Language/Alerts/CaptureDesc14"
],
MT_COUNTER_INTEL: [
"/Lotus/Language/Alerts/CounterIntelDesc1",
"/Lotus/Language/Alerts/CounterIntelDesc2",
"/Lotus/Language/Alerts/CounterIntelDesc4",
"/Lotus/Language/Alerts/CounterIntelDesc5",
"/Lotus/Language/Alerts/CounterIntelDesc10",
"/Lotus/Language/Alerts/CounterIntelDesc14",
"/Lotus/Language/Alerts/CounterIntelDesc15",
"/Lotus/Language/Alerts/CounterIntelDesc20",
"/Lotus/Language/Alerts/CounterIntelDesc22",
"/Lotus/Language/Alerts/CounterIntelDesc25"
],
MT_INTEL: [
"/Lotus/Language/Alerts/IntelDesc1",
"/Lotus/Language/Alerts/IntelDesc3",
"/Lotus/Language/Alerts/IntelDesc10",
"/Lotus/Language/Alerts/IntelDesc15",
"/Lotus/Language/Alerts/IntelDesc20",
"/Lotus/Language/Alerts/IntelDesc21",
"/Lotus/Language/Alerts/IntelDesc24",
"/Lotus/Language/Alerts/IntelDesc25",
"/Lotus/Language/Alerts/IntelDesc29",
"/Lotus/Language/Alerts/IntelDesc30",
"/Lotus/Language/Alerts/IntelDesc31",
"/Lotus/Language/Alerts/IntelDesc32",
"/Lotus/Language/Alerts/IntelDesc33",
"/Lotus/Language/Alerts/IntelDesc34",
"/Lotus/Language/Alerts/IntelDesc36",
"/Lotus/Language/Alerts/IntelDesc37",
"/Lotus/Language/Alerts/IntelDesc39",
"/Lotus/Language/Alerts/IntelDesc40",
"/Lotus/Language/Alerts/IntelDesc42",
"/Lotus/Language/Alerts/IntelDesc43",
"/Lotus/Language/Alerts/IntelDesc44",
"/Lotus/Language/Alerts/IntelDesc47"
],
MT_DEFENSE: ["/Lotus/Language/Alerts/DefenseDesc9", "/Lotus/Language/Alerts/DefenseDesc15"],
MT_MOBILE_DEFENSE: [
"/Lotus/Language/Alerts/DefenseDesc2",
"/Lotus/Language/Alerts/DefenseDesc15",
"/Lotus/Language/Alerts/DefenseDesc19"
],
MT_TERRITORY: [
"/Lotus/Language/Alerts/IntelDesc26",
"/Lotus/Language/Alerts/IntelDesc25",
"/Lotus/Language/Alerts/CounterIntelDesc2"
],
MT_RETRIEVAL: [
"/Lotus/Language/Alerts/RaidDesc15",
"/Lotus/Language/Alerts/IntelDesc39",
"/Lotus/Language/Alerts/IntelDesc34"
],
MT_HIVE: ["/Lotus/Language/Alerts/SabotageDesc6"],
MT_EXCAVATE: ["/Lotus/Language/Alerts/IntelDesc4", "/Lotus/Language/Alerts/IntelDesc22"]
};
const getFilteredAlertHelmets = (buildVersion: number): string[] => {
return alertHelmets.filter(helmet => {
if (helmet.includes("BardAlt")) return buildVersion >= gameToBuildVersionInt["20.0.0"];
if (helmet.includes("NidusAlt")) return buildVersion >= gameToBuildVersionInt["19.5.3"];
if (helmet.includes("FairyAlt")) return buildVersion >= gameToBuildVersionInt["19.0.1"];
if (helmet.includes("WukongAlt")) return buildVersion >= gameToBuildVersionInt["18.0.2"];
if (helmet.includes("BrawlerAlt")) return buildVersion >= gameToBuildVersionInt["17.7.1"];
if (helmet.includes("SandmanAlt")) return buildVersion >= gameToBuildVersionInt["18.5.0"];
if (helmet.includes("NezhaAlt")) return buildVersion >= gameToBuildVersionInt["18.0.2"];
if (helmet.includes("RangerAlt")) return buildVersion >= gameToBuildVersionInt["18.0.2"];
if (helmet.includes("AnimaAlt")) return buildVersion >= gameToBuildVersionInt["17.7.1"];
if (helmet.includes("ChromaAlt") || helmet.includes("DragonAlt"))
return buildVersion >= gameToBuildVersionInt["16.0.2"];
if (helmet.includes("MesaAlt") || helmet.includes("CowgirlAlt"))
return buildVersion >= gameToBuildVersionInt["15.5.0"];
if (helmet.includes("LimboAlt") || helmet.includes("LimboAristeas"))
return buildVersion >= gameToBuildVersionInt["15.0.0"];
if (helmet.includes("HarlequinAlt") || helmet.includes("MirageAlt"))
return buildVersion >= gameToBuildVersionInt["14.0.0"];
if (helmet.includes("PirateAlt")) return buildVersion >= gameToBuildVersionInt["13.0.0"];
if (helmet.includes("Zephyr")) return buildVersion >= gameToBuildVersionInt["12.1.2"];
if (helmet.includes("Oberon")) return buildVersion >= gameToBuildVersionInt["11.1.3"];
if (helmet.includes("Valkyr")) return buildVersion >= gameToBuildVersionInt["11.1.3"];
if (helmet.includes("Vauban")) return buildVersion >= gameToBuildVersionInt["8.0.0"];
return true;
});
};
const generateSeededAlert = (
alertIndex: number,
eligibleNodes: string[],
usedNodes: Set<string>,
buildVersion: number,
regions: Record<string, IRegion>
): IAlert | undefined => {
const seed = new SRng(alertIndex * 2654435761).randomInt(0, 100_000);
const rng = new SRng(seed);
const availableNodes = eligibleNodes.filter(n => !usedNodes.has(n));
if (availableNodes.length === 0) return undefined;
const nodeId = rng.randomElement(availableNodes)!;
const nodeData = regions[nodeId];
const isPreU14 = buildVersion < gameToBuildVersionInt["14.0.0"];
const isPostU18 = buildVersion >= gameToBuildVersionInt["18.0.2"];
const isArch = isArchwingMission(nodeData);
const factions: TFaction[] = isArch ? ["FC_GRINEER", "FC_CORPUS"] : ["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION"];
let faction: TFaction = rng.randomInt(0, 9) < 7 ? (nodeData.faction as TFaction) : rng.randomElement(factions)!;
if (faction === "FC_OROKIN" && isPostU18) {
faction = "FC_CORRUPTED" as TFaction;
}
const missionTypes: TMissionType[] = [
"MT_SURVIVAL",
"MT_DEFENSE",
"MT_RESCUE",
"MT_CAPTURE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_MOBILE_DEFENSE",
"MT_INTEL"
];
let missionType: TMissionType;
let levelOverride = nodeData.levelOverride;
let enemySpec = nodeData.enemySpec;
let extraEnemySpec = nodeData.extraEnemySpec;
if (isPreU14) {
missionType = nodeData.missionType;
} else {
missionType = rng.randomInt(0, 9) < 7 ? nodeData.missionType : rng.randomElement(missionTypes)!;
}
if (missionType !== nodeData.missionType) {
levelOverride = undefined;
}
if (isArch) {
missionType = rng.randomElement(
faction == "FC_GRINEER"
? ["MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_TERRITORY", "MT_PURSUIT"]
: ["MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_SABOTAGE", "MT_RACE"]
)!;
}
if (missionType !== nodeData.missionType || faction !== nodeData.faction) {
let foundEnemySpec: string | undefined = undefined;
let foundExtraEnemySpec: string | undefined = undefined;
const searchFaction = (faction as string) === "FC_CORRUPTED" ? "FC_OROKIN" : faction;
if (isArch) {
const targetTileset =
faction == "FC_GRINEER" ? "ArchwingGrineerSpaceTileset" : "ArchwingCorpusTrenchrunTileset";
foundEnemySpec = rng.randomElement(ExportTilesets[targetTileset].missions[missionType]!.enemySpecs!)!;
}
if (!foundEnemySpec) {
const nodes = Object.values(regions).filter(node => node.faction == searchFaction && node.enemySpec);
const node = nodes.find(node => node.missionType == missionType) ?? nodes[0];
foundEnemySpec = node.enemySpec;
foundExtraEnemySpec = node.extraEnemySpec;
}
enemySpec = foundEnemySpec;
extraEnemySpec = foundExtraEnemySpec;
}
const difficulty = rng.randomInt(10, 99) / 100;
const minEnemyLevel = 10 + Math.round(difficulty * 20);
const maxEnemyLevel = minEnemyLevel + rng.randomInt(5, 10);
let rewardCredits = 2000 + Math.round(difficulty * 18000);
let rewardItems: string[] | undefined = undefined;
let rewardCountedItems: { ItemType: string; ItemCount: number }[] | undefined = undefined;
let isNightmare = false;
const filteredStdRes = [...alertStandardResources];
if (buildVersion >= gameToBuildVersionInt["14.0.0"]) {
filteredStdRes.push({ path: "/Lotus/Types/Items/MiscItems/ArgonCrystal", qty: 1 });
}
if (buildVersion >= gameToBuildVersionInt["10.8.0"]) {
filteredStdRes.push({ path: "/Lotus/Types/Items/MiscItems/OxiumAlloy", qty: 300 });
}
if (buildVersion >= gameToBuildVersionInt["15.5.0"]) {
filteredStdRes.push({ path: "/Lotus/Types/Items/MiscItems/Tellurium", qty: 1 });
}
const filteredSpecRes: { path: string; qty: number }[] = [];
if (buildVersion >= gameToBuildVersionInt["14.0.0"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", qty: 1 });
}
if (buildVersion >= gameToBuildVersionInt["18.5.0"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", qty: 5 });
}
if (buildVersion >= gameToBuildVersionInt["14.0.0"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Items/MiscItems/Eventium", qty: 5 });
}
if (buildVersion >= gameToBuildVersionInt["17.12.0"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Items/MiscItems/Alertium", qty: 1 });
}
if (buildVersion >= gameToBuildVersionInt["18.13.3"]) {
filteredSpecRes.push({ path: "/Lotus/Types/Items/MiscItems/VoidTearDrop", qty: 20 });
}
const categories: { name: string; weight: number }[] = [
{ name: "CREDITS", weight: 60 },
{ name: "STANDARD_RESOURCES", weight: 120 },
{ name: "ENDO", weight: 40 },
{ name: "ALT_HELMETS", weight: 80 },
{ name: "WEAPONS", weight: 35 },
{ name: "AURAS", weight: 18 },
{ name: "OROKIN_BP", weight: 4 }
];
if (buildVersion >= gameToBuildVersionInt["9.1.0"]) categories.push({ name: "NIGHTMARE_MODS", weight: 30 });
if (filteredSpecRes.length > 0) categories.push({ name: "SPECIAL_RESOURCES", weight: 10 });
if (buildVersion >= gameToBuildVersionInt["7.11.0"]) {
categories.push({ name: "VAUBAN_PARTS", weight: 7 });
}
let totalWeight = 0;
for (const cat of categories) {
totalWeight += cat.weight;
}
let roll = rng.randomInt(0, totalWeight * 100) / 100;
let selectedCategory = "STANDARD_RESOURCES";
for (const cat of categories) {
if (roll < cat.weight) {
selectedCategory = cat.name;
break;
}
roll -= cat.weight;
}
switch (selectedCategory) {
case "CREDITS": {
rewardCredits = 5000 + Math.round(difficulty * 15000);
break;
}
case "STANDARD_RESOURCES": {
const res = rng.randomElement(filteredStdRes)!;
rewardCountedItems = [{ ItemType: res.path, ItemCount: res.qty }];
break;
}
case "ENDO": {
const isPreU19 = buildVersion < gameToBuildVersionInt["19.0.1"];
const endoRoll = rng.randomInt(0, 99);
if (isPreU19) {
let corePath = "/Lotus/Upgrades/Mods/FusionCores/CoreClassCommonThree";
if (endoRoll < 60) {
corePath = "/Lotus/Upgrades/Mods/FusionCores/CoreClassCommonThree";
} else if (endoRoll < 90) {
corePath = "/Lotus/Upgrades/Mods/FusionCores/CoreClassUncommonFive";
} else {
corePath = "/Lotus/Upgrades/Mods/FusionCores/CoreClassRareFive";
}
rewardItems = [toStoreItem(corePath)];
} else {
let endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleSmall";
if (endoRoll < 60) {
endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleSmall";
} else if (endoRoll < 90) {
endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleMedium";
} else {
endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleLarge";
}
rewardItems = [endoPath];
}
break;
}
case "SPECIAL_RESOURCES": {
const res = rng.randomElement(filteredSpecRes)!;
rewardCountedItems = [{ ItemType: res.path, ItemCount: res.qty }];
break;
}
case "AURAS": {
const aura = rng.randomElement(alertAuras)!;
rewardItems = [toStoreItem(aura)];
break;
}
case "ALT_HELMETS": {
const filteredHelmets = getFilteredAlertHelmets(buildVersion);
const helmet = rng.randomElement(filteredHelmets)!;
rewardItems = [toStoreItem(helmet)];
break;
}
case "VAUBAN_PARTS": {
const part = rng.randomElement(alertVaubanParts)!;
rewardItems = [toStoreItem(part)];
break;
}
case "WEAPONS": {
const weapon = rng.randomElement(alertWeapons)!;
rewardItems = [toStoreItem(weapon)];
break;
}
case "NIGHTMARE_MODS": {
const nmMod = rng.randomElement(alertNightmareMods)!;
rewardItems = [toStoreItem(nmMod)];
isNightmare = true;
break;
}
case "OROKIN_BP": {
const specialBP = rng.randomElement(alertOrokinBP)!;
rewardItems = [toStoreItem(specialBP)];
break;
}
}
let durationMs = ALERT_BASE_DURATION_MS + rng.randomInt(0, 40) * 60 * 1000;
let multiplier = 1;
if (rewardItems && rewardItems.length > 0) {
multiplier = alertDurationMultipliers.get(fromStoreItem(rewardItems[0])) ?? 1;
} else if (rewardCountedItems && rewardCountedItems.length > 0) {
multiplier = alertDurationMultipliers.get(rewardCountedItems[0].ItemType) ?? 1;
}
durationMs = Math.round(durationMs * multiplier);
const activation = EPOCH + alertIndex * ALERT_INTERVAL_MS;
const expiry = activation + durationMs;
const oid =
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"a1e4" +
seed.toString(16).padStart(5, "0").substring(0, 5) +
(alertIndex & 0xffffff).toString(16).padStart(7, "0");
const descTexts = alertDescTexts[missionType];
const descText = isPreU14 && descTexts ? rng.randomElement(descTexts) : undefined;
if (buildVersion < gameToBuildVersionInt["14.0.0"] && missionType === "MT_EXCAVATE") {
return undefined;
}
const alert: IAlert = {
_id: toOid2(oid, buildVersion),
Activation: toMongoDate2(activation, buildVersion),
Expiry: toMongoDate2(expiry, buildVersion),
MissionInfo: {
location: nodeId,
missionType: missionType,
faction: faction,
difficulty: difficulty,
missionReward: {
credits: rewardCredits,
items: rewardItems,
countedItems: rewardCountedItems
},
levelOverride: levelOverride,
enemySpec: enemySpec,
extraEnemySpec: extraEnemySpec,
minEnemyLevel: minEnemyLevel,
maxEnemyLevel: maxEnemyLevel,
descText: descText,
nightmare: isNightmare || undefined,
archwingRequired: isArchwingMission(nodeData) || undefined
}
};
return alert;
};
export const populateAlerts = async (worldState: IWorldState): Promise<void> => {
const buildLabel = worldState.BuildLabel;
const buildVersion = buildVersionToInt(buildLabel);
if (
config.worldState?.classicAlerts ??
(buildVersion >= gameToBuildVersionInt["5.1.0"] && buildVersion < gameToBuildVersionInt["24.3.0"]) // alerts were retired with U24.3.0
) {
const regions = await getRegions(buildLabel);
const validNodes = getEligibleAlertNodes(regions, buildVersion);
if (validNodes.length === 0) return;
const timeMs = worldState.Time * 1000;
const currentAlertIndex = Math.floor((timeMs - EPOCH) / ALERT_INTERVAL_MS);
const activeAlerts: IAlert[] = [];
const usedNodes = new Set<string>();
for (let idx = currentAlertIndex - 8; idx <= currentAlertIndex + 1; idx++) {
const alert = generateSeededAlert(idx, validNodes, usedNodes, buildVersion, regions);
if (alert) {
const activationTime =
"$date" in alert.Activation
? Number(alert.Activation.$date.$numberLong)
: alert.Activation.sec * 1000;
const expiryTime =
"$date" in alert.Expiry ? Number(alert.Expiry.$date.$numberLong) : alert.Expiry.sec * 1000;
if (timeMs >= activationTime && timeMs < expiryTime) {
usedNodes.add(alert.MissionInfo.location);
const isPreU14 = buildVersion < gameToBuildVersionInt["14.0.0"];
if (alert.MissionInfo.missionReward) {
if (alert.MissionInfo.missionReward.items) {
alert.MissionInfo.missionReward.items = alert.MissionInfo.missionReward.items.map(item => {
let processedItem = item;
if (processedItem.includes("/Recipes/Helmets/")) {
processedItem = getVersionAppropriateHelmet(processedItem, buildVersion);
}
if (isPreU14) {
const parts = processedItem.split("/");
const filename = parts[parts.length - 1];
if (processedItem.includes("/Recipes/WarframeRecipes/")) {
return `/Lotus/Types/Recipes/WarframeRecipes/${filename}StoreItem`;
}
const isPreU13 = buildVersion < gameToBuildVersionInt["13.0.0"];
if (filename === "FormaBlueprint") {
if (isPreU13) {
return "/Lotus/Types/StoreItems/Recipes/OrokinCatalystBlueprintStoreItem";
}
return "/Lotus/Types/Recipes/Components/FormaBlueprintStoreItem";
}
if (filename === "Forma") {
if (isPreU13) {
return "/Lotus/Types/StoreItems/Recipes/OrokinCatalystStoreItem";
}
return "/Lotus/Types/Recipes/Components/FormaStoreItem";
}
if (filename === "OrokinReactorBlueprint") {
if (isPreU13) {
return "/Lotus/Types/StoreItems/Recipes/OrokinReactorBlueprintStoreItem";
}
return "/Lotus/Types/Recipes/Components/OrokinReactorBlueprintStoreItem";
}
if (filename === "OrokinCatalystBlueprint") {
return "/Lotus/Types/StoreItems/Recipes/OrokinCatalystBlueprintStoreItem";
}
if (filename.endsWith("Blueprint")) {
return `/Lotus/Types/StoreItems/Recipes/${filename}StoreItem`;
}
return processedItem;
}
return processedItem;
});
}
if (alert.MissionInfo.missionReward.countedItems) {
if (isPreU14) {
alert.MissionInfo.missionReward.countedItems =
alert.MissionInfo.missionReward.countedItems.map(ci => {
if (ci.ItemType === "/Lotus/Types/Items/MiscItems/ArgonCrystal") {
return {
ItemType: "/Lotus/Types/Items/MiscItems/OrokinCell",
ItemCount: ci.ItemCount
};
}
if (ci.ItemType === "/Lotus/Types/Items/MiscItems/OxiumAlloy") {
return {
ItemType: "/Lotus/Types/Items/MiscItems/Gallium",
ItemCount: Math.max(1, Math.round(ci.ItemCount / 10))
};
}
if (ci.ItemType === "/Lotus/Types/Items/MiscItems/Tellurium") {
return {
ItemType: "/Lotus/Types/Items/MiscItems/Morphic",
ItemCount: ci.ItemCount
};
}
if (ci.ItemType === "/Lotus/Types/Items/MiscItems/Alertium") {
return {
ItemType: "/Lotus/Types/Items/MiscItems/Neurode",
ItemCount: ci.ItemCount
};
}
return ci;
});
}
}
}
activeAlerts.push(alert);
}
}
}
worldState.Alerts.push(...activeAlerts);
}
};
export const updateWorldStateCollections = async (): Promise<void> => {
await Promise.all([updateFissures(), updateDailyDeal()]);
};
const pushConclaveDaily = (
activeChallenges: IPVPChallengeInstance[],
PVPMode: string,
pool: {
key: string;
ScriptParamValue: number;
PVPModeAllowed: string[];
SyndicateXP: number;
DuringSingleMatch?: boolean;
}[],
day: number,
id: number,
buildVersion: number
): void => {
if (buildVersion < gameToBuildVersionInt["18.0.2"]) {
PVPMode = PVPMode.replace("PVPMODE_", "");
}
const conclaveDayStart = EPOCH + day * unixTimesInMs.day + 5 * unixTimesInMs.hour + 30 * unixTimesInMs.minute;
const conclaveDayEnd = conclaveDayStart + unixTimesInMs.day;
const challengeId = day * 8 + id;
const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
let challenge: {
key: string;
ScriptParamValue: number;
PVPModeAllowed?: string[];
SyndicateXP?: number;
DuringSingleMatch?: boolean;
};
do {
challenge = rng.randomElement(pool)!;
} while (
activeChallenges.some(x => x.challengeTypeRefID == challenge.key) &&
activeChallenges.some(x => x.PVPMode == PVPMode)
);
activeChallenges.push({
_id: toOid2("689ec5d985b55902" + challengeId.toString().padStart(8, "0"), buildVersion),
challengeTypeRefID: challenge.key,
startDate: toMongoDate2(conclaveDayStart, buildVersion),
endDate: toMongoDate2(conclaveDayEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: challenge.ScriptParamValue }],
isGenerated: true,
PVPMode,
subChallenges: [],
Category: "PVPChallengeTypeCategory_DAILY"
});
};
const pushConclaveDailys = (activeChallenges: IPVPChallengeInstance[], day: number, buildVersion: number): void => {
const modes = ["PVPMODE_CAPTURETHEFLAG", "PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"];
// closest known version to Update: Lunaro
if (buildVersion > gameToBuildVersionInt["18.13.3"]) {
modes.push("PVPMODE_SPEEDBALL");
}
const challengesMap: Record<
string,
{
key: string;
ScriptParamValue: number;
PVPModeAllowed: string[];
SyndicateXP: number;
DuringSingleMatch?: boolean;
}[]
> = {};
for (const mode of modes) {
challengesMap[mode] = Object.entries(pvpChallenges)
.filter(([_, challenge]) => challenge.PVPModeAllowed.includes(mode))
.map(([key, challenge]) => ({ key, ...challenge }));
}
modes.forEach((mode, index) => {
pushConclaveDaily(activeChallenges, mode, challengesMap[mode], day, index * 2, buildVersion);
pushConclaveDaily(activeChallenges, mode, challengesMap[mode], day, index * 2 + 1, buildVersion);
});
};
const pushConclaveWeekly = (activeChallenges: IPVPChallengeInstance[], week: number, buildVersion: number): void => {
const weekStart = EPOCH + week * unixTimesInMs.week;
const conclaveWeekStart = weekStart + 40 * unixTimesInMs.minute - 2 * unixTimesInMs.day;
const conclaveWeekEnd = conclaveWeekStart + unixTimesInMs.week;
const conclaveIdStart = ((conclaveWeekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0").padEnd(23, "0");
activeChallenges.push(
{
_id: toOid2(conclaveIdStart + "1", buildVersion),
challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeGameModeWins",
startDate: toMongoDate2(conclaveWeekStart, buildVersion),
endDate: toMongoDate2(conclaveWeekEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: 6 }],
isGenerated: true,
PVPMode: "PVPMODE_ALL",
subChallenges: [],
Category: "PVPChallengeTypeCategory_WEEKLY"
},
{
_id: toOid2(conclaveIdStart + "2", buildVersion),
challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeGameModeComplete",
startDate: toMongoDate2(conclaveWeekStart, buildVersion),
endDate: toMongoDate2(conclaveWeekEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: 20 }],
isGenerated: true,
PVPMode: "PVPMODE_ALL",
subChallenges: [],
Category: "PVPChallengeTypeCategory_WEEKLY"
},
{
_id: toOid2(conclaveIdStart + "3", buildVersion),
challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeOtherChallengeCompleteANY",
startDate: toMongoDate2(conclaveWeekStart, buildVersion),
endDate: toMongoDate2(conclaveWeekEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: 10 }],
isGenerated: true,
PVPMode: "PVPMODE_ALL",
subChallenges: [],
Category: "PVPChallengeTypeCategory_WEEKLY"
},
{
_id: toOid2(conclaveIdStart + "4", buildVersion),
challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeWeeklyStandardSet",
startDate: toMongoDate2(conclaveWeekStart, buildVersion),
endDate: toMongoDate2(conclaveWeekEnd, buildVersion),
params: [{ n: "ScriptParamValue", v: 0 }],
isGenerated: true,
PVPMode: "PVPMODE_NONE",
subChallenges: [
toOid2(conclaveIdStart + "1", buildVersion),
toOid2(conclaveIdStart + "2", buildVersion),
toOid2(conclaveIdStart + "3", buildVersion)
],
Category: "PVPChallengeTypeCategory_WEEKLY_ROOT"
}
);
};
const pushGoalAlerts = (ws: IWorldState, tag: string, buildVersion: number): void => {
const genMeta = alertGeneratorConfig[tag];
const neededAlerts = genMeta.alertLength / genMeta.alertInterval;
const currentAlertIndex = Math.floor((ws.Time * 1000 - EPOCH) / genMeta.alertInterval);
for (let i = 0; i <= neededAlerts; i++) {
ws.Alerts.push(generateGoalAlert(tag, currentAlertIndex - i, ws.Alerts, buildVersion));
}
};
const generateGoalAlert = (tag: string, alertIdx: number, wsAlerts: IAlert[], buildVersion: number): IAlert => {
const genMeta = alertGeneratorConfig[tag];
const defenseWavesPerRotation = buildVersion < gameToBuildVersionInt["38.5.0"] ? 5 : 3;
const activation = EPOCH + alertIdx * genMeta.alertInterval;
const expiry = activation + genMeta.alertLength;
const tagHash = catBreadHash(tag);
const rng = new SRng(alertIdx ^ tagHash);
const allowedNodes = Object.fromEntries(
Object.entries(ExportRegions).filter(
([nodeTag, node]) =>
genMeta.allowedSystemIndexes.includes(node.systemIndex) &&
node.nodeType != 3 && // not hub
node.nodeType != 7 && // not junction
!["MT_ASSASSINATION", "MT_LANDSCAPE", "MT_RAILJACK", "MT_PVPVE", "MT_PVP", "MT_ARENA"].includes(
node.missionType
) &&
node.name.indexOf("1999NodeI") == -1 && // not stage defense
node.name.indexOf("1999NodeJ") == -1 && // not lich bounty
node.name.indexOf("SolarMapEntratiNode") == -1 && // not albrecht lab mission
!isArchwingMission(node) &&
!wsAlerts.map(a => a.MissionInfo.location).includes(nodeTag)
)
);
let nodeTag: string;
let node: IRegion;
do {
[nodeTag, node] = rng.randomElement(Object.entries(allowedNodes))!;
if (!node.tileset) logger.debug(`node ${nodeTag} without tileset - reroll it`);
else if (!node.faction) logger.debug(`node ${nodeTag} without faction - reroll it`);
} while (!node.tileset || !node.faction);
let missionType: string;
let mission: ITilesetMission;
do {
[missionType, mission] = rng.randomElement(Object.entries(ExportTilesets[node.tileset].missions))!;
} while (
["MT_ASSASSINATION", "MT_LANDSCAPE", "MT_RAILJACK", "MT_PVPVE", "MT_PVP", "MT_ARENA"].includes(missionType)
);
const oid =
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
tagHash.toString(16).padEnd(8, "0") +
(alertIdx & 0xffffffff).toString(16).padStart(8, "0");
const alert: IAlert = {
_id: { $oid: oid },
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
MissionInfo: {
location: nodeTag,
missionType: missionType as TMissionType,
faction: node.faction,
difficulty: 1,
seed: alertIdx,
levelOverride: mission.procLevel,
...genMeta.baseMissionInfo
},
...genMeta.baseAlert
};
if (mission.enemySpecs) alert.MissionInfo.enemySpec = rng.randomElement(mission.enemySpecs);
if (mission.extraEnemySpecs) alert.MissionInfo.extraEnemySpec = rng.randomElement(mission.extraEnemySpecs);
if (mission.vipAgent) alert.MissionInfo.vipAgent = mission.vipAgent;
if (mission.advancedSpawners) alert.MissionInfo.customAdvancedSpawners = mission.advancedSpawners;
if (["MT_INTEL", "MT_TERRITORY", "MT_EXCAVATE"].includes(missionType)) {
alert.MissionInfo.maxWaveNum = 2;
} else if (missionType == "MT_SURVIVAL") {
alert.MissionInfo.maxWaveNum = 5 * genMeta.baseMissionInfo.maxRotations!;
} else if (missionType == "MT_DEFENSE") {
alert.MissionInfo.maxWaveNum = defenseWavesPerRotation * genMeta.baseMissionInfo.maxRotations!;
}
delete alert.MissionInfo.maxRotations;
return alert;
};
export const populateFeaturedGuilds = async (worldState: IWorldState): Promise<void> => {
const guilds = await Guild.find({ Featured: true }, "Name Tier AllianceId Emblem");
for (const guild of guilds) {
worldState.FeaturedGuilds.push({
_id: toOid(guild._id),
Name: guild.Name,
Tier: guild.Tier,
AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined,
Emblem: guild.Emblem
});
}
};
const pushFlashSales = (
ws: IWorldState,
storeItems: IFlashSaleData[],
startDate: number | Date,
endDate: number | Date,
category: string,
buildVersion: number
): void => {
const filteredItems = storeItems.filter(
item => !item.minBuildVersionInt || buildVersion >= item.minBuildVersionInt
);
ws.FlashSales.push(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
...filteredItems.map(({ minBuildVersionInt, ...item }) => ({
...item,
StartDate: toMongoDate2(startDate, buildVersion),
EndDate: toMongoDate2(endDate, buildVersion),
ProductExpiryOverride: toMongoDate2(endDate, buildVersion),
// Defaulting all of this is probably not needed, but might be needed for Market 2.0 (pre-U25):
ShowInMarket: item.ShowInMarket ?? true,
HideFromMarket: item.HideFromMarket ?? false,
SupporterPack: item.SupporterPack ?? false,
Discount: item.Discount ?? 0,
BogoBuy: item.BogoBuy ?? 0,
BogoGet: item.BogoGet ?? 0,
RegularOverride: item.RegularOverride ?? 0,
PremiumOverride: item.PremiumOverride ?? 0,
Featured: item.Featured ?? category != "POPULAR",
Popular: item.Popular ?? category == "POPULAR",
BannerIndex: item.BannerIndex ?? 10
}))
);
const seasonalCategory = ws.InGameMarket.LandingPage.Categories.find(c => c.CategoryName == category);
if (!seasonalCategory) {
throw new Error(`No market category ${category} in static worldState data`);
}
seasonalCategory.Items!.push(...filteredItems.map(x => x.TypeName));
};