feat: server-side conquest generation for U40 and above
Some checks failed
Build / build (pull_request) Failing after 1m45s

This commit is contained in:
Sainan 2025-10-31 09:43:47 +01:00
parent 5ceddddd81
commit ce43da79b4
4 changed files with 498 additions and 12 deletions

View File

@ -0,0 +1,430 @@
import { ExportFactions, type TFaction, type TMissionType } from "warframe-public-export-plus";
import type { CalendarSeasonType, IConquest, IConquestMission, TConquestType } from "../types/worldStateTypes.ts";
import { mixSeeds, SRng } from "./rngService.ts";
import { EPOCH } from "../constants/timeConstants.ts";
/*const missionTypes: Record<TConquestType, TMissionType[]> = {
CT_LAB: ["MT_EXTERMINATION", "MT_SURVIVAL", "MT_ALCHEMY", "MT_DEFENSE", "MT_ARTIFACT"],
CT_HEX: ["MT_EXTERMINATION", "MT_SURVIVAL", "MT_DEFENSE", "MT_ENDLESS_CAPTURE"]
};*/
const missionAndFactionTypes: Record<TConquestType, Partial<Record<TMissionType, TFaction[]>>> = {
CT_LAB: {
MT_EXTERMINATION: ["FC_MITW"],
MT_SURVIVAL: ["FC_MITW"],
MT_ALCHEMY: ["FC_MITW"],
MT_DEFENSE: ["FC_MITW"],
MT_ARTIFACT: ["FC_MITW"]
},
CT_HEX: {
MT_EXTERMINATION: ["FC_SCALDRA", "FC_TECHROT"],
MT_SURVIVAL: ["FC_SCALDRA", "FC_TECHROT"],
MT_DEFENSE: ["FC_SCALDRA"],
MT_ENDLESS_CAPTURE: ["FC_TECHROT"]
}
};
const assassinationFactionOptions: Record<TConquestType, TFaction[]> = {
CT_LAB: ["FC_MITW"],
CT_HEX: ["FC_SCALDRA"]
};
type TConquestDifficulty = "CD_NORMAL" | "CD_HARD";
interface IConquestConditional {
tag: string;
missionType?: TMissionType;
conquest?: TConquestType;
difficulty?: TConquestDifficulty;
season?: CalendarSeasonType;
}
const deviations: readonly IConquestConditional[] = [
{
tag: "AlchemicalShields",
missionType: "MT_ALCHEMY"
},
{
tag: "ContaminationZone",
missionType: "MT_SURVIVAL",
conquest: "CT_HEX"
},
{
tag: "DoubleTrouble",
missionType: "MT_ARTIFACT"
},
{
tag: "EscalateImmediately",
missionType: "MT_EXTERMINATION",
conquest: "CT_HEX"
},
{
tag: "EximusGrenadiers",
missionType: "MT_ALCHEMY"
},
{
tag: "FortifiedFoes",
missionType: "MT_EXTERMINATION"
},
{
tag: "FragileNodes",
missionType: "MT_ARTIFACT"
},
{
tag: "GrowingIncursion",
missionType: "MT_EXTERMINATION",
conquest: "CT_LAB"
},
{
tag: "HarshWords",
missionType: "MT_DEFENSE",
conquest: "CT_LAB"
},
{
tag: "HighScalingLegacyte",
missionType: "MT_ENDLESS_CAPTURE",
conquest: "CT_HEX"
},
{
tag: "DoubleTroubleLegacyte",
missionType: "MT_ENDLESS_CAPTURE",
conquest: "CT_HEX"
},
{
tag: "HostileSecurity",
missionType: "MT_DEFENSE",
conquest: "CT_LAB"
},
{
tag: "InfiniteTide",
missionType: "MT_ASSASSINATION",
conquest: "CT_LAB"
},
{
tag: "LostInTranslation",
missionType: "MT_DEFENSE",
conquest: "CT_LAB"
},
{
tag: "MutatedEnemies",
missionType: "MT_ENDLESS_CAPTURE",
conquest: "CT_HEX"
},
{
tag: "NecramechActivation",
missionType: "MT_SURVIVAL",
conquest: "CT_LAB"
},
{
tag: "Reinforcements",
missionType: "MT_ASSASSINATION",
conquest: "CT_LAB"
},
{
tag: "StickyFingers",
missionType: "MT_ARTIFACT",
conquest: "CT_LAB"
},
{
tag: "TankStrongArmor",
missionType: "MT_ASSASSINATION",
conquest: "CT_HEX"
},
{
tag: "TankReinforcements",
missionType: "MT_ASSASSINATION",
conquest: "CT_HEX"
},
{
tag: "TankSuperToxic",
missionType: "MT_ASSASSINATION",
conquest: "CT_HEX"
},
{
tag: "TechrotConjunction",
missionType: "MT_SURVIVAL",
conquest: "CT_HEX"
},
{
tag: "UnpoweredCapsules",
missionType: "MT_SURVIVAL",
conquest: "CT_LAB"
},
{
tag: "VolatileGrenades",
missionType: "MT_ALCHEMY"
},
{
tag: "GestatingTumors",
missionType: "MT_SURVIVAL",
conquest: "CT_HEX"
},
{
tag: "ChemicalNoise",
missionType: "MT_DEFENSE",
conquest: "CT_HEX"
},
{
tag: "ExplosiveEnergy",
missionType: "MT_DEFENSE",
conquest: "CT_HEX"
},
{
tag: "DisruptiveSounds",
missionType: "MT_DEFENSE",
conquest: "CT_HEX"
}
];
const risks: readonly IConquestConditional[] = [
{
tag: "Voidburst"
},
{
tag: "RegeneratingEnemies"
},
{
tag: "VoidAberration"
},
{
tag: "ShieldedFoes"
},
{
tag: "PointBlank"
},
{
tag: "Deflectors",
conquest: "CT_LAB"
},
{
tag: "AcceleratedEnemies"
},
{
tag: "DrainingResiduals"
},
{
tag: "Quicksand"
},
{
tag: "AntiMaterialWeapons",
conquest: "CT_LAB"
},
{
tag: "ExplosiveCrawlers",
conquest: "CT_LAB"
},
{
tag: "EMPBlackHole",
conquest: "CT_LAB"
},
{
tag: "ArtilleryBeacons",
conquest: "CT_HEX"
},
{
tag: "InfectedTechrot",
conquest: "CT_HEX"
},
{
tag: "BalloonFest",
conquest: "CT_HEX"
},
{
tag: "MiasmiteHive",
conquest: "CT_HEX"
},
{
tag: "CompetitionSpillover",
conquest: "CT_HEX"
},
{
tag: "HostileOvergrowth",
conquest: "CT_HEX"
},
{
tag: "MurmurIncursion",
conquest: "CT_HEX"
},
{
tag: "FactionSwarm_Techrot",
conquest: "CT_HEX",
difficulty: "CD_NORMAL"
},
{
tag: "FactionSwarm_Scaldra",
conquest: "CT_HEX",
difficulty: "CD_NORMAL"
},
{
tag: "HeavyWarfare",
conquest: "CT_HEX"
},
{
tag: "ArcadeAutomata",
conquest: "CT_HEX",
difficulty: "CD_NORMAL"
},
{
tag: "EfervonFog",
conquest: "CT_HEX"
},
{
tag: "WinterFrost",
conquest: "CT_HEX",
season: "CST_WINTER"
},
{
tag: "JadeSpring",
conquest: "CT_HEX",
season: "CST_SPRING"
},
{
tag: "ExplosiveSummer",
conquest: "CT_HEX",
season: "CST_SUMMER"
},
{
tag: "FallFog",
conquest: "CT_HEX",
season: "CST_FALL"
}
];
const filterConditionals = (
arr: readonly IConquestConditional[],
missionType: TMissionType | null,
conquest: TConquestType | null,
difficulty: TConquestDifficulty | null,
season: CalendarSeasonType | null
): string[] => {
const applicable = [];
for (const cond of arr) {
if (
(!cond.missionType || cond.missionType == missionType) &&
(!cond.conquest || cond.conquest == conquest) &&
(!cond.difficulty || cond.difficulty == difficulty) &&
(!cond.season || cond.season == season)
) {
applicable.push(cond.tag);
}
}
return applicable;
};
const buildMission = (
rng: SRng,
conquest: TConquestType,
missionType: TMissionType,
faction: TFaction,
season: CalendarSeasonType | null
): IConquestMission => {
const deviation = rng.randomElement(filterConditionals(deviations, missionType, conquest, null, season))!;
const easyRisk = rng.randomElement(filterConditionals(risks, missionType, conquest, "CD_NORMAL", season))!;
const hardRiskOptions = filterConditionals(risks, missionType, conquest, "CD_HARD", season);
{
const i = hardRiskOptions.indexOf(easyRisk);
if (i != -1) {
hardRiskOptions.splice(i, 1);
}
}
const hardRisk = rng.randomElement(hardRiskOptions)!;
return {
faction,
missionType,
difficulties: [
{
type: "CD_NORMAL",
deviation,
risks: [easyRisk]
},
{
type: "CD_HARD",
deviation,
risks: [easyRisk, hardRisk]
}
]
};
};
const conquestStartingDay: Record<TConquestType, number> = {
CT_LAB: 3703,
CT_HEX: 4053
};
// This function produces identical results to clients pre-40.0.0.
const getFrameVariables = (conquestType: TConquestType, time: number): [string, string, string, string] => {
const day = Math.floor((time - 1391990400_000) / 86400_000) - conquestStartingDay[conquestType];
const week = Math.floor(day / 7) + 1;
const frameVariables = [
"Framecurse",
"Knifestep",
"Exhaustion",
"Gearless",
"TimeDilation",
"Armorless",
"Starvation",
"ShieldDelay",
"Withering",
"ContactDamage",
"AbilityLockout",
"OperatorLockout",
"EnergyStarved",
"OverSensitive",
"AntiGuard",
"DecayingFlesh",
"VoidEnergyOverload",
"DullBlades",
"Undersupplied"
];
const mag = Math.floor(frameVariables.length / 4);
const rng = new SRng(conquestStartingDay[conquestType] + Math.floor(week / mag));
rng.shuffleArray(frameVariables);
const i = week % mag;
return [frameVariables[i], frameVariables[i + 1], frameVariables[i + 2], frameVariables[i + 3]];
};
export const getConquest = (
conquestType: TConquestType,
week: number,
season: CalendarSeasonType | null
): IConquest => {
const rng = new SRng(mixSeeds(conquestStartingDay[conquestType], week));
const missions: IConquestMission[] = [];
{
const missionOptions = Object.entries(missionAndFactionTypes[conquestType]);
{
const i = rng.randomInt(0, missionOptions.length - 1);
const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
missions.push(
buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
);
}
{
const i = rng.randomInt(0, missionOptions.length - 1);
const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
missions.push(
buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
);
}
missionOptions.push(["MT_ASSASSINATION", assassinationFactionOptions[conquestType]]);
{
const i = rng.randomInt(0, missionOptions.length - 1);
const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
missions.push(
buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
);
}
}
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
return {
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Type: conquestType,
Missions: missions,
Variables: getFrameVariables(conquestType, weekStart),
RandomSeed: rng.randomInt(0, 1_000_000)
};
};

View File

@ -41,6 +41,7 @@ import type {
import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers.ts";
import { logger } from "../utils/logger.ts";
import { DailyDeal, Fissure } from "../models/worldStateModel.ts";
import { getConquest } from "./conquestService.ts";
const sortieBosses = [
"SORTIE_BOSS_HYENA",
@ -3467,6 +3468,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}
}
// 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);
}
// Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST)
{
const rollover = getSortieTime(day);
@ -3549,16 +3562,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
}
// 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);
if (!buildLabel || version_compare(buildLabel, "2025.10.14.16.10") >= 0) {
worldState.Conquests = [];
{
const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[week % 4];
worldState.Conquests.push(getConquest("CT_LAB", week, null));
worldState.Conquests.push(getConquest("CT_HEX", week, season));
}
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));
}
pushVoidStorms(worldState.VoidStorms, hour - 1);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
pushVoidStorms(worldState.VoidStorms, hour);
}
// Sentient Anomaly + Xtra Cheese cycles

View File

@ -14,6 +14,7 @@ import type { IOrbiterClient } from "../personalRoomsTypes.ts";
import type { ICountedStoreItem } from "warframe-public-export-plus";
import type { IEquipmentClient, IEquipmentDatabase, ITraits } from "../equipmentTypes.ts";
import type { ILoadOutPresets } from "../saveLoadoutTypes.ts";
import type { CalendarSeasonType } from "../worldStateTypes.ts";
export type InventoryDatabaseEquipment = {
[_ in TEquipmentKey]: IEquipmentDatabase[];
@ -1180,7 +1181,7 @@ export interface IMarker {
}
export interface ISeasonProgress {
SeasonType: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
SeasonType: CalendarSeasonType;
LastCompletedDayIdx: number;
LastCompletedChallengeDayIdx: number;
ActivatedChallenges: string[];

View File

@ -32,6 +32,7 @@ export interface IWorldState {
ActiveChallenges: ISeasonChallenge[];
};
KnownCalendarSeasons: ICalendarSeason[];
Conquests?: IConquest[];
Tmp?: string;
}
@ -352,10 +353,11 @@ export interface ISeasonChallenge {
Challenge: string;
}
export type CalendarSeasonType = "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
export interface ICalendarSeason {
Activation: IMongoDate;
Expiry: IMongoDate;
Season: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
Season: CalendarSeasonType;
Days: ICalendarDay[];
YearIteration: number;
Version: number;
@ -416,6 +418,33 @@ export interface IGameMarketCategory {
Items?: string[];
}
// >= 40.0.0
export type TConquestType = "CT_LAB" | "CT_HEX";
export interface IConquest {
Activation: IMongoDate;
Expiry: IMongoDate;
Type: TConquestType;
Missions: IConquestMission[];
Variables: [string, string, string, string];
RandomSeed: number;
}
export interface IConquestMission {
faction: TFaction;
missionType: TMissionType;
difficulties: [
{
type: "CD_NORMAL";
deviation: string;
risks: [string];
},
{
type: "CD_HARD";
deviation: string;
risks: [string, string];
}
];
}
export interface ITmp {
cavabegin: string;
PurchasePlatformLockEnabled: boolean; // Seems unused
@ -423,6 +452,8 @@ export interface ITmp {
ennnd?: boolean; // True if 1999 demo is available (no effect for >=38.6.0)
mbrt?: boolean; // Related to mobile app rating request
fbst: IFbst;
lqo?: IConquestOverride;
hqo?: IConquestOverride;
sfn: number;
edg?: TCircuitGameMode[]; // The Circuit game modes overwrite
}
@ -451,3 +482,12 @@ interface IFbst {
e: number;
n: number;
}
// < 40.0.0
interface IConquestOverride {
mt?: string[]; // mission types but "Exterminate" instead of "MT_EXTERMINATION", etc. and "DualDefense" instead of "Defense" for hex conquest
mv?: string[];
mf?: number[]; // hex conquest only
c?: [string, string][];
fv?: string[];
}