2025-04-10 12:54:48 -07:00
import staticWorldState from "@/static/fixed_responses/worldState/worldState.json" ;
2025-04-23 11:36:32 -07:00
import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json" ;
2025-05-07 20:14:05 -07:00
import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json" ;
2025-04-26 19:27:50 -07:00
import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json" ;
2025-04-10 12:54:48 -07:00
import { buildConfig } from "@/src/services/buildConfigService" ;
import { unixTimesInMs } from "@/src/constants/timeConstants" ;
import { config } from "@/src/services/configService" ;
2025-05-09 21:36:22 -07:00
import { SRng } from "@/src/services/rngService" ;
2025-05-23 06:12:54 -07:00
import { ExportRegions , ExportSyndicates , IRegion } from "warframe-public-export-plus" ;
2025-04-23 11:36:32 -07:00
import {
ICalendarDay ,
2025-04-25 11:53:04 -07:00
ICalendarEvent ,
2025-04-23 11:36:32 -07:00
ICalendarSeason ,
ILiteSortie ,
ISeasonChallenge ,
2025-05-06 02:29:11 -07:00
ISortie ,
2025-04-23 11:36:32 -07:00
ISortieMission ,
2025-05-06 19:05:23 -07:00
ISyndicateMissionInfo ,
2025-06-16 14:55:35 -07:00
IVoidStorm ,
2025-04-23 11:36:32 -07:00
IWorldState
} from "../types/worldStateTypes" ;
2025-05-09 00:20:54 -07:00
import { version_compare } from "../helpers/inventoryHelpers" ;
2025-06-09 03:37:42 -07:00
import { logger } from "../utils/logger" ;
2025-04-10 12:54:48 -07:00
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"
2025-04-30 13:28:34 -07:00
] as const ;
type TSortieBoss = ( typeof sortieBosses ) [ number ] ;
2025-04-10 12:54:48 -07:00
2025-04-30 13:28:34 -07:00
const sortieBossToFaction : Record < TSortieBoss , string > = {
2025-04-10 12:54:48 -07:00
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" ,
2025-04-26 11:56:41 -07:00
SORTIE_BOSS_CORRUPTED_VOR : "FC_OROKIN"
2025-04-10 12:54:48 -07:00
} ;
const sortieFactionToSystemIndexes : Record < string , number [ ] > = {
FC_GRINEER : [ 0 , 2 , 3 , 5 , 6 , 9 , 11 , 18 ] ,
FC_CORPUS : [ 1 , 4 , 7 , 8 , 12 , 15 ] ,
FC_INFESTATION : [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 15 ] ,
2025-04-26 11:56:41 -07:00
FC_OROKIN : [ 14 ]
2025-04-10 12:54:48 -07:00
} ;
const sortieFactionToFactionIndexes : Record < string , number [ ] > = {
FC_GRINEER : [ 0 ] ,
FC_CORPUS : [ 1 ] ,
FC_INFESTATION : [ 0 , 1 , 2 ] ,
2025-04-26 11:56:41 -07:00
FC_OROKIN : [ 3 ]
} ;
2025-04-30 13:28:34 -07:00
const sortieBossNode : Record < Exclude < TSortieBoss , "SORTIE_BOSS_CORRUPTED_VOR" > , string > = {
SORTIE_BOSS_ALAD : "SolNode53" ,
SORTIE_BOSS_AMBULAS : "SolNode51" ,
SORTIE_BOSS_HEK : "SolNode24" ,
2025-04-10 12:54:48 -07:00
SORTIE_BOSS_HYENA : "SolNode127" ,
2025-04-30 13:28:34 -07:00
SORTIE_BOSS_INFALAD : "SolNode166" ,
SORTIE_BOSS_JACKAL : "SolNode104" ,
2025-04-10 12:54:48 -07:00
SORTIE_BOSS_KELA : "SolNode193" ,
SORTIE_BOSS_KRIL : "SolNode99" ,
2025-04-30 13:28:34 -07:00
SORTIE_BOSS_LEPHANTIS : "SolNode712" ,
2025-04-10 12:54:48 -07:00
SORTIE_BOSS_NEF : "SettlementNode20" ,
2025-04-30 13:28:34 -07:00
SORTIE_BOSS_PHORID : "SolNode171" ,
2025-04-10 12:54:48 -07:00
SORTIE_BOSS_RAPTOR : "SolNode210" ,
2025-04-30 13:28:34 -07:00
SORTIE_BOSS_RUK : "SolNode32" ,
SORTIE_BOSS_TYL : "SolNode105" ,
SORTIE_BOSS_VOR : "SolNode108"
2025-04-10 12:54:48 -07:00
} ;
2025-04-15 20:41:46 -07:00
const eidolonJobs = [
"/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 = [
"/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 venusJobs = [
"/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 = [
"/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 = [
"/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 = [
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty" ,
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty" ,
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
2025-04-11 06:56:31 -07:00
] ;
2025-04-10 12:54:48 -07:00
const EPOCH = 1734307200 * 1000 ; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
2025-06-07 16:46:45 -07:00
const isBeforeNextExpectedWorldStateRefresh = ( nowMs : number , thenMs : number ) : boolean = > {
return nowMs + 300 _000 > thenMs ;
2025-04-15 20:53:27 -07:00
} ;
2025-04-10 12:54:48 -07:00
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 ;
} ;
2025-04-22 10:00:49 -07:00
const pushSyndicateMissions = (
worldState : IWorldState ,
day : number ,
seed : number ,
idSuffix : string ,
syndicateTag : string
) : void = > {
2025-04-26 19:27:50 -07:00
const nodeOptions : string [ ] = [ . . . syndicateMissions ] ;
2025-04-22 10:00:49 -07:00
2025-05-09 21:36:22 -07:00
const rng = new SRng ( seed ) ;
2025-04-22 10:00:49 -07:00
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 ) ;
}
2025-05-06 02:29:11 -07:00
const dayStart = getSortieTime ( day ) ;
const dayEnd = getSortieTime ( day + 1 ) ;
2025-04-22 10:00:49 -07:00
worldState . SyndicateMissions . push ( {
2025-04-25 18:55:09 -07:00
_id : { $oid : ( ( dayStart / 1000 ) & 0xffffffff ) . toString ( 16 ) . padStart ( 8 , "0" ) + idSuffix } ,
2025-04-22 10:00:49 -07:00
Activation : { $date : { $numberLong : dayStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : dayEnd.toString ( ) } } ,
Tag : syndicateTag ,
Seed : seed ,
Nodes : nodes
} ) ;
} ;
2025-05-07 20:14:05 -07:00
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 ;
}
} ;
2025-05-06 02:29:11 -07:00
export const getSortie = ( day : number ) : ISortie = > {
2025-05-09 21:36:22 -07:00
const seed = new SRng ( day ) . randomInt ( 0 , 100 _000 ) ;
const rng = new SRng ( seed ) ;
2025-04-15 20:53:27 -07:00
2025-04-29 12:27:25 -07:00
const boss = rng . randomElement ( sortieBosses ) ! ;
2025-04-15 20:53:27 -07:00
const nodes : string [ ] = [ ] ;
for ( const [ key , value ] of Object . entries ( ExportRegions ) ) {
if (
sortieFactionToSystemIndexes [ sortieBossToFaction [ boss ] ] . includes ( value . systemIndex ) &&
sortieFactionToFactionIndexes [ sortieBossToFaction [ boss ] ] . includes ( value . factionIndex ! ) &&
2025-04-26 11:56:41 -07:00
key in sortieTilesets
2025-04-15 20:53:27 -07:00
) {
nodes . push ( key ) ;
}
}
2025-04-23 11:36:32 -07:00
const selectedNodes : ISortieMission [ ] = [ ] ;
2025-04-15 20:53:27 -07:00
const missionTypes = new Set ( ) ;
for ( let i = 0 ; i < 3 ; i ++ ) {
2025-05-07 20:14:05 -07:00
const randomIndex = rng . randomInt ( 0 , nodes . length - 1 ) ;
const node = nodes [ randomIndex ] ;
2025-04-15 20:53:27 -07:00
2025-05-01 13:51:31 -07:00
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"
] ;
2025-04-30 13:28:34 -07:00
if ( i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng . randomInt ( 0 , 2 ) == 2 ) {
2025-05-07 20:14:05 -07:00
const tileset = sortieTilesets [ sortieBossNode [ boss ] as keyof typeof sortieTilesets ] as TSortieTileset ;
pushTilesetModifiers ( modifiers , tileset ) ;
2025-05-02 15:07:00 -07:00
2025-05-01 13:51:31 -07:00
const modifierType = rng . randomElement ( modifiers ) ! ;
2025-04-15 20:53:27 -07:00
2025-04-30 13:28:34 -07:00
selectedNodes . push ( {
missionType : "MT_ASSASSINATION" ,
modifierType ,
node : sortieBossNode [ boss ] ,
2025-05-02 15:07:00 -07:00
tileset
2025-04-30 13:28:34 -07:00
} ) ;
continue ;
2025-04-15 20:53:27 -07:00
}
2025-05-07 20:14:05 -07:00
const tileset = sortieTilesets [ node as keyof typeof sortieTilesets ] as TSortieTileset ;
pushTilesetModifiers ( modifiers , tileset ) ;
2025-04-15 20:53:27 -07:00
2025-05-07 20:14:05 -07:00
const missionType = rng . randomElement ( sortieTilesetMissions [ tileset ] ) ! ;
if ( missionTypes . has ( missionType ) || missionType == "MT_ASSASSINATION" ) {
2025-04-15 20:53:27 -07:00
i -- ;
continue ;
}
2025-05-01 13:51:31 -07:00
modifiers . push ( "SORTIE_MODIFIER_MELEE_ONLY" ) ; // not an assassination mission, can now push this
if ( missionType != "MT_TERRITORY" ) {
modifiers . push ( "SORTIE_MODIFIER_HAZARD_RADIATION" ) ;
}
if ( ExportRegions [ node ] . factionIndex == 0 ) {
// Grineer
modifiers . push ( "SORTIE_MODIFIER_ARMOR" ) ;
} else if ( ExportRegions [ node ] . factionIndex == 1 ) {
// Corpus
modifiers . push ( "SORTIE_MODIFIER_SHIELDS" ) ;
}
2025-04-15 20:53:27 -07:00
2025-05-01 13:51:31 -07:00
const modifierType = rng . randomElement ( modifiers ) ! ;
2025-04-15 20:53:27 -07:00
2025-04-23 11:36:32 -07:00
selectedNodes . push ( {
missionType ,
modifierType ,
node ,
2025-05-02 15:07:00 -07:00
tileset
2025-04-23 11:36:32 -07:00
} ) ;
2025-04-15 20:53:27 -07:00
nodes . splice ( randomIndex , 1 ) ;
missionTypes . add ( missionType ) ;
}
2025-05-06 02:29:11 -07:00
const dayStart = getSortieTime ( day ) ;
const dayEnd = getSortieTime ( day + 1 ) ;
return {
2025-04-25 18:55:09 -07:00
_id : { $oid : ( ( dayStart / 1000 ) & 0xffffffff ) . toString ( 16 ) . padStart ( 8 , "0" ) + "d4d932c97c0a3acd" } ,
2025-04-15 20:53:27 -07:00
Activation : { $date : { $numberLong : dayStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : dayEnd.toString ( ) } } ,
Reward : "/Lotus/Types/Game/MissionDecks/SortieRewards" ,
2025-04-24 11:24:11 -07:00
Seed : seed ,
2025-04-15 20:53:27 -07:00
Boss : boss ,
Variants : selectedNodes
2025-05-06 02:29:11 -07:00
} ;
2025-04-15 20:53:27 -07:00
} ;
2025-05-23 06:12:54 -07:00
interface IRotatingSeasonChallengePools {
daily : string [ ] ;
weekly : string [ ] ;
hardWeekly : string [ ] ;
2025-06-07 02:16:19 -07:00
hasWeeklyPermanent : boolean ;
2025-05-23 06:12:54 -07:00
}
2025-04-10 12:54:48 -07:00
2025-05-23 06:12:54 -07:00
const getSeasonChallengePools = ( syndicateTag : string ) : IRotatingSeasonChallengePools = > {
const syndicate = ExportSyndicates [ 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" )
) ,
2025-06-07 02:16:19 -07:00
hardWeekly : syndicate.weeklyChallenges ! . filter ( x = >
x . startsWith ( "/Lotus/Types/Challenges/Seasons/WeeklyHard/" )
) ,
hasWeeklyPermanent : ! ! syndicate . weeklyChallenges ! . find ( x = >
x . startsWith ( "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent" )
)
2025-05-23 06:12:54 -07:00
} ;
} ;
const getSeasonDailyChallenge = ( pools : IRotatingSeasonChallengePools , day : number ) : ISeasonChallenge = > {
2025-04-10 12:54:48 -07:00
const dayStart = EPOCH + day * 86400000 ;
const dayEnd = EPOCH + ( day + 3 ) * 86400000 ;
2025-05-09 21:36:22 -07:00
const rng = new SRng ( new SRng ( day ) . randomInt ( 0 , 100 _000 ) ) ;
2025-04-10 12:54:48 -07:00
return {
_id : { $oid : "67e1b5ca9d00cb47" + day . toString ( ) . padStart ( 8 , "0" ) } ,
Daily : true ,
Activation : { $date : { $numberLong : dayStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : dayEnd.toString ( ) } } ,
2025-05-23 06:12:54 -07:00
Challenge : rng.randomElement ( pools . daily ) !
2025-04-10 12:54:48 -07:00
} ;
} ;
2025-05-23 06:12:54 -07:00
const getSeasonWeeklyChallenge = ( pools : IRotatingSeasonChallengePools , week : number , id : number ) : ISeasonChallenge = > {
2025-04-10 12:54:48 -07:00
const weekStart = EPOCH + week * 604800000 ;
const weekEnd = weekStart + 604800000 ;
const challengeId = week * 7 + id ;
2025-05-09 21:36:22 -07:00
const rng = new SRng ( new SRng ( challengeId ) . randomInt ( 0 , 100 _000 ) ) ;
2025-04-10 12:54:48 -07:00
return {
_id : { $oid : "67e1bb2d9d00cb47" + challengeId . toString ( ) . padStart ( 8 , "0" ) } ,
Activation : { $date : { $numberLong : weekStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : weekEnd.toString ( ) } } ,
2025-05-23 06:12:54 -07:00
Challenge : rng.randomElement ( pools . weekly ) !
2025-04-10 12:54:48 -07:00
} ;
} ;
2025-05-23 06:12:54 -07:00
const getSeasonWeeklyHardChallenge = (
pools : IRotatingSeasonChallengePools ,
week : number ,
id : number
) : ISeasonChallenge = > {
2025-04-10 12:54:48 -07:00
const weekStart = EPOCH + week * 604800000 ;
const weekEnd = weekStart + 604800000 ;
const challengeId = week * 7 + id ;
2025-05-09 21:36:22 -07:00
const rng = new SRng ( new SRng ( challengeId ) . randomInt ( 0 , 100 _000 ) ) ;
2025-04-10 12:54:48 -07:00
return {
_id : { $oid : "67e1bb2d9d00cb47" + challengeId . toString ( ) . padStart ( 8 , "0" ) } ,
Activation : { $date : { $numberLong : weekStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : weekEnd.toString ( ) } } ,
2025-05-23 06:12:54 -07:00
Challenge : rng.randomElement ( pools . hardWeekly ) !
2025-04-10 12:54:48 -07:00
} ;
} ;
2025-05-23 06:12:54 -07:00
const pushWeeklyActs = (
activeChallenges : ISeasonChallenge [ ] ,
pools : IRotatingSeasonChallengePools ,
week : number
) : void = > {
2025-04-19 09:06:49 -07:00
const weekStart = EPOCH + week * 604800000 ;
const weekEnd = weekStart + 604800000 ;
2025-05-23 06:12:54 -07:00
activeChallenges . push ( getSeasonWeeklyChallenge ( pools , week , 0 ) ) ;
activeChallenges . push ( getSeasonWeeklyChallenge ( pools , week , 1 ) ) ;
2025-06-07 02:16:19 -07:00
if ( pools . hasWeeklyPermanent ) {
activeChallenges . push ( {
_id : { $oid : "67e1b96e9d00cb47" + ( week * 7 + 0 ) . toString ( ) . padStart ( 8 , "0" ) } ,
Activation : { $date : { $numberLong : weekStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : weekEnd.toString ( ) } } ,
Challenge : "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions"
} ) ;
activeChallenges . push ( {
_id : { $oid : "67e1b96e9d00cb47" + ( week * 7 + 1 ) . toString ( ) . padStart ( 8 , "0" ) } ,
Activation : { $date : { $numberLong : weekStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : weekEnd.toString ( ) } } ,
Challenge : "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus"
} ) ;
activeChallenges . push ( {
_id : { $oid : "67e1b96e9d00cb47" + ( week * 7 + 2 ) . toString ( ) . padStart ( 8 , "0" ) } ,
Activation : { $date : { $numberLong : weekStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : weekEnd.toString ( ) } } ,
Challenge : "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies"
} ) ;
activeChallenges . push ( getSeasonWeeklyHardChallenge ( pools , week , 2 ) ) ;
activeChallenges . push ( getSeasonWeeklyHardChallenge ( pools , week , 3 ) ) ;
} else {
activeChallenges . push ( getSeasonWeeklyChallenge ( pools , week , 2 ) ) ;
activeChallenges . push ( getSeasonWeeklyChallenge ( pools , week , 3 ) ) ;
activeChallenges . push ( getSeasonWeeklyChallenge ( pools , week , 4 ) ) ;
activeChallenges . push ( getSeasonWeeklyHardChallenge ( pools , week , 5 ) ) ;
activeChallenges . push ( getSeasonWeeklyHardChallenge ( pools , week , 6 ) ) ;
}
2025-04-19 09:06:49 -07:00
} ;
2025-06-13 04:46:18 -07:00
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]
2025-05-06 19:05:23 -07:00
export const pushClassicBounties = ( syndicateMissions : ISyndicateMissionInfo [ ] , bountyCycle : number ) : void = > {
const table = String . fromCharCode ( 65 + ( bountyCycle % 3 ) ) ;
const vaultTable = String . fromCharCode ( 65 + ( ( bountyCycle + 1 ) % 3 ) ) ;
const deimosDTable = String . fromCharCode ( 65 + ( bountyCycle % 2 ) ) ;
2025-05-09 21:36:22 -07:00
const seed = new SRng ( bountyCycle ) . randomInt ( 0 , 100 _000 ) ;
2025-05-06 19:05:23 -07:00
const bountyCycleStart = bountyCycle * 9000000 ;
const bountyCycleEnd = bountyCycleStart + 9000000 ;
{
2025-05-09 21:36:22 -07:00
const rng = new SRng ( seed ) ;
2025-05-06 19:05:23 -07:00
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.randomElement ( eidolonJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable ${ table } Rewards ` ,
masteryReq : 0 ,
minEnemyLevel : 5 ,
maxEnemyLevel : 15 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 3 , 1000 , 1500 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( eidolonJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable ${ table } Rewards ` ,
masteryReq : 1 ,
minEnemyLevel : 10 ,
maxEnemyLevel : 30 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 3 , 1750 , 2250 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( eidolonJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable ${ table } Rewards ` ,
masteryReq : 2 ,
minEnemyLevel : 20 ,
maxEnemyLevel : 40 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 4 , 2500 , 3000 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( eidolonJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable ${ table } Rewards ` ,
masteryReq : 3 ,
minEnemyLevel : 30 ,
maxEnemyLevel : 50 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 5 , 3250 , 3750 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( eidolonJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable ${ table } Rewards ` ,
masteryReq : 5 ,
minEnemyLevel : 40 ,
maxEnemyLevel : 60 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 5 , 4000 , 4500 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( eidolonJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable ${ table } Rewards ` ,
masteryReq : 10 ,
minEnemyLevel : 100 ,
maxEnemyLevel : 100 ,
xpAmounts : [ 840 , 840 , 840 , 840 , 1660 ]
} ,
{
jobType : rng.randomElement ( eidolonNarmerJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable ${ table } Rewards ` ,
masteryReq : 0 ,
minEnemyLevel : 50 ,
maxEnemyLevel : 70 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 5 , 4500 , 5000 )
2025-05-06 19:05:23 -07:00
}
]
} ) ;
}
{
2025-05-09 21:36:22 -07:00
const rng = new SRng ( seed ) ;
2025-05-06 19:05:23 -07:00
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.randomElement ( venusJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable ${ table } Rewards ` ,
masteryReq : 0 ,
minEnemyLevel : 5 ,
maxEnemyLevel : 15 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 3 , 1000 , 1500 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( venusJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable ${ table } Rewards ` ,
masteryReq : 1 ,
minEnemyLevel : 10 ,
maxEnemyLevel : 30 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 3 , 1750 , 2250 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( venusJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable ${ table } Rewards ` ,
masteryReq : 2 ,
minEnemyLevel : 20 ,
maxEnemyLevel : 40 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 4 , 2500 , 3000 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( venusJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable ${ table } Rewards ` ,
masteryReq : 3 ,
minEnemyLevel : 30 ,
maxEnemyLevel : 50 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 5 , 3250 , 3750 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( venusJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable ${ table } Rewards ` ,
masteryReq : 5 ,
minEnemyLevel : 40 ,
maxEnemyLevel : 60 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 5 , 4000 , 4500 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( venusJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable ${ table } Rewards ` ,
masteryReq : 10 ,
minEnemyLevel : 100 ,
maxEnemyLevel : 100 ,
xpAmounts : [ 840 , 840 , 840 , 840 , 1660 ]
} ,
{
jobType : rng.randomElement ( venusNarmerJobs ) ,
rewards : "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards" ,
masteryReq : 0 ,
minEnemyLevel : 50 ,
maxEnemyLevel : 70 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 5 , 4500 , 5000 )
2025-05-06 19:05:23 -07:00
}
]
} ) ;
}
{
2025-05-09 21:36:22 -07:00
const rng = new SRng ( seed ) ;
2025-05-06 19:05:23 -07:00
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.randomElement ( microplanetJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable ${ table } Rewards ` ,
masteryReq : 0 ,
minEnemyLevel : 5 ,
maxEnemyLevel : 15 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 3 , 12 , 18 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( microplanetJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable ${ table } Rewards ` ,
masteryReq : 1 ,
minEnemyLevel : 15 ,
maxEnemyLevel : 25 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 3 , 24 , 36 )
2025-05-06 19:05:23 -07:00
} ,
{
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.randomElement ( microplanetJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable ${ deimosDTable } Rewards ` ,
masteryReq : 2 ,
minEnemyLevel : 30 ,
maxEnemyLevel : 40 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 4 , 72 , 88 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( microplanetJobs ) ,
rewards : ` /Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards ` ,
masteryReq : 3 ,
minEnemyLevel : 40 ,
maxEnemyLevel : 60 ,
2025-06-13 04:46:18 -07:00
xpAmounts : generateXpAmounts ( rng , 5 , 115 , 135 )
2025-05-06 19:05:23 -07:00
} ,
{
jobType : rng.randomElement ( microplanetJobs ) ,
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
}
]
} ) ;
}
} ;
2025-04-17 08:00:19 -07:00
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 ;
2025-04-25 11:53:04 -07:00
const seasonDay1 = [ 1 , 91 , 182 , 274 ] [ seasonIndex ] ;
const seasonDay91 = seasonDay1 + 90 ;
2025-04-17 08:00:19 -07:00
const eventDays : ICalendarDay [ ] = [ ] ;
for ( const day of birthdays ) {
if ( day < seasonDay1 ) {
continue ;
}
if ( day >= seasonDay91 ) {
break ;
}
//logger.debug(`birthday on day ${day}`);
eventDays . push ( { day , events : [ ] } ) ; // This is how CET_PLOT looks in worldState as of around 38.5.0
}
2025-05-09 21:36:22 -07:00
const rng = new SRng ( new SRng ( week ) . randomInt ( 0 , 100 _000 ) ) ;
2025-04-17 08:00:19 -07:00
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 ) ;
2025-04-25 11:53:04 -07:00
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
2025-04-17 08:00:19 -07:00
challenges . splice ( challengeIndex , 1 ) ;
//logger.debug(`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 ) {
2025-04-25 11:53:04 -07:00
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 ) ;
}
2025-04-17 08:00:19 -07:00
2025-04-25 11:53:04 -07:00
//logger.debug(`trying to fit rewards between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`);
2025-04-17 08:00:19 -07:00
let day : number ;
do {
day = rng . randomInt ( rewardRanges [ i ] + 1 , rewardRanges [ i + 1 ] - 1 ) ;
} while ( eventDays . find ( x = > x . day == day ) ) ;
2025-04-25 11:53:04 -07:00
eventDays . push ( { day , events } ) ;
2025-04-17 08:00:19 -07:00
}
2025-04-25 11:53:04 -07:00
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"
]
2025-04-17 08:00:19 -07:00
] ;
for ( let i = 0 ; i != upgradeRanges . length - 1 ; ++ i ) {
2025-04-25 11:53:04 -07:00
// 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 ) ;
}
2025-04-17 08:00:19 -07:00
2025-04-25 11:53:04 -07:00
//logger.debug(`trying to fit upgrades between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`);
2025-04-17 08:00:19 -07:00
let day : number ;
do {
day = rng . randomInt ( upgradeRanges [ i ] + 1 , upgradeRanges [ i + 1 ] - 1 ) ;
} while ( eventDays . find ( x = > x . day == day ) ) ;
2025-04-25 11:53:04 -07:00
eventDays . push ( { day , events } ) ;
2025-04-17 08:00:19 -07:00
}
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 ,
2025-04-25 11:53:34 -07:00
Season : ( [ "CST_WINTER" , "CST_SPRING" , "CST_SUMMER" , "CST_FALL" ] as const ) [ seasonIndex ] ,
2025-04-17 08:00:19 -07:00
YearIteration : Math.trunc ( week / 4 ) ,
Version : 19 ,
UpgradeAvaliabilityRequirements : [ "/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement" ]
} ;
} ;
2025-06-16 14:55:35 -07:00
// 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 voidStormMissionsA = {
VoidT1 : [ "CrewBattleNode519" , "CrewBattleNode518" , "CrewBattleNode515" , "CrewBattleNode503" ] ,
VoidT2 : [ "CrewBattleNode501" , "CrewBattleNode534" , "CrewBattleNode530" ] ,
VoidT3 : [ "CrewBattleNode521" , "CrewBattleNode516" ] ,
VoidT4 : [
"CrewBattleNode555" ,
"CrewBattleNode553" ,
"CrewBattleNode554" ,
"CrewBattleNode539" ,
"CrewBattleNode531" ,
"CrewBattleNode527"
]
} ;
const voidStormMissionsB = {
VoidT1 : [ "CrewBattleNode509" , "CrewBattleNode522" , "CrewBattleNode511" , "CrewBattleNode512" ] ,
VoidT2 : [ "CrewBattleNode535" , "CrewBattleNode533" ] ,
VoidT3 : [ "CrewBattleNode524" , "CrewBattleNode525" ] ,
VoidT4 : [
"CrewBattleNode542" ,
"CrewBattleNode538" ,
"CrewBattleNode543" ,
"CrewBattleNode536" ,
"CrewBattleNode550" ,
"CrewBattleNode529"
]
} ;
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 rng = new SRng ( new SRng ( hour ) . randomInt ( 0 , 100 _000 ) ) ;
const voidStormMissions = structuredClone ( hour & 1 ? voidStormMissionsA : voidStormMissionsB ) ;
for ( const tier of [ "VoidT1" , "VoidT1" , "VoidT2" , "VoidT3" , "VoidT4" , "VoidT4" ] as const ) {
const idx = rng . randomInt ( 0 , voidStormMissions [ tier ] . length - 1 ) ;
const node = voidStormMissions [ tier ] [ idx ] ;
voidStormMissions [ tier ] . splice ( idx , 1 ) ;
arr . push ( {
_id : {
$oid :
( ( activation / 1000 ) & 0xffffffff ) . toString ( 16 ) . padStart ( 8 , "0" ) +
"0321e89b" +
( accum ++ ) . toString ( ) . padStart ( 8 , "0" )
} ,
Node : node ,
Activation : { $date : { $numberLong : activation.toString ( ) } } ,
Expiry : { $date : { $numberLong : expiry.toString ( ) } } ,
ActiveMissionTier : tier
} ) ;
}
} ;
2025-06-09 06:54:58 -07:00
const doesTimeSatsifyConstraints = ( timeSecs : number ) : boolean = > {
if ( config . worldState ? . eidolonOverride ) {
const eidolonEpoch = 1391992660 ;
const eidolonCycle = Math . trunc ( ( timeSecs - eidolonEpoch ) / 9000 ) ;
const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000 ;
const eidolonCycleEnd = eidolonCycleStart + 9000 ;
const eidolonCycleNightStart = eidolonCycleEnd - 3000 ;
if ( config . worldState . eidolonOverride == "day" ) {
if (
//timeSecs < eidolonCycleStart ||
isBeforeNextExpectedWorldStateRefresh ( timeSecs * 1000 , eidolonCycleNightStart * 1000 )
) {
return false ;
}
} else {
if (
timeSecs < eidolonCycleNightStart ||
isBeforeNextExpectedWorldStateRefresh ( timeSecs * 1000 , eidolonCycleEnd * 1000 )
) {
return false ;
}
}
}
if ( config . worldState ? . vallisOverride ) {
const vallisEpoch = 1541837628 ;
const vallisCycle = Math . trunc ( ( timeSecs - vallisEpoch ) / 1600 ) ;
const vallisCycleStart = vallisEpoch + vallisCycle * 1600 ;
const vallisCycleEnd = vallisCycleStart + 1600 ;
const vallisCycleColdStart = vallisCycleStart + 400 ;
if ( config . worldState . vallisOverride == "cold" ) {
if (
timeSecs < vallisCycleColdStart ||
isBeforeNextExpectedWorldStateRefresh ( timeSecs * 1000 , vallisCycleEnd * 1000 )
) {
return false ;
}
} else {
if (
//timeSecs < vallisCycleStart ||
isBeforeNextExpectedWorldStateRefresh ( timeSecs * 1000 , vallisCycleColdStart * 1000 )
) {
return false ;
}
}
}
return true ;
} ;
2025-04-10 12:54:48 -07:00
export const getWorldState = ( buildLabel? : string ) : IWorldState = > {
2025-06-09 06:54:58 -07:00
let timeSecs = Math . round ( Date . now ( ) / 1000 ) ;
while ( ! doesTimeSatsifyConstraints ( timeSecs ) ) {
timeSecs -= 60 ;
}
2025-06-07 16:46:45 -07:00
const timeMs = timeSecs * 1000 ;
const day = Math . trunc ( ( timeMs - EPOCH ) / 86400000 ) ;
2025-04-10 12:54:48 -07:00
const week = Math . trunc ( day / 7 ) ;
const weekStart = EPOCH + week * 604800000 ;
const weekEnd = weekStart + 604800000 ;
const worldState : IWorldState = {
BuildLabel : typeof buildLabel == "string" ? buildLabel . split ( " " ) . join ( "+" ) : buildConfig . buildLabel ,
2025-06-07 16:46:45 -07:00
Time : timeSecs ,
2025-04-10 12:54:48 -07:00
Goals : [ ] ,
2025-04-15 20:41:46 -07:00
Alerts : [ ] ,
2025-04-10 12:54:48 -07:00
Sorties : [ ] ,
LiteSorties : [ ] ,
2025-04-15 20:41:46 -07:00
GlobalUpgrades : [ ] ,
2025-06-16 14:55:35 -07:00
VoidStorms : [ ] ,
2025-04-10 12:54:48 -07:00
EndlessXpChoices : [ ] ,
2025-04-17 08:00:19 -07:00
KnownCalendarSeasons : [ ] ,
2025-04-15 20:41:46 -07:00
. . . staticWorldState ,
SyndicateMissions : [ . . . staticWorldState . SyndicateMissions ]
2025-04-10 12:54:48 -07:00
} ;
2025-05-01 13:52:43 -07:00
// Omit void fissures for versions prior to Dante Unbound to avoid script errors.
if ( buildLabel && version_compare ( buildLabel , "2024.03.24.20.00" ) < 0 ) {
2025-04-30 13:28:24 -07:00
worldState . ActiveMissions = [ ] ;
2025-05-05 18:09:03 -07:00
if ( version_compare ( buildLabel , "2017.10.12.17.04" ) < 0 ) {
// Old versions seem to really get hung up on not being able to load these.
worldState . PVPChallengeInstances = [ ] ;
}
2025-04-30 13:28:24 -07:00
}
2025-04-10 12:54:48 -07:00
if ( config . worldState ? . starDays ) {
worldState . Goals . push ( {
_id : { $oid : "67a4dcce2a198564d62e1647" } ,
Activation : { $date : { $numberLong : "1738868400000" } } ,
Expiry : { $date : { $numberLong : "2000000000000" } } ,
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"
} ) ;
}
2025-04-16 06:31:16 -07:00
// Nightwave Challenges
2025-05-23 06:12:54 -07:00
const nightwaveSyndicateTag = getNightwaveSyndicateTag ( buildLabel ) ;
if ( nightwaveSyndicateTag ) {
worldState . SeasonInfo = {
Activation : { $date : { $numberLong : "1715796000000" } } ,
Expiry : { $date : { $numberLong : "2000000000000" } } ,
AffiliationTag : nightwaveSyndicateTag ,
Season : nightwaveTagToSeason [ nightwaveSyndicateTag ] ,
Phase : 0 ,
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 ) ) ;
2025-06-07 16:46:45 -07:00
if ( isBeforeNextExpectedWorldStateRefresh ( timeMs , EPOCH + ( day + 1 ) * 86400000 ) ) {
2025-05-23 06:12:54 -07:00
worldState . SeasonInfo . ActiveChallenges . push ( getSeasonDailyChallenge ( pools , day + 1 ) ) ;
2025-05-03 17:24:31 -07:00
}
2025-05-23 06:12:54 -07:00
pushWeeklyActs ( worldState . SeasonInfo . ActiveChallenges , pools , week ) ;
2025-06-07 16:46:45 -07:00
if ( isBeforeNextExpectedWorldStateRefresh ( timeMs , weekEnd ) ) {
2025-05-23 06:12:54 -07:00
pushWeeklyActs ( worldState . SeasonInfo . ActiveChallenges , pools , week + 1 ) ;
2025-05-03 17:24:31 -07:00
}
2025-04-19 09:06:49 -07:00
}
2025-04-16 06:31:16 -07:00
2025-04-10 12:54:48 -07:00
// Elite Sanctuary Onslaught cycling every week
2025-05-09 21:36:22 -07:00
worldState . NodeOverrides . find ( x = > x . Node == "SolNode802" ) ! . Seed = new SRng ( week ) . randomInt ( 0 , 0xff _ffff ) ;
2025-04-10 12:54:48 -07:00
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
2025-06-07 16:46:45 -07:00
let bountyCycle = Math . trunc ( timeSecs / 9000 ) ;
2025-04-15 20:41:46 -07:00
let bountyCycleEnd : number | undefined ;
do {
const bountyCycleStart = bountyCycle * 9000000 ;
bountyCycleEnd = bountyCycleStart + 9000000 ;
worldState . SyndicateMissions . push ( {
2025-04-25 18:55:09 -07:00
_id : { $oid : ( ( bountyCycleStart / 1000 ) & 0xffffffff ) . toString ( 16 ) . padStart ( 8 , "0" ) + "0000000000000029" } ,
2025-04-15 20:41:46 -07:00
Activation : { $date : { $numberLong : bountyCycleStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : bountyCycleEnd.toString ( ) } } ,
Tag : "ZarimanSyndicate" ,
Seed : bountyCycle ,
Nodes : [ ]
} ) ;
worldState . SyndicateMissions . push ( {
2025-04-25 18:55:09 -07:00
_id : { $oid : ( ( bountyCycleStart / 1000 ) & 0xffffffff ) . toString ( 16 ) . padStart ( 8 , "0" ) + "0000000000000004" } ,
2025-04-15 20:41:46 -07:00
Activation : { $date : { $numberLong : bountyCycleStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : bountyCycleEnd.toString ( ) } } ,
Tag : "EntratiLabSyndicate" ,
Seed : bountyCycle ,
Nodes : [ ]
} ) ;
worldState . SyndicateMissions . push ( {
2025-04-25 18:55:09 -07:00
_id : { $oid : ( ( bountyCycleStart / 1000 ) & 0xffffffff ) . toString ( 16 ) . padStart ( 8 , "0" ) + "0000000000000006" } ,
2025-04-15 20:41:46 -07:00
Activation : { $date : { $numberLong : bountyCycleStart.toString ( 10 ) } } ,
Expiry : { $date : { $numberLong : bountyCycleEnd.toString ( 10 ) } } ,
Tag : "HexSyndicate" ,
Seed : bountyCycle ,
Nodes : [ ]
} ) ;
2025-05-06 19:05:23 -07:00
pushClassicBounties ( worldState . SyndicateMissions , bountyCycle ) ;
2025-06-07 16:46:45 -07:00
} while ( isBeforeNextExpectedWorldStateRefresh ( timeMs , bountyCycleEnd ) && ++ bountyCycle ) ;
2025-04-10 12:54:48 -07:00
if ( config . worldState ? . creditBoost ) {
worldState . GlobalUpgrades . push ( {
_id : { $oid : "5b23106f283a555109666672" } ,
Activation : { $date : { $numberLong : "1740164400000" } } ,
ExpiryDate : { $date : { $numberLong : "2000000000000" } } ,
UpgradeType : "GAMEPLAY_MONEY_REWARD_AMOUNT" ,
OperationType : "MULTIPLY" ,
Value : 2 ,
LocalizeTag : "" ,
LocalizeDescTag : ""
} ) ;
}
if ( config . worldState ? . affinityBoost ) {
worldState . GlobalUpgrades . push ( {
_id : { $oid : "5b23106f283a555109666673" } ,
Activation : { $date : { $numberLong : "1740164400000" } } ,
ExpiryDate : { $date : { $numberLong : "2000000000000" } } ,
UpgradeType : "GAMEPLAY_KILL_XP_AMOUNT" ,
OperationType : "MULTIPLY" ,
Value : 2 ,
LocalizeTag : "" ,
LocalizeDescTag : ""
} ) ;
}
if ( config . worldState ? . resourceBoost ) {
worldState . GlobalUpgrades . push ( {
_id : { $oid : "5b23106f283a555109666674" } ,
Activation : { $date : { $numberLong : "1740164400000" } } ,
ExpiryDate : { $date : { $numberLong : "2000000000000" } } ,
UpgradeType : "GAMEPLAY_PICKUP_AMOUNT" ,
OperationType : "MULTIPLY" ,
Value : 2 ,
LocalizeTag : "" ,
LocalizeDescTag : ""
} ) ;
}
2025-04-22 10:00:49 -07:00
// Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST)
2025-05-06 02:29:11 -07:00
{
const rollover = getSortieTime ( day ) ;
2025-06-07 16:46:45 -07:00
if ( timeMs < rollover ) {
2025-05-06 02:29:11 -07:00
worldState . Sorties . push ( getSortie ( day - 1 ) ) ;
}
2025-06-07 16:46:45 -07:00
if ( isBeforeNextExpectedWorldStateRefresh ( timeMs , rollover ) ) {
2025-05-06 02:29:11 -07:00
worldState . Sorties . push ( getSortie ( day ) ) ;
}
// The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
2025-06-07 16:46:45 -07:00
const sdy = timeMs >= rollover ? day : day - 1 ;
2025-05-09 21:36:22 -07:00
const rng = new SRng ( sdy ) ;
2025-05-08 18:00:22 -07:00
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" ) ;
2025-05-06 02:29:11 -07:00
}
2025-04-10 12:54:48 -07:00
// Archon Hunt cycling every week
2025-04-19 09:06:38 -07:00
worldState . LiteSorties . push ( getLiteSortie ( week ) ) ;
2025-06-07 16:46:45 -07:00
if ( isBeforeNextExpectedWorldStateRefresh ( timeMs , weekEnd ) ) {
2025-04-19 09:06:38 -07:00
worldState . LiteSorties . push ( getLiteSortie ( week + 1 ) ) ;
2025-04-10 12:54:48 -07:00
}
// Circuit choices cycling every week
worldState . EndlessXpChoices . push ( {
Category : "EXC_NORMAL" ,
Choices : [
[ "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" ]
] [ week % 12 ]
} ) ;
worldState . EndlessXpChoices . push ( {
Category : "EXC_HARD" ,
Choices : [
[ "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" ] ,
[ "Braton" , "Lato" , "Skana" , "Paris" , "Kunai" ]
] [ week % 8 ]
} ) ;
// 1999 Calendar Season cycling every week + YearIteration every 4 weeks
2025-04-17 08:00:19 -07:00
worldState . KnownCalendarSeasons . push ( getCalendarSeason ( week ) ) ;
2025-06-07 16:46:45 -07:00
if ( isBeforeNextExpectedWorldStateRefresh ( timeMs , weekEnd ) ) {
2025-04-17 08:00:19 -07:00
worldState . KnownCalendarSeasons . push ( getCalendarSeason ( week + 1 ) ) ;
2025-06-16 14:55:35 -07:00
}
// Void Storms
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 ) ;
2025-04-17 08:00:19 -07:00
}
2025-04-10 12:54:48 -07:00
// Sentient Anomaly cycling every 30 minutes
2025-06-07 16:46:45 -07:00
const halfHour = Math . trunc ( timeMs / ( unixTimesInMs . hour / 2 ) ) ;
2025-04-10 12:54:48 -07:00
const tmp = {
cavabegin : "1690761600" ,
PurchasePlatformLockEnabled : true ,
tcsn : 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 ,
sfn : [ 550 , 553 , 554 , 555 ] [ halfHour % 4 ]
} ;
worldState . Tmp = JSON . stringify ( tmp ) ;
return worldState ;
} ;
2025-04-19 09:06:38 -07:00
2025-05-06 19:05:23 -07:00
export const idToBountyCycle = ( id : string ) : number = > {
return Math . trunc ( ( parseInt ( id . substring ( 0 , 8 ) , 16 ) * 1000 ) / 9000 _000 ) ;
} ;
2025-05-06 02:29:11 -07:00
export const idToDay = ( id : string ) : number = > {
return Math . trunc ( ( parseInt ( id . substring ( 0 , 8 ) , 16 ) * 1000 - EPOCH ) / 86400 _000 ) ;
} ;
2025-04-19 09:06:38 -07:00
export const idToWeek = ( id : string ) : number = > {
2025-05-06 02:29:11 -07:00
return Math . trunc ( ( parseInt ( id . substring ( 0 , 8 ) , 16 ) * 1000 - EPOCH ) / 604800 _000 ) ;
2025-04-19 09:06:38 -07:00
} ;
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 . factionIndex !== undefined &&
value . factionIndex < 2 &&
2025-04-23 11:40:45 -07:00
! isArchwingMission ( value ) &&
2025-05-07 20:13:45 -07:00
value . missionIndex != 0 && // Exclude MT_ASSASSINATION
2025-05-10 19:12:34 -07:00
value . missionIndex != 23 && // Exclude junctions
value . missionIndex != 28 && // Exclude open worlds
2025-05-07 20:13:45 -07:00
value . missionIndex != 32 // Exclude railjack
2025-04-19 09:06:38 -07:00
) {
nodes . push ( key ) ;
}
}
2025-05-09 21:36:22 -07:00
const seed = new SRng ( week ) . randomInt ( 0 , 100 _000 ) ;
const rng = new SRng ( seed ) ;
2025-04-19 09:06:38 -07:00
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 : {
2025-04-25 18:55:09 -07:00
$oid : ( ( weekStart / 1000 ) & 0xffffffff ) . toString ( 16 ) . padStart ( 8 , "0" ) + "5e23a244740a190c"
2025-04-19 09:06:38 -07:00
} ,
Activation : { $date : { $numberLong : weekStart.toString ( ) } } ,
Expiry : { $date : { $numberLong : weekEnd.toString ( ) } } ,
Reward : "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards" ,
2025-04-24 11:24:11 -07:00
Seed : seed ,
2025-04-19 09:06:38 -07:00
Boss : boss ,
Missions : [
{
missionType : rng.randomElement ( [
"MT_INTEL" ,
"MT_MOBILE_DEFENSE" ,
"MT_EXTERMINATION" ,
"MT_SABOTAGE" ,
"MT_RESCUE"
2025-04-29 12:27:25 -07:00
] ) ! ,
2025-04-19 09:06:38 -07:00
node : firstNode
} ,
{
missionType : rng.randomElement ( [
"MT_DEFENSE" ,
"MT_TERRITORY" ,
"MT_ARTIFACT" ,
"MT_EXCAVATE" ,
"MT_SURVIVAL"
2025-04-29 12:27:25 -07:00
] ) ! ,
node : rng.randomElement ( nodes ) !
2025-04-19 09:06:38 -07:00
} ,
{
missionType : "MT_ASSASSINATION" ,
node : showdownNode
}
]
} ;
} ;
2025-04-23 11:40:45 -07:00
export const isArchwingMission = ( node : IRegion ) : boolean = > {
if ( node . name . indexOf ( "Archwing" ) != - 1 ) {
return true ;
}
// SettlementNode10
if ( node . missionIndex == 25 ) {
return true ;
}
return false ;
} ;
2025-05-23 06:12:54 -07:00
export const getNightwaveSyndicateTag = ( buildLabel : string | undefined ) : string | undefined = > {
2025-06-07 02:16:19 -07:00
if ( config . worldState ? . nightwaveOverride ) {
2025-06-09 03:37:42 -07:00
if ( config . worldState . nightwaveOverride in nightwaveTagToSeason ) {
return config . worldState . nightwaveOverride ;
}
logger . warn ( ` ignoring invalid config value for worldState.nightwaveOverride ` , {
value : config.worldState.nightwaveOverride ,
valid_values : Object.keys ( nightwaveTagToSeason )
} ) ;
2025-06-07 02:16:19 -07:00
}
2025-05-23 06:12:54 -07:00
if ( ! buildLabel || version_compare ( buildLabel , "2025.05.20.10.18" ) >= 0 ) {
return "RadioLegionIntermission13Syndicate" ;
}
if ( version_compare ( buildLabel , "2025.02.05.11.19" ) >= 0 ) {
return "RadioLegionIntermission12Syndicate" ;
}
return undefined ;
} ;
const nightwaveTagToSeason : Record < string , number > = {
2025-06-07 02:16:19 -07:00
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
2025-05-23 06:12:54 -07:00
} ;