XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFESpaceNinjaServer

feat: alert generation for closed beta to >24.3.0 (#4304)

I've got absolutely no idea on the "spawn" chance of some rewards. This should work with closed beta up to Fortuna 24.2.15 as 24.3 introduced Nightwaves. Tested on 22.13.4. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/4304 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Reviewed-by: dutlist <5+ameloninsidelemon@noreply.localhost> Co-authored-by: Vitruvio <16+vitruvio@noreply.localhost> Co-committed-by: Vitruvio <16+vitruvio@noreply.localhost>

3d7d3da8
Vitruvio <16+vitruvio@noreply.localhost>
提交于

代码差异

5 个文件 +549 -6
Modified src/constants/gameToBuildVersion.ts +2 -0
@@ -42,6 +42,7 @@ const gameToBuildVersion = {
42 42 "25.0.0": "2019.05.22.23.12",
43 43 "24.5.1": "2019.03.15.18.11",
44 44 "24.4.0": "2019.03.07.20.21",
45 "24.3.0": "2019.02.27.19.58", // guessed from hotfix forum post
45 46 "24.0.0": "2018.11.08.14.45",
46 47 "23.10.0": "2018.10.11.23.29",
47 48 "23.2.0": "2018.08.01.08.09",
@@ -71,6 +72,7 @@ const gameToBuildVersion = {
71 72 "15.0.6": "2014.10.27.17.07",
72 73 "15.0.0": "2014.10.24.08.24",
73 74 "14.0.0": "2014.07.21.18.38",
75 "13.2.3": "2014.05.08.12.08", // guesed from hotfix forum post
74 76 "13.0.0": "2014.04.10.17.47",
75 77 "12.5.2": "2014.03.21.12.01",
76 78 "12.1.2": "2014.02.14.17.05",
Modified src/controllers/dynamic/worldStateController.ts +4 -2
@@ -3,7 +3,8 @@ import {
3 3 getWorldState,
4 4 populateDailyDeal,
5 5 populateFeaturedGuilds,
6 populateFissures
6 populateFissures,
7 populateAlerts
7 8 } from "../../services/worldStateService.ts";
8 9 import { getAccountForRequest, getBuildLabel } from "../../services/loginService.ts";
9 10 import { BL_LATEST } from "../../constants/gameVersions.ts";
@@ -34,7 +35,8 @@ export const worldStateController: RequestHandler = async (req, res) => {
34 35 await Promise.all([
35 36 populateDailyDeal(worldState),
36 37 populateFeaturedGuilds(worldState),
37 populateFissures(worldState)
38 populateFissures(worldState),
39 populateAlerts(worldState)
38 40 ]);
39 41
40 42 if (elionWorkaroundNeeded) {
Modified src/models/worldStateModel.ts +30 -1
@@ -1,4 +1,4 @@
1 import type { IDailyDealDatabase, IFissureDatabase } from "../types/worldStateTypes.ts";
1 import type { IAlertDatabase, IDailyDealDatabase, IFissureDatabase } from "../types/worldStateTypes.ts";
2 2 import { model, Schema } from "mongoose";
3 3
4 4 const fissureSchema = new Schema<IFissureDatabase>({
@@ -28,3 +28,32 @@ dailyDealSchema.index({ StoreItem: 1 }, { unique: true });
28 28 dailyDealSchema.index({ Expiry: 1 }, { expireAfterSeconds: 86400 });
29 29
30 30 export const DailyDeal = model<IDailyDealDatabase>("DailyDeal", dailyDealSchema);
31
32 const alertSchema = new Schema<IAlertDatabase>({
33 Activation: { type: Date, required: true },
34 Expiry: { type: Date, required: true },
35 MissionInfo: {
36 location: { type: String, required: true },
37 missionType: { type: String, required: true },
38 faction: { type: String, required: true },
39 difficulty: { type: Number, required: true },
40 missionReward: {
41 credits: { type: Number, required: true },
42 items: [String],
43 countedItems: [
44 {
45 ItemType: String,
46 ItemCount: Number
47 }
48 ]
49 },
50 minEnemyLevel: { type: Number, required: true },
51 maxEnemyLevel: { type: Number, required: true },
52 descText: String,
53 nightmare: Boolean
54 }
55 });
56
57 alertSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 });
58
59 export const Alert = model<IAlertDatabase>("Alert", alertSchema);
Modified src/services/worldStateService.ts +492 -3
@@ -13,12 +13,13 @@ import { EPOCH, unixTimesInMs } from "../constants/timeConstants.ts";
13 13 import { config } from "./configService.ts";
14 14 import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "./rngService.ts";
15 15 import type { IMissionReward, IRegion, ITilesetMission, TFaction, TMissionType } from "warframe-public-export-plus";
16 import { ExportRegions, ExportSyndicates, ExportTilesets } from "warframe-public-export-plus";
16 import { ExportRegions, ExportSyndicates, ExportTilesets, ExportRecipes } from "warframe-public-export-plus";
17 17 import type {
18 18 ICalendarDay,
19 19 ICalendarEvent,
20 20 ICalendarSeason,
21 21 IAlert,
22 IAlertDatabase,
22 23 IGoal,
23 24 IInvasion,
24 25 ILiteSortie,
@@ -42,7 +43,8 @@ import type {
42 43 } from "../types/worldStateTypes.ts";
43 44 import { toMongoDate2, toOid, toOid2, version_compare } from "../helpers/inventoryHelpers.ts";
44 45 import { logger } from "../utils/logger.ts";
45 import { DailyDeal, Fissure } from "../models/worldStateModel.ts";
46 import { DailyDeal, Fissure, Alert } from "../models/worldStateModel.ts";
47 import { toStoreItem, fromStoreItem } from "./itemDataService.ts";
46 48 import { factionToInt, getConquest, getMissionTypeForLegacyOverride } from "./conquestService.ts";
47 49 import gameToBuildVersion from "../constants/gameToBuildVersion.ts";
48 50 import { getDescent } from "./descentService.ts";
@@ -5149,8 +5151,495 @@ const updateDailyDeal = async (): Promise<void> => {
5149 5151 } while (darvoEnd < Date.now() + 6 * unixTimesInMs.minute && ++darvoIndex);
5150 5152 };
5151 5153
5154 const alertStandardResources = [
5155 { path: "/Lotus/Types/Items/MiscItems/AlloyPlate", qty: 1500 },
5156 { path: "/Lotus/Types/Items/MiscItems/Circuits", qty: 1500 },
5157 { path: "/Lotus/Types/Items/MiscItems/ControlModule", qty: 1 },
5158 { path: "/Lotus/Types/Items/MiscItems/Ferrite", qty: 3000 },
5159 { path: "/Lotus/Types/Items/MiscItems/Gallium", qty: 1 },
5160 { path: "/Lotus/Types/Items/MiscItems/Morphic", qty: 1 },
5161 { path: "/Lotus/Types/Items/MiscItems/Nanospores", qty: 3000 },
5162 { path: "/Lotus/Types/Items/MiscItems/NeuralSensor", qty: 1 },
5163 { path: "/Lotus/Types/Items/MiscItems/Neurode", qty: 1 },
5164 { path: "/Lotus/Types/Items/MiscItems/OrokinCell", qty: 1 },
5165 { path: "/Lotus/Types/Items/MiscItems/Plastids", qty: 300 },
5166 { path: "/Lotus/Types/Items/MiscItems/PolymerBundle", qty: 300 },
5167 { path: "/Lotus/Types/Items/MiscItems/Rubedo", qty: 450 },
5168 { path: "/Lotus/Types/Items/MiscItems/Salvage", qty: 300 },
5169 { path: "/Lotus/Types/Items/MiscItems/ArgonCrystal", qty: 1 },
5170 { path: "/Lotus/Types/Items/MiscItems/OxiumAlloy", qty: 300 },
5171 { path: "/Lotus/Types/Items/MiscItems/Tellurium", qty: 1 }
5172 ];
5173
5174 const alertSpecialResources = [
5175 { path: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", qty: 1 },
5176 { path: "/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", qty: 5 },
5177 { path: "/Lotus/Types/Items/MiscItems/Eventium", qty: 5 },
5178 { path: "/Lotus/Types/Items/MiscItems/Alertium", qty: 1 },
5179 { path: "/Lotus/Types/Items/MiscItems/VoidTearDrop", qty: 20 }
5180 ];
5181
5182 const alertAuras = [
5183 "/Lotus/Upgrades/Mods/Aura/PlayerEnemyRadarAuraMod",
5184 "/Lotus/Upgrades/Mods/Aura/PlayerEnergyRegenAuraMod",
5185 "/Lotus/Upgrades/Mods/Aura/PlayerHealthRegenAuraMod",
5186 "/Lotus/Upgrades/Mods/Aura/PlayerMeleeAuraMod",
5187 "/Lotus/Upgrades/Mods/Aura/PlayerPistolAmmoAuraMod",
5188 "/Lotus/Upgrades/Mods/Aura/PlayerRifleAmmoAuraMod",
5189 "/Lotus/Upgrades/Mods/Aura/PlayerRifleDamageAuraMod",
5190 "/Lotus/Upgrades/Mods/Aura/PlayerShellAmmoAuraMod",
5191 "/Lotus/Upgrades/Mods/Aura/PlayerSniperAmmoAuraMod",
5192 "/Lotus/Upgrades/Mods/Aura/PlayerHealthAuraMod",
5193 "/Lotus/Upgrades/Mods/Aura/EnemyArmorReductionAuraMod",
5194 "/Lotus/Upgrades/Mods/Aura/EnemyShieldReductionAuraMod",
5195 "/Lotus/Upgrades/Mods/Aura/InfestationSpeedReductionAuraMod",
5196 "/Lotus/Upgrades/Mods/Aura/PlayerHolsterSpeedAuraMod",
5197 "/Lotus/Upgrades/Mods/Aura/PlayerSprintAuraMod",
5198 "/Lotus/Upgrades/Mods/Aura/PlayerSniperDamageAuraMod",
5199 "/Lotus/Upgrades/Mods/Aura/PlayerLootRadarAuraMod",
5200 "/Lotus/Upgrades/Mods/Aura/RobotPoorAimAuraMod"
5201 ];
5202
5203 const alertHelmets = [
5204 "/Lotus/Types/Recipes/Helmets/StatlessAshAltHelmetBlueprint",
5205 "/Lotus/Types/Recipes/Helmets/StatlessBansheeAltHelmetBlueprint",
5206 "/Lotus/Types/Recipes/Helmets/StatlessEmberAltHelmetBlueprint",
5207 "/Lotus/Types/Recipes/Helmets/StatlessExcaliburAltHelmetBlueprint",
5208 "/Lotus/Types/Recipes/Helmets/StatlessFrostAltHelmetBlueprint",
5209 "/Lotus/Types/Recipes/Helmets/StatlessLokiAltHelmetBlueprint",
5210 "/Lotus/Types/Recipes/Helmets/StatlessMagAltHelmetBlueprint",
5211 "/Lotus/Types/Recipes/Helmets/StatlessNyxAltHelmetBlueprint",
5212 "/Lotus/Types/Recipes/Helmets/StatlessRhinoAltHelmetBlueprint",
5213 "/Lotus/Types/Recipes/Helmets/StatlessSarynAltHelmetBlueprint",
5214 "/Lotus/Types/Recipes/Helmets/StatlessTrinityAltHelmetBlueprint",
5215 "/Lotus/Types/Recipes/Helmets/StatlessVoltAltHelmetBlueprint",
5216 "/Lotus/Types/Recipes/Helmets/ValkyrBastetHelmetBlueprint",
5217 "/Lotus/Types/Recipes/Helmets/OberonAltHelmetBlueprint",
5218 "/Lotus/Types/Recipes/Helmets/ZephyrCierzoHelmetBlueprint",
5219 "/Lotus/Types/Recipes/Helmets/HarlequinAltHelmetBlueprint",
5220 "/Lotus/Types/Recipes/Helmets/LimboAltBHelmetBlueprint",
5221 "/Lotus/Types/Recipes/Helmets/MirageAltBHelmetBlueprint",
5222 "/Lotus/Types/Recipes/Helmets/StatlessV2AshAltHelmetBlueprint",
5223 "/Lotus/Types/Recipes/Helmets/StatlessV2BansheeAltHelmetBlueprint",
5224 "/Lotus/Types/Recipes/Helmets/StatlessV2EmberAltHelmetBlueprint",
5225 "/Lotus/Types/Recipes/Helmets/StatlessV2ExcaliburAltHelmetBlueprint",
5226 "/Lotus/Types/Recipes/Helmets/StatlessV2FrostAltHelmetBlueprint",
5227 "/Lotus/Types/Recipes/Helmets/StatlessV2LokiAltHelmetBlueprint",
5228 "/Lotus/Types/Recipes/Helmets/StatlessV2MagAltHelmetBlueprint",
5229 "/Lotus/Types/Recipes/Helmets/StatlessV2NyxAltHelmetBlueprint",
5230 "/Lotus/Types/Recipes/Helmets/StatlessV2RhinoAltHelmetBlueprint",
5231 "/Lotus/Types/Recipes/Helmets/StatlessV2SarynAltHelmetBlueprint",
5232 "/Lotus/Types/Recipes/Helmets/StatlessV2TrinityAltHelmetBlueprint",
5233 "/Lotus/Types/Recipes/Helmets/StatlessV2VoltAltHelmetBlueprint",
5234 "/Lotus/Types/Recipes/Helmets/OberonAltBHelmetBlueprint",
5235 "/Lotus/Types/Recipes/Helmets/ValkyrAltBHelmetBlueprint",
5236 "/Lotus/Types/Recipes/Helmets/PirateAltHelmetBlueprint",
5237 "/Lotus/Types/Recipes/Helmets/LimboAristeasHelmetBlueprint",
5238 "/Lotus/Types/Recipes/Helmets/ZephyrTenguHelmetBlueprint",
5239 "/Lotus/Types/Recipes/Helmets/CowgirlAltHelmetBlueprint",
5240 "/Lotus/Types/Recipes/Helmets/MesaAltBHelmetBlueprint",
5241 "/Lotus/Types/Recipes/Helmets/DragonAltHelmetBlueprint",
5242 "/Lotus/Types/Recipes/Helmets/ChromaAltBHelmetBlueprint",
5243 "/Lotus/Types/Recipes/Helmets/VaubanHelmetSoldierBlueprint",
5244 "/Lotus/Types/Recipes/Helmets/ExcaliburMordredHelmetBlueprint",
5245 "/Lotus/Types/Recipes/Helmets/AnimaAltHelmetBlueprint",
5246 "/Lotus/Types/Recipes/Helmets/RangerAltHelmetBlueprint",
5247 "/Lotus/Types/Recipes/Helmets/NezhaAltHelmetBlueprint",
5248 "/Lotus/Types/Recipes/Helmets/SandmanAltHelmetBlueprint",
5249 "/Lotus/Types/Recipes/Helmets/BrawlerAltTwoHelmetBlueprint",
5250 "/Lotus/Types/Recipes/Helmets/StatlessVaubanAltHelmetBlueprint",
5251 "/Lotus/Types/Recipes/Helmets/StatlessV2VaubanAltHelmetBlueprint",
5252 "/Lotus/Types/Recipes/Helmets/StatlessNovaAltHelmetBlueprint",
5253 "/Lotus/Types/Recipes/Helmets/NekrosAraknidHelmetBlueprint",
5254 "/Lotus/Types/Recipes/Helmets/NovaQuantumHelmetBlueprint",
5255 "/Lotus/Types/Recipes/Helmets/NekrosShroudHelmetBlueprint",
5256 "/Lotus/Types/Recipes/Helmets/NovaSlipstreamHelmetBlueprint",
5257 "/Lotus/Types/Recipes/Helmets/LokiEnigmaHelmetBlueprint",
5258 "/Lotus/Types/Recipes/Helmets/PirateAltBHelmetBlueprint",
5259 "/Lotus/Types/Recipes/Helmets/BrawlerAltHelmetBlueprint",
5260 "/Lotus/Types/Recipes/Helmets/WukongAltHelmetBlueprint",
5261 "/Lotus/Types/Recipes/Helmets/FairyAltHelmetBlueprint",
5262 "/Lotus/Types/Recipes/Helmets/NidusAltHelmetBlueprint",
5263 "/Lotus/Types/Recipes/Helmets/SandmanAltBHelmetBlueprint",
5264 "/Lotus/Types/Recipes/Helmets/RangerAltBHelmetBlueprint",
5265 "/Lotus/Types/Recipes/Helmets/BardAltHelmetBlueprint"
5266 ];
5267
5268 const alertVaubanParts = [
5269 "/Lotus/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint",
5270 "/Lotus/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint",
5271 "/Lotus/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint"
5272 ];
5273
5274 const alertWeapons = [
5275 "/Lotus/Types/Recipes/Weapons/CeramicDaggerBlueprint",
5276 "/Lotus/Types/Recipes/Weapons/DarkDaggerBlueprint",
5277 "/Lotus/Types/Recipes/Weapons/HeatDaggerBlueprint",
5278 "/Lotus/Types/Recipes/Weapons/HeatSwordBlueprint",
5279 "/Lotus/Types/Recipes/Weapons/JawBlueprint",
5280 "/Lotus/Types/Recipes/Weapons/PangolinSwordBlueprint",
5281 "/Lotus/Types/Recipes/Weapons/PlasmaSwordBlueprint",
5282 "/Lotus/Types/Recipes/Weapons/GlaiveBlueprint",
5283 "/Lotus/Types/Recipes/DarkSwordBlueprint",
5284 "/Lotus/Types/Recipes/Weapons/Skins/DaggerAxeBlueprint",
5285 "/Lotus/Types/Recipes/Weapons/Skins/DualDaggerAxeBlueprint",
5286 "/Lotus/Types/Recipes/Weapons/Skins/GrnHammerBlueprint",
5287 "/Lotus/Types/Recipes/Weapons/Skins/GrnAxeBlueprint"
5288 ];
5289
5290 const alertNightmareMods = [
5291 "/Lotus/Upgrades/Mods/Pistol/DualStat/StunningSpeedMod",
5292 "/Lotus/Upgrades/Mods/Shotgun/DualStat/BlazeMod",
5293 "/Lotus/Upgrades/Mods/Rifle/DualStat/WildfireMod",
5294 "/Lotus/Upgrades/Mods/Shotgun/DualStat/AcceleratedBlastMod",
5295 "/Lotus/Upgrades/Mods/Warframe/DualStat/ConstitutionMod",
5296 "/Lotus/Upgrades/Mods/Pistol/DualStat/IceStormMod",
5297 "/Lotus/Upgrades/Mods/Warframe/DualStat/FortitudeMod",
5298 "/Lotus/Upgrades/Mods/Rifle/DualStat/HammerShotMod",
5299 "/Lotus/Upgrades/Mods/Melee/DualStat/FocusEnergyMod",
5300 "/Lotus/Upgrades/Mods/Melee/DualStat/RendingStrikeMod",
5301 "/Lotus/Upgrades/Mods/Rifle/DualStat/ShredMod",
5302 "/Lotus/Upgrades/Mods/Pistol/DualStat/GrinderMod",
5303 "/Lotus/Upgrades/Mods/Warframe/DualStat/VigorMod",
5304 "/Lotus/Upgrades/Mods/Warframe/DualStat/RunSpeedArmorMod",
5305 "/Lotus/Upgrades/Mods/Shotgun/DualStat/ReloadSpeedPunchThroughMod"
5306 ];
5307
5308 const alertOrokinBP = [
5309 "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
5310 "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
5311 "/Lotus/Types/Recipes/Components/FormaBlueprint"
5312 ];
5313
5314 const alertDurationMultipliers = new Map<string, number>([
5315 ["/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint", 2],
5316 ["/Lotus/Types/Recipes/Components/OrokinReactorBlueprint", 2],
5317 ["/Lotus/Types/Recipes/Components/FormaBlueprint", 2],
5318 ["/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", 2],
5319 ["/Lotus/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint", 2],
5320 ["/Lotus/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint", 2],
5321 ["/Lotus/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint", 2],
5322 ["/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", 2],
5323 ["/Lotus/Types/Items/MiscItems/Eventium", 2]
5324 ]);
5325
5326 const getVersionAppropriateHelmet = (helmetPath: string, buildLabel: string): string => {
5327 let isStore = false;
5328 let typePath = helmetPath;
5329 if (helmetPath.startsWith("/Lotus/StoreItems/")) {
5330 isStore = true;
5331 typePath = "/Lotus/" + helmetPath.substring("/Lotus/StoreItems/".length);
5332 }
5333
5334 const isPreU13_2_3 = version_compare(buildLabel, gameToBuildVersion["13.2.3"]) < 0; // arcane helmets were available until U13.2.3
5335 let resultPath = typePath;
5336
5337 if (isPreU13_2_3) {
5338 if (typePath.includes("Statless")) {
5339 const arcanePath = typePath.replace("Statless", "");
5340 if (arcanePath in ExportRecipes) {
5341 resultPath = arcanePath;
5342 }
5343 }
5344 } else {
5345 if (!typePath.includes("Statless")) {
5346 const baseName = typePath.replace("/Lotus/Types/Recipes/Helmets/", "");
5347 const statlessPath = "/Lotus/Types/Recipes/Helmets/Statless" + baseName;
5348 if (statlessPath in ExportRecipes) {
5349 resultPath = statlessPath;
5350 }
5351 }
5352 }
5353
5354 return isStore ? toStoreItem(resultPath) : resultPath;
5355 };
5356
5357 const getEligibleAlertNodes = (): string[] => {
5358 const eligibleNodes: string[] = [];
5359 const validMissionTypes = new Set([
5360 "MT_SURVIVAL",
5361 "MT_DEFENSE",
5362 "MT_RESCUE",
5363 "MT_CAPTURE",
5364 "MT_EXTERMINATION",
5365 "MT_SABOTAGE",
5366 "MT_MOBILE_DEFENSE",
5367 "MT_EXCAVATE",
5368 "MT_INTEL"
5369 ]);
5370 const validFactions = new Set(["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION", "FC_CORRUPTED", "FC_OROKIN"]);
5371 const invalidSystems = new Set([
5372 "/Lotus/Language/Locations/Moon",
5373 "/Lotus/Language/Locations/Derelict",
5374 "/Lotus/Language/Locations/Fortress",
5375 "/Lotus/Language/Locations/RelayStationSanctuary"
5376 ]);
5377
5378 for (const [nodeId, nodeData] of Object.entries(ExportRegions)) {
5379 if (!nodeId.startsWith("SolNode") && !nodeId.startsWith("SettlementNode")) {
5380 continue;
5381 }
5382 if (nodeData.nodeType !== 0) {
5383 continue;
5384 }
5385 if (!validMissionTypes.has(nodeData.missionType)) {
5386 continue;
5387 }
5388 if (!nodeData.faction || !validFactions.has(nodeData.faction)) {
5389 continue;
5390 }
5391 if (invalidSystems.has(nodeData.systemName) || nodeData.systemName.endsWith("_SPACE")) {
5392 continue;
5393 }
5394 eligibleNodes.push(nodeId);
5395 }
5396 return eligibleNodes;
5397 };
5398
5399 const spawnAlert = async (activeNodes: Set<string>): Promise<any> => {
5400 const eligibleNodes = getEligibleAlertNodes();
5401 if (eligibleNodes.length === 0) return null;
5402
5403 const availableNodes = eligibleNodes.filter(node => !activeNodes.has(node));
5404 const nodeId = getRandomElement(availableNodes.length > 0 ? availableNodes : eligibleNodes)!;
5405 const nodeData = ExportRegions[nodeId];
5406
5407 const missionTypes = [
5408 "MT_SURVIVAL",
5409 "MT_DEFENSE",
5410 "MT_RESCUE",
5411 "MT_CAPTURE",
5412 "MT_EXTERMINATION",
5413 "MT_SABOTAGE",
5414 "MT_MOBILE_DEFENSE",
5415 "MT_EXCAVATE",
5416 "MT_INTEL"
5417 ];
5418 const missionType = Math.random() < 0.7 ? nodeData.missionType : getRandomElement(missionTypes)!;
5419
5420 const factions = ["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION", "FC_CORRUPTED"];
5421 let faction = Math.random() < 0.7 ? nodeData.faction : getRandomElement(factions)!;
5422 if (faction === "FC_OROKIN") {
5423 faction = "FC_CORRUPTED";
5424 }
5425
5426 const difficulty = parseFloat((0.1 + Math.random() * 0.9).toFixed(2));
5427 const minEnemyLevel = Math.round(10 + difficulty * 20);
5428 const maxEnemyLevel = minEnemyLevel + Math.round(5 + Math.random() * 5);
5429
5430 let rewardCredits = Math.floor(2000 + difficulty * 18000);
5431
5432 let rewardItems: string[] | undefined = undefined;
5433 let rewardCountedItems: { ItemType: string; ItemCount: number }[] | undefined = undefined;
5434 let isNightmare = false;
5435
5436 const categories = [
5437 { name: "CREDITS", weight: 60 },
5438 { name: "STANDARD_RESOURCES", weight: 120 },
5439 { name: "ENDO", weight: 40 },
5440 { name: "ALT_HELMETS", weight: 80 },
5441 { name: "WEAPONS", weight: 35 },
5442 { name: "NIGHTMARE_MODS", weight: 30 },
5443 { name: "AURAS", weight: 18 },
5444 { name: "SPECIAL_RESOURCES", weight: 10 },
5445 { name: "VAUBAN_PARTS", weight: 7 },
5446 { name: "OROKIN_BP", weight: 4 }
5447 ];
5448
5449 let totalWeight = 0;
5450 for (const cat of categories) {
5451 totalWeight += cat.weight;
5452 }
5453
5454 let roll = Math.random() * totalWeight;
5455 let selectedCategory = "STANDARD_RESOURCES";
5456 for (const cat of categories) {
5457 if (roll < cat.weight) {
5458 selectedCategory = cat.name;
5459 break;
5460 }
5461 roll -= cat.weight;
5462 }
5463
5464 switch (selectedCategory) {
5465 case "CREDITS": {
5466 rewardCredits = Math.floor(5000 + difficulty * 15000);
5467 break;
5468 }
5469 case "STANDARD_RESOURCES": {
5470 const res = getRandomElement(alertStandardResources)!;
5471 rewardCountedItems = [{ ItemType: res.path, ItemCount: res.qty }];
5472 break;
5473 }
5474 case "ENDO": {
5475 const endoRoll = Math.random() * 100;
5476 let endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleSmall";
5477 if (endoRoll < 60) {
5478 endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleSmall";
5479 } else if (endoRoll < 90) {
5480 endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleMedium";
5481 } else {
5482 endoPath = "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/AlertFusionBundleLarge";
5483 }
5484 rewardItems = [endoPath];
5485 break;
5486 }
5487 case "SPECIAL_RESOURCES": {
5488 const res = getRandomElement(alertSpecialResources)!;
5489 rewardCountedItems = [{ ItemType: res.path, ItemCount: res.qty }];
5490 break;
5491 }
5492 case "AURAS": {
5493 const aura = getRandomElement(alertAuras)!;
5494 rewardItems = [toStoreItem(aura)];
5495 break;
5496 }
5497 case "ALT_HELMETS": {
5498 const helmet = getRandomElement(alertHelmets)!;
5499 rewardItems = [toStoreItem(helmet)];
5500 break;
5501 }
5502 case "VAUBAN_PARTS": {
5503 const part = getRandomElement(alertVaubanParts)!;
5504 rewardItems = [toStoreItem(part)];
5505 break;
5506 }
5507 case "WEAPONS": {
5508 const weapon = getRandomElement(alertWeapons)!;
5509 rewardItems = [toStoreItem(weapon)];
5510 break;
5511 }
5512 case "NIGHTMARE_MODS": {
5513 const nmMod = getRandomElement(alertNightmareMods)!;
5514 rewardItems = [toStoreItem(nmMod)];
5515 isNightmare = true;
5516 break;
5517 }
5518 case "OROKIN_BP": {
5519 const specialBP = getRandomElement(alertOrokinBP)!;
5520 rewardItems = [toStoreItem(specialBP)];
5521 break;
5522 }
5523 }
5524
5525 let durationMin = 30 + Math.random() * 40;
5526 let multiplier = 1;
5527 if (rewardItems && rewardItems.length > 0) {
5528 multiplier = alertDurationMultipliers.get(fromStoreItem(rewardItems[0])) ?? 1;
5529 } else if (rewardCountedItems && rewardCountedItems.length > 0) {
5530 multiplier = alertDurationMultipliers.get(rewardCountedItems[0].ItemType) ?? 1;
5531 }
5532 durationMin *= multiplier;
5533
5534 const activationDate = new Date(Date.now() - 10 * 60 * 1000);
5535 const expiryDate = new Date(activationDate.getTime() + durationMin * 60 * 1000);
5536
5537 const newAlert = new Alert({
5538 Activation: activationDate,
5539 Expiry: expiryDate,
5540 MissionInfo: {
5541 location: nodeId,
5542 missionType: missionType,
5543 faction: faction,
5544 difficulty: difficulty,
5545 missionReward: {
5546 credits: rewardCredits,
5547 items: rewardItems,
5548 countedItems: rewardCountedItems
5549 },
5550 minEnemyLevel: minEnemyLevel,
5551 maxEnemyLevel: maxEnemyLevel,
5552 nightmare: isNightmare || undefined
5553 }
5554 });
5555 return await newAlert.save();
5556 };
5557
5558 const updateAlerts = async (): Promise<void> => {
5559 const alerts = await Alert.find();
5560 const activeAlerts: any[] = [];
5561 const activeNodes = new Set<string>();
5562 let latestActivation = 0;
5563
5564 for (const alert of alerts) {
5565 if (alert.Expiry.getTime() > Date.now()) {
5566 activeAlerts.push(alert);
5567 activeNodes.add(alert.MissionInfo.location);
5568 }
5569 latestActivation = Math.max(latestActivation, alert.Activation.getTime());
5570 }
5571
5572 while (activeAlerts.length < 3) {
5573 const newAlert = (await spawnAlert(activeNodes)) as IAlertDatabase | null;
5574 if (newAlert) {
5575 activeAlerts.push(newAlert);
5576 activeNodes.add(newAlert.MissionInfo.location);
5577 latestActivation = Math.max(latestActivation, newAlert.Activation.getTime());
5578 } else {
5579 break;
5580 }
5581 }
5582
5583 if (activeAlerts.length < 5 && latestActivation > 0) {
5584 const timeSinceLastSpawn = Date.now() - latestActivation;
5585 const interval = 20 + Math.random() * 20;
5586 if (timeSinceLastSpawn > interval * 60 * 1000) {
5587 const newAlert = (await spawnAlert(activeNodes)) as IAlertDatabase | null;
5588 if (newAlert) {
5589 activeAlerts.push(newAlert);
5590 activeNodes.add(newAlert.MissionInfo.location);
5591 }
5592 }
5593 }
5594 };
5595
5596 export const populateAlerts = async (worldState: IWorldState): Promise<void> => {
5597 const buildLabel = worldState.BuildLabel;
5598 if (
5599 version_compare(buildLabel, gameToBuildVersion["5.1.0"]) >= 0 &&
5600 version_compare(buildLabel, gameToBuildVersion["24.3.0"]) < 0 // alerts were retired with 23.3.0
5601 ) {
5602 const activeAlerts = await Alert.find({ Expiry: { $gt: new Date() } });
5603 for (const dbAlert of activeAlerts) {
5604 let mappedItems: string[] | undefined = undefined;
5605 if (dbAlert.MissionInfo.missionReward.items) {
5606 mappedItems = dbAlert.MissionInfo.missionReward.items.map(item => {
5607 if (item.includes("/Recipes/Helmets/")) {
5608 return getVersionAppropriateHelmet(item, buildLabel);
5609 }
5610 return item;
5611 });
5612 }
5613
5614 worldState.Alerts.push({
5615 _id: toOid2(dbAlert._id.toString(), buildLabel),
5616 Activation:
5617 dbAlert.Activation.getTime() < Date.now()
5618 ? toMongoDate2(1000000000000, buildLabel)
5619 : toMongoDate2(dbAlert.Activation.getTime(), buildLabel),
5620 Expiry: toMongoDate2(dbAlert.Expiry.getTime(), buildLabel),
5621 MissionInfo: {
5622 location: dbAlert.MissionInfo.location,
5623 missionType: dbAlert.MissionInfo.missionType as TMissionType,
5624 faction: dbAlert.MissionInfo.faction as TFaction,
5625 difficulty: dbAlert.MissionInfo.difficulty,
5626 missionReward: {
5627 credits: dbAlert.MissionInfo.missionReward.credits,
5628 items: mappedItems,
5629 countedItems: dbAlert.MissionInfo.missionReward.countedItems
5630 },
5631 minEnemyLevel: dbAlert.MissionInfo.minEnemyLevel,
5632 maxEnemyLevel: dbAlert.MissionInfo.maxEnemyLevel,
5633 descText: dbAlert.MissionInfo.descText,
5634 nightmare: dbAlert.MissionInfo.nightmare || undefined
5635 }
5636 });
5637 }
5638 }
5639 };
5640
5152 5641 export const updateWorldStateCollections = async (): Promise<void> => {
5153 await Promise.all([updateFissures(), updateDailyDeal()]);
5642 await Promise.all([updateFissures(), updateDailyDeal(), updateAlerts()]);
5154 5643 };
5155 5644
5156 5645 const pushConclaveDaily = (
Modified src/types/worldStateTypes.ts +21 -0
@@ -114,10 +114,31 @@ export interface IAlertMissionInfo {
114 114 leadersAlwaysAllowed?: true;
115 115 seed?: number;
116 116 enemyCacheOverride?: string;
117 nightmare?: boolean;
117 118
118 119 maxRotations?: number; // SNS specific field
119 120 }
120 121
122 export interface IAlertDatabase {
123 Activation: Date;
124 Expiry: Date;
125 MissionInfo: {
126 location: string;
127 missionType: string;
128 faction: string;
129 difficulty: number;
130 missionReward: {
131 credits: number;
132 items?: string[];
133 countedItems?: { ItemType: string; ItemCount: number }[];
134 };
135 minEnemyLevel: number;
136 maxEnemyLevel: number;
137 descText?: string;
138 nightmare?: boolean;
139 };
140 }
141
121 142 export interface IGoal extends Omit<IGoalV9, "GoalInterim" | "GoalInterim2" | "RewardInterim" | "RewardInterim2"> {
122 143 GracePeriod?: IMongoDate; // U41+
123 144