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

SpaceNinjaServer

A simple server for a small space ninja game

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

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
AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
提交于

代码差异

3 个文件 +292 -19
Modified src/controllers/dynamic/worldStateController.ts +162 -2
@@ -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;
Added src/helpers/worlstateHelper.ts +130 -0
@@ -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 };
Modified static/fixed_responses/worldState/worldState.json +0 -17
@@ -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" },