返回提交历史
Modified
src/constants/gameToBuildVersion.ts
+2
-0
Modified
src/controllers/dynamic/worldStateController.ts
+4
-2
Modified
src/models/worldStateModel.ts
+30
-1
Modified
src/services/worldStateService.ts
+492
-3
Modified
src/types/worldStateTypes.ts
+21
-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
代码差异
5 个文件
+549
-6
@@ -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",
@@ -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) {
@@ -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);
@@ -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 = (
@@ -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