From 6df67464ae18ff8b5fdea271e55b03f39a850567 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:49:52 +0200 Subject: [PATCH] feat: sortie rotation Closes #1179 --- .../dynamic/worldStateController.ts | 147 ++++++++++++++++++ src/helpers/worlstateHelper.ts | 114 ++++++++++++++ .../worldState/worldState.json | 17 -- 3 files changed, 261 insertions(+), 17 deletions(-) create mode 100644 src/helpers/worlstateHelper.ts diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 05c7f60f..82313183 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -10,6 +10,14 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { ExportNightwave, ExportRegions } from "warframe-public-export-plus"; +import { + missionTags, + sortieBosses, + sortieBossNode, + sortieBossToFaction, + sortieFactionToFactionIndexes, + sortieFactionToSystemIndexes +} from "@/src/helpers/worlstateHelper"; const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 @@ -27,6 +35,7 @@ export const worldStateController: RequestHandler = (req, res) => { Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), Goals: [], GlobalUpgrades: [], + Sorties: [], LiteSorties: [], EndlessXpChoices: [], SeasonInfo: { @@ -154,6 +163,129 @@ export const worldStateController: RequestHandler = (req, res) => { }); } + // Sortie cycling every day + { + const dayStart = EPOCH + day * 86400000; + const dayEnd = dayStart + 86400000; + + const rng = new CRng(day); + + const boss = rng.randomElement(sortieBosses); + + 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_GAS", + "SORTIE_MODIFIER_FIRE", + "SORTIE_MODIFIER_EXPLOSION", + "SORTIE_MODIFIER_FREEZE", + "SORTIE_MODIFIER_TOXIN", + "SORTIE_MODIFIER_POISON", + "SORTIE_MODIFIER_HAZARD_RADIATION", + "SORTIE_MODIFIER_HAZARD_MAGNETIC", + "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest + "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon + "SORTIE_MODIFIER_HAZARD_ICE", + "SORTIE_MODIFIER_HAZARD_COLD", + "SORTIE_MODIFIER_SECONDARY_ONLY", + "SORTIE_MODIFIER_SHOTGUN_ONLY", + "SORTIE_MODIFIER_SNIPER_ONLY", + "SORTIE_MODIFIER_RIFLE_ONLY", + "SORTIE_MODIFIER_MELEE_ONLY", + "SORTIE_MODIFIER_BOW_ONLY" + ]; + + if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); + if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); + + const nodes: string[] = []; + const availableMissionIndexes: number[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && + sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 && // Exclude MT_ASSASSINATION + value.missionIndex != 5 && // Exclude MT_CAPTURE + value.missionIndex != 21 && // Exclude MT_PURIFY + value.missionIndex != 23 && // Exclude MT_JUNCTION + value.missionIndex <= 28 + ) { + if (!availableMissionIndexes.includes(value.missionIndex)) { + availableMissionIndexes.push(value.missionIndex); + } + nodes.push(key); + } + } + + const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; + const missionTypes = new Set(); + + for (let i = 0; i < 3; i++) { + const randomIndex = rng.randomInt(0, nodes.length - 1); + const node = nodes[randomIndex]; + let missionIndex = ExportRegions[node].missionIndex; + + if ( + !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions + missionIndex != 28 && + rng.randomInt(0, 2) == 2 + ) { + missionIndex = rng.randomElement(availableMissionIndexes); + } + + if (i == 2 && rng.randomInt(0, 2) == 2) { + const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); + const modifierType = rng.randomElement(filteredModifiers); + + if (boss == "SORTIE_BOSS_PHORID") { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); + nodes.splice(randomIndex, 1); + continue; + } else if (sortieBossNode[boss]) { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); + continue; + } + } + + const missionType = missionTags[missionIndex]; + + if (missionTypes.has(missionType)) { + i--; + continue; + } + + const filteredModifiers = + missionType === "MT_TERRITORY" + ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") + : modifiers; + + const modifierType = rng.randomElement(filteredModifiers); + + selectedNodes.push({ missionType, modifierType, node }); + nodes.splice(randomIndex, 1); + missionTypes.add(missionType); + } + + worldState.Sorties.push({ + _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", + Seed: day, + Boss: boss, + Variants: selectedNodes + }); + } + // Archon Hunt cycling every week { const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; @@ -298,6 +430,7 @@ interface IWorldState { Goals: IGoal[]; SyndicateMissions: ISyndicateMission[]; GlobalUpgrades: IGlobalUpgrade[]; + Sorties: ISortie[]; LiteSorties: ILiteSortie[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; @@ -361,6 +494,20 @@ interface INodeOverride { CustomNpcEncounters?: string; } +interface ISortie { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards"; + Seed: number; + Boss: string; + Variants: { + missionType: string; + modifierType: string; + node: string; + }[]; +} + interface ILiteSortie { _id: IOid; Activation: IMongoDate; diff --git a/src/helpers/worlstateHelper.ts b/src/helpers/worlstateHelper.ts new file mode 100644 index 00000000..4b4554f0 --- /dev/null +++ b/src/helpers/worlstateHelper.ts @@ -0,0 +1,114 @@ +export const missionTags = [ + "MT_ASSASSINATION", + "MT_EXTERMINATION", + "MT_SURVIVAL", + "MT_RESCUE", + "MT_SABOTAGE", + "MT_CAPTURE", + "MT_COUNTER_INTEL", + "MT_INTEL", + "MT_DEFENSE", + "MT_MOBILE_DEFENSE", + "MT_PVP", + "MT_MASTERY", + "MT_RECOVERY", + "MT_TERRITORY", + "MT_RETRIEVAL", + "MT_HIVE", + "MT_SALVAGE", + "MT_EXCAVATE", + "MT_RAID", + "MT_PURGE", + "MT_GENERIC", + "MT_PURIFY", + "MT_ARENA", + "MT_JUNCTION", + "MT_PURSUIT", + "MT_RACE", + "MT_ASSAULT", + "MT_EVACUATION", + "MT_LANDSCAPE", + "MT_RESOURCE_THEFT", + "MT_ENDLESS_EXTERMINATION", + "MT_ENDLESS_DUVIRI", + "MT_RAILJACK", + "MT_ARTIFACT", + "MT_CORRUPTION", + "MT_VOID_CASCADE", + "MT_ARMAGEDDON", + "MT_VAULTS", + "MT_ALCHEMY", + "MT_ASCENSION", + "MT_ENDLESS_CAPTURE", + "MT_OFFERING", + "MT_PVPVE" +]; + +export 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" +]; + +export const sortieBossToFaction: Record = { + 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_CORRUPTED" +}; + +export const sortieFactionToSystemIndexes: Record = { + 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], + FC_CORRUPTED: [14] +}; + +export const sortieFactionToFactionIndexes: Record = { + FC_GRINEER: [0], + FC_CORPUS: [1], + FC_INFESTATION: [0, 1, 2], + FC_CORRUPTED: [3] +}; + +export const sortieBossNode: Record = { + SORTIE_BOSS_HYENA: "SolNode127", + SORTIE_BOSS_KELA: "SolNode193", + SORTIE_BOSS_VOR: "SolNode108", + SORTIE_BOSS_RUK: "SolNode32", + SORTIE_BOSS_HEK: "SolNode24", + SORTIE_BOSS_KRIL: "SolNode99", + SORTIE_BOSS_TYL: "SolNode105", + SORTIE_BOSS_JACKAL: "SolNode104", + SORTIE_BOSS_ALAD: "SolNode53", + SORTIE_BOSS_AMBULAS: "SolNode51", + SORTIE_BOSS_NEF: "SettlementNode20", + SORTIE_BOSS_RAPTOR: "SolNode210", + SORTIE_BOSS_LEPHANTIS: "SolNode712", + SORTIE_BOSS_INFALAD: "SolNode705" +}; diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 74463e4d..d9e228ae 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -62,23 +62,6 @@ } } ], - "Sorties": [ - { - "_id": { "$oid": "663a4c7d4d932c97c0a3acd7" }, - "Activation": { "$date": { "$numberLong": "1715097600000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Reward": "/Lotus/Types/Game/MissionDecks/SortieRewards", - "Seed": 24491, - "Boss": "SORTIE_BOSS_TYL", - "ExtraDrops": [], - "Variants": [ - { "missionType": "MT_TERRITORY", "modifierType": "SORTIE_MODIFIER_ARMOR", "node": "SolNode122", "tileset": "GrineerOceanTileset" }, - { "missionType": "MT_MOBILE_DEFENSE", "modifierType": "SORTIE_MODIFIER_LOW_ENERGY", "node": "SolNode184", "tileset": "GrineerGalleonTileset" }, - { "missionType": "MT_LANDSCAPE", "modifierType": "SORTIE_MODIFIER_EXIMUS", "node": "SolNode228", "tileset": "EidolonTileset" } - ], - "Twitter": true - } - ], "SyndicateMissions": [ { "_id": { "$oid": "663a4fc5ba6f84724fa48049" },