feat: sortie rotation (#1453)
Reviewed-on: OpenWF/SpaceNinjaServer#1453 Reviewed-by: Sainan <sainan@calamity.inc> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									94993a16aa
								
							
						
					
					
						commit
						919f12b8f9
					
				@ -10,8 +10,16 @@ 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";
 | 
			
		||||
 | 
			
		||||
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
 | 
			
		||||
import {
 | 
			
		||||
    EPOCH,
 | 
			
		||||
    getSortieTime,
 | 
			
		||||
    missionTags,
 | 
			
		||||
    sortieBosses,
 | 
			
		||||
    sortieBossNode,
 | 
			
		||||
    sortieBossToFaction,
 | 
			
		||||
    sortieFactionToFactionIndexes,
 | 
			
		||||
    sortieFactionToSystemIndexes
 | 
			
		||||
} from "@/src/helpers/worlstateHelper";
 | 
			
		||||
 | 
			
		||||
export const worldStateController: RequestHandler = (req, res) => {
 | 
			
		||||
    const day = Math.trunc((Date.now() - EPOCH) / 86400000);
 | 
			
		||||
@ -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,142 @@ export const worldStateController: RequestHandler = (req, res) => {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sortie cycling every day
 | 
			
		||||
    {
 | 
			
		||||
        let genDay;
 | 
			
		||||
        let dayStart;
 | 
			
		||||
        let dayEnd;
 | 
			
		||||
        const sortieRolloverToday = getSortieTime(day);
 | 
			
		||||
        if (Date.now() < sortieRolloverToday) {
 | 
			
		||||
            // Early in the day, generate sortie for `day - 1`, expiring at `sortieRolloverToday`.
 | 
			
		||||
            genDay = day - 1;
 | 
			
		||||
            dayStart = getSortieTime(genDay);
 | 
			
		||||
            dayEnd = sortieRolloverToday;
 | 
			
		||||
        } else {
 | 
			
		||||
            // Late in the day, generate sortie for `day`, expiring at `getSortieTime(day + 1)`.
 | 
			
		||||
            genDay = day;
 | 
			
		||||
            dayStart = sortieRolloverToday;
 | 
			
		||||
            dayEnd = getSortieTime(day + 1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const rng = new CRng(genDay);
 | 
			
		||||
 | 
			
		||||
        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: genDay,
 | 
			
		||||
            Boss: boss,
 | 
			
		||||
            Variants: selectedNodes
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Archon Hunt cycling every week
 | 
			
		||||
    {
 | 
			
		||||
        const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3];
 | 
			
		||||
@ -298,6 +443,7 @@ interface IWorldState {
 | 
			
		||||
    Goals: IGoal[];
 | 
			
		||||
    SyndicateMissions: ISyndicateMission[];
 | 
			
		||||
    GlobalUpgrades: IGlobalUpgrade[];
 | 
			
		||||
    Sorties: ISortie[];
 | 
			
		||||
    LiteSorties: ILiteSortie[];
 | 
			
		||||
    NodeOverrides: INodeOverride[];
 | 
			
		||||
    EndlessXpChoices: IEndlessXpChoice[];
 | 
			
		||||
@ -361,6 +507,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;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										130
									
								
								src/helpers/worlstateHelper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/helpers/worlstateHelper.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
			
		||||
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<string, string> = {
 | 
			
		||||
    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<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],
 | 
			
		||||
    FC_CORRUPTED: [14]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sortieFactionToFactionIndexes: Record<string, number[]> = {
 | 
			
		||||
    FC_GRINEER: [0],
 | 
			
		||||
    FC_CORPUS: [1],
 | 
			
		||||
    FC_INFESTATION: [0, 1, 2],
 | 
			
		||||
    FC_CORRUPTED: [3]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sortieBossNode: Record<string, string> = {
 | 
			
		||||
    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"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
 | 
			
		||||
 | 
			
		||||
export 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;
 | 
			
		||||
};
 | 
			
		||||
@ -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" },
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user