返回提交历史
Modified
src/controllers/dynamic/worldStateController.ts
+162
-2
Added
src/helpers/worlstateHelper.ts
+130
-0
Modified
static/fixed_responses/worldState/worldState.json
+0
-17
XFEstudio/SpaceNinjaServer
feat: sortie rotation (#1453)
Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/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>
919f12b8
代码差异
3 个文件
+292
-19
@@ -10,8 +10,16 @@ import { unixTimesInMs } from "@/src/constants/timeConstants";
10
10
import { config } from "@/src/services/configService";
11
11
import { CRng } from "@/src/services/rngService";
12
12
import { ExportNightwave, ExportRegions } from "warframe-public-export-plus";
13
14
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
13
import {
14
EPOCH,
15
getSortieTime,
16
missionTags,
17
sortieBosses,
18
sortieBossNode,
19
sortieBossToFaction,
20
sortieFactionToFactionIndexes,
21
sortieFactionToSystemIndexes
22
} from "@/src/helpers/worlstateHelper";
15
23
16
24
export const worldStateController: RequestHandler = (req, res) => {
17
25
const day = Math.trunc((Date.now() - EPOCH) / 86400000);
@@ -27,6 +35,7 @@ export const worldStateController: RequestHandler = (req, res) => {
27
35
Time: config.worldState?.lockTime || Math.round(Date.now() / 1000),
28
36
Goals: [],
29
37
GlobalUpgrades: [],
38
Sorties: [],
30
39
LiteSorties: [],
31
40
EndlessXpChoices: [],
32
41
SeasonInfo: {
@@ -154,6 +163,142 @@ export const worldStateController: RequestHandler = (req, res) => {
154
163
});
155
164
}
156
165
166
// Sortie cycling every day
167
{
168
let genDay;
169
let dayStart;
170
let dayEnd;
171
const sortieRolloverToday = getSortieTime(day);
172
if (Date.now() < sortieRolloverToday) {
173
// Early in the day, generate sortie for `day - 1`, expiring at `sortieRolloverToday`.
174
genDay = day - 1;
175
dayStart = getSortieTime(genDay);
176
dayEnd = sortieRolloverToday;
177
} else {
178
// Late in the day, generate sortie for `day`, expiring at `getSortieTime(day + 1)`.
179
genDay = day;
180
dayStart = sortieRolloverToday;
181
dayEnd = getSortieTime(day + 1);
182
}
183
184
const rng = new CRng(genDay);
185
186
const boss = rng.randomElement(sortieBosses);
187
188
const modifiers = [
189
"SORTIE_MODIFIER_LOW_ENERGY",
190
"SORTIE_MODIFIER_IMPACT",
191
"SORTIE_MODIFIER_SLASH",
192
"SORTIE_MODIFIER_PUNCTURE",
193
"SORTIE_MODIFIER_EXIMUS",
194
"SORTIE_MODIFIER_MAGNETIC",
195
"SORTIE_MODIFIER_CORROSIVE",
196
"SORTIE_MODIFIER_VIRAL",
197
"SORTIE_MODIFIER_ELECTRICITY",
198
"SORTIE_MODIFIER_RADIATION",
199
"SORTIE_MODIFIER_GAS",
200
"SORTIE_MODIFIER_FIRE",
201
"SORTIE_MODIFIER_EXPLOSION",
202
"SORTIE_MODIFIER_FREEZE",
203
"SORTIE_MODIFIER_TOXIN",
204
"SORTIE_MODIFIER_POISON",
205
"SORTIE_MODIFIER_HAZARD_RADIATION",
206
"SORTIE_MODIFIER_HAZARD_MAGNETIC",
207
"SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest
208
"SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon
209
"SORTIE_MODIFIER_HAZARD_ICE",
210
"SORTIE_MODIFIER_HAZARD_COLD",
211
"SORTIE_MODIFIER_SECONDARY_ONLY",
212
"SORTIE_MODIFIER_SHOTGUN_ONLY",
213
"SORTIE_MODIFIER_SNIPER_ONLY",
214
"SORTIE_MODIFIER_RIFLE_ONLY",
215
"SORTIE_MODIFIER_MELEE_ONLY",
216
"SORTIE_MODIFIER_BOW_ONLY"
217
];
218
219
if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS");
220
if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR");
221
222
const nodes: string[] = [];
223
const availableMissionIndexes: number[] = [];
224
for (const [key, value] of Object.entries(ExportRegions)) {
225
if (
226
sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) &&
227
sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) &&
228
value.name.indexOf("Archwing") == -1 &&
229
value.missionIndex != 0 && // Exclude MT_ASSASSINATION
230
value.missionIndex != 5 && // Exclude MT_CAPTURE
231
value.missionIndex != 21 && // Exclude MT_PURIFY
232
value.missionIndex != 23 && // Exclude MT_JUNCTION
233
value.missionIndex <= 28
234
) {
235
if (!availableMissionIndexes.includes(value.missionIndex)) {
236
availableMissionIndexes.push(value.missionIndex);
237
}
238
nodes.push(key);
239
}
240
}
241
242
const selectedNodes: { missionType: string; modifierType: string; node: string }[] = [];
243
const missionTypes = new Set();
244
245
for (let i = 0; i < 3; i++) {
246
const randomIndex = rng.randomInt(0, nodes.length - 1);
247
const node = nodes[randomIndex];
248
let missionIndex = ExportRegions[node].missionIndex;
249
250
if (
251
!["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions
252
missionIndex != 28 &&
253
rng.randomInt(0, 2) == 2
254
) {
255
missionIndex = rng.randomElement(availableMissionIndexes);
256
}
257
258
if (i == 2 && rng.randomInt(0, 2) == 2) {
259
const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY");
260
const modifierType = rng.randomElement(filteredModifiers);
261
262
if (boss == "SORTIE_BOSS_PHORID") {
263
selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node });
264
nodes.splice(randomIndex, 1);
265
continue;
266
} else if (sortieBossNode[boss]) {
267
selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] });
268
continue;
269
}
270
}
271
272
const missionType = missionTags[missionIndex];
273
274
if (missionTypes.has(missionType)) {
275
i--;
276
continue;
277
}
278
279
const filteredModifiers =
280
missionType === "MT_TERRITORY"
281
? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION")
282
: modifiers;
283
284
const modifierType = rng.randomElement(filteredModifiers);
285
286
selectedNodes.push({ missionType, modifierType, node });
287
nodes.splice(randomIndex, 1);
288
missionTypes.add(missionType);
289
}
290
291
worldState.Sorties.push({
292
_id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" },
293
Activation: { $date: { $numberLong: dayStart.toString() } },
294
Expiry: { $date: { $numberLong: dayEnd.toString() } },
295
Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards",
296
Seed: genDay,
297
Boss: boss,
298
Variants: selectedNodes
299
});
300
}
301
157
302
// Archon Hunt cycling every week
158
303
{
159
304
const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3];
@@ -298,6 +443,7 @@ interface IWorldState {
298
443
Goals: IGoal[];
299
444
SyndicateMissions: ISyndicateMission[];
300
445
GlobalUpgrades: IGlobalUpgrade[];
446
Sorties: ISortie[];
301
447
LiteSorties: ILiteSortie[];
302
448
NodeOverrides: INodeOverride[];
303
449
EndlessXpChoices: IEndlessXpChoice[];
@@ -361,6 +507,20 @@ interface INodeOverride {
361
507
CustomNpcEncounters?: string;
362
508
}
363
509
510
interface ISortie {
511
_id: IOid;
512
Activation: IMongoDate;
513
Expiry: IMongoDate;
514
Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards";
515
Seed: number;
516
Boss: string;
517
Variants: {
518
missionType: string;
519
modifierType: string;
520
node: string;
521
}[];
522
}
523
364
524
interface ILiteSortie {
365
525
_id: IOid;
366
526
Activation: IMongoDate;
@@ -0,0 +1,130 @@
1
export const missionTags = [
2
"MT_ASSASSINATION",
3
"MT_EXTERMINATION",
4
"MT_SURVIVAL",
5
"MT_RESCUE",
6
"MT_SABOTAGE",
7
"MT_CAPTURE",
8
"MT_COUNTER_INTEL",
9
"MT_INTEL",
10
"MT_DEFENSE",
11
"MT_MOBILE_DEFENSE",
12
"MT_PVP",
13
"MT_MASTERY",
14
"MT_RECOVERY",
15
"MT_TERRITORY",
16
"MT_RETRIEVAL",
17
"MT_HIVE",
18
"MT_SALVAGE",
19
"MT_EXCAVATE",
20
"MT_RAID",
21
"MT_PURGE",
22
"MT_GENERIC",
23
"MT_PURIFY",
24
"MT_ARENA",
25
"MT_JUNCTION",
26
"MT_PURSUIT",
27
"MT_RACE",
28
"MT_ASSAULT",
29
"MT_EVACUATION",
30
"MT_LANDSCAPE",
31
"MT_RESOURCE_THEFT",
32
"MT_ENDLESS_EXTERMINATION",
33
"MT_ENDLESS_DUVIRI",
34
"MT_RAILJACK",
35
"MT_ARTIFACT",
36
"MT_CORRUPTION",
37
"MT_VOID_CASCADE",
38
"MT_ARMAGEDDON",
39
"MT_VAULTS",
40
"MT_ALCHEMY",
41
"MT_ASCENSION",
42
"MT_ENDLESS_CAPTURE",
43
"MT_OFFERING",
44
"MT_PVPVE"
45
];
46
47
export const sortieBosses = [
48
"SORTIE_BOSS_HYENA",
49
"SORTIE_BOSS_KELA",
50
"SORTIE_BOSS_VOR",
51
"SORTIE_BOSS_RUK",
52
"SORTIE_BOSS_HEK",
53
"SORTIE_BOSS_KRIL",
54
"SORTIE_BOSS_TYL",
55
"SORTIE_BOSS_JACKAL",
56
"SORTIE_BOSS_ALAD",
57
"SORTIE_BOSS_AMBULAS",
58
"SORTIE_BOSS_NEF",
59
"SORTIE_BOSS_RAPTOR",
60
"SORTIE_BOSS_PHORID",
61
"SORTIE_BOSS_LEPHANTIS",
62
"SORTIE_BOSS_INFALAD",
63
"SORTIE_BOSS_CORRUPTED_VOR"
64
];
65
66
export const sortieBossToFaction: Record<string, string> = {
67
SORTIE_BOSS_HYENA: "FC_CORPUS",
68
SORTIE_BOSS_KELA: "FC_GRINEER",
69
SORTIE_BOSS_VOR: "FC_GRINEER",
70
SORTIE_BOSS_RUK: "FC_GRINEER",
71
SORTIE_BOSS_HEK: "FC_GRINEER",
72
SORTIE_BOSS_KRIL: "FC_GRINEER",
73
SORTIE_BOSS_TYL: "FC_GRINEER",
74
SORTIE_BOSS_JACKAL: "FC_CORPUS",
75
SORTIE_BOSS_ALAD: "FC_CORPUS",
76
SORTIE_BOSS_AMBULAS: "FC_CORPUS",
77
SORTIE_BOSS_NEF: "FC_CORPUS",
78
SORTIE_BOSS_RAPTOR: "FC_CORPUS",
79
SORTIE_BOSS_PHORID: "FC_INFESTATION",
80
SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION",
81
SORTIE_BOSS_INFALAD: "FC_INFESTATION",
82
SORTIE_BOSS_CORRUPTED_VOR: "FC_CORRUPTED"
83
};
84
85
export const sortieFactionToSystemIndexes: Record<string, number[]> = {
86
FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 18],
87
FC_CORPUS: [1, 4, 7, 8, 12, 15],
88
FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15],
89
FC_CORRUPTED: [14]
90
};
91
92
export const sortieFactionToFactionIndexes: Record<string, number[]> = {
93
FC_GRINEER: [0],
94
FC_CORPUS: [1],
95
FC_INFESTATION: [0, 1, 2],
96
FC_CORRUPTED: [3]
97
};
98
99
export const sortieBossNode: Record<string, string> = {
100
SORTIE_BOSS_HYENA: "SolNode127",
101
SORTIE_BOSS_KELA: "SolNode193",
102
SORTIE_BOSS_VOR: "SolNode108",
103
SORTIE_BOSS_RUK: "SolNode32",
104
SORTIE_BOSS_HEK: "SolNode24",
105
SORTIE_BOSS_KRIL: "SolNode99",
106
SORTIE_BOSS_TYL: "SolNode105",
107
SORTIE_BOSS_JACKAL: "SolNode104",
108
SORTIE_BOSS_ALAD: "SolNode53",
109
SORTIE_BOSS_AMBULAS: "SolNode51",
110
SORTIE_BOSS_NEF: "SettlementNode20",
111
SORTIE_BOSS_RAPTOR: "SolNode210",
112
SORTIE_BOSS_LEPHANTIS: "SolNode712",
113
SORTIE_BOSS_INFALAD: "SolNode705"
114
};
115
116
export const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
117
118
export const getSortieTime = (day: number): number => {
119
const dayStart = EPOCH + day * 86400000;
120
const date = new Date(dayStart);
121
date.setUTCHours(12);
122
const isDst = new Intl.DateTimeFormat("en-US", {
123
timeZone: "America/Toronto",
124
timeZoneName: "short"
125
})
126
.formatToParts(date)
127
.find(part => part.type === "timeZoneName")!
128
.value.includes("DT");
129
return dayStart + (isDst ? 16 : 17) * 3600000;
130
};
@@ -62,23 +62,6 @@
62
62
}
63
63
}
64
64
],
65
"Sorties": [
66
{
67
"_id": { "$oid": "663a4c7d4d932c97c0a3acd7" },
68
"Activation": { "$date": { "$numberLong": "1715097600000" } },
69
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
70
"Reward": "/Lotus/Types/Game/MissionDecks/SortieRewards",
71
"Seed": 24491,
72
"Boss": "SORTIE_BOSS_TYL",
73
"ExtraDrops": [],
74
"Variants": [
75
{ "missionType": "MT_TERRITORY", "modifierType": "SORTIE_MODIFIER_ARMOR", "node": "SolNode122", "tileset": "GrineerOceanTileset" },
76
{ "missionType": "MT_MOBILE_DEFENSE", "modifierType": "SORTIE_MODIFIER_LOW_ENERGY", "node": "SolNode184", "tileset": "GrineerGalleonTileset" },
77
{ "missionType": "MT_LANDSCAPE", "modifierType": "SORTIE_MODIFIER_EXIMUS", "node": "SolNode228", "tileset": "EidolonTileset" }
78
],
79
"Twitter": true
80
}
81
],
82
65
"SyndicateMissions": [
83
66
{
84
67
"_id": { "$oid": "663a4fc5ba6f84724fa48049" },