返回提交历史
Modified
src/services/missionInventoryUpdateService.ts
+20
-9
Modified
src/services/worldStateService.ts
+173
-23
Modified
src/types/requestTypes.ts
+1
-0
Modified
src/types/worldStateTypes.ts
+5
-0
XFEstudio/XFESpaceNinjaServer
feat: alert generator (#3184)
alert generator for blood of perita and belly of the beast Re #1103 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3184 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
e8384a3f
代码差异
4 个文件
+199
-32
@@ -653,7 +653,9 @@ export const addMissionInventoryUpdates = async (
653
653
}
654
654
case "GoalProgress": {
655
655
for (const uploadProgress of value) {
656
const goal = getWorldState().Goals.find(x => x._id.$oid == uploadProgress._id.$oid);
656
const goal = getWorldState(account.BuildLabel).Goals.find(
657
x => x._id.$oid == uploadProgress._id.$oid
658
);
657
659
if (goal && goal.Personal) {
658
660
inventory.PersonalGoalProgress ??= [];
659
661
const goalProgress = inventory.PersonalGoalProgress.find(x => x.goalId.equals(goal._id.$oid));
@@ -1159,6 +1161,7 @@ export const addMissionRewards = async (
1159
1161
RewardInfo: rewardInfo,
1160
1162
LevelKeyName: levelKeyName,
1161
1163
Missions: missions,
1164
Alerts: alerts,
1162
1165
RegularCredits: creditDrops,
1163
1166
VoidTearParticipantsCurrWave: voidTearWave,
1164
1167
StrippedItems: strippedItems,
@@ -1179,6 +1182,7 @@ export const addMissionRewards = async (
1179
1182
1180
1183
//TODO: check double reward merging
1181
1184
const MissionRewards: IMissionReward[] = getRandomMissionDrops(
1185
account,
1182
1186
inventory,
1183
1187
rewardInfo,
1184
1188
levelKeyName,
@@ -1188,13 +1192,14 @@ export const addMissionRewards = async (
1188
1192
);
1189
1193
logger.debug("random mission drops:", MissionRewards);
1190
1194
const inventoryChanges: IInventoryChanges = {};
1195
const isSteelPath = missions?.Tier || alerts?.Tier;
1191
1196
let SyndicateXPItemReward;
1192
1197
let ConquestCompletedMissionsCount;
1193
1198
1194
1199
let missionCompletionCredits = 0;
1195
1200
1196
1201
if (rewardInfo.alertId) {
1197
const alert = getWorldState().Alerts.find(x => x._id.$oid == rewardInfo.alertId);
1202
const alert = getWorldState(account.BuildLabel).Alerts.find(x => x._id.$oid == rewardInfo.alertId);
1198
1203
if (!alert) {
1199
1204
logger.warn(`mission completed unknown alert`, { alertId: rewardInfo.alertId });
1200
1205
} else {
@@ -1205,6 +1210,13 @@ export const addMissionRewards = async (
1205
1210
inventory.CompletedAlerts.push(alert._id.$oid);
1206
1211
}
1207
1212
if (alert.MissionInfo.missionReward) {
1213
if (alert.Tag && ["12MinWarEvent", "JadeShadows"].includes(alert.Tag) && isSteelPath) {
1214
if (alert.MissionInfo.missionReward.countedItems) {
1215
alert.MissionInfo.missionReward.countedItems.forEach(item => {
1216
item.ItemCount *= 1.5;
1217
});
1218
}
1219
}
1208
1220
missionCompletionCredits += addFixedLevelRewards(
1209
1221
alert.MissionInfo.missionReward,
1210
1222
MissionRewards,
@@ -1231,7 +1243,7 @@ export const addMissionRewards = async (
1231
1243
}
1232
1244
1233
1245
if (rewardInfo.goalId) {
1234
const goal = getWorldState().Goals.find(x => x._id.$oid == rewardInfo.goalId);
1246
const goal = getWorldState(account.BuildLabel).Goals.find(x => x._id.$oid == rewardInfo.goalId);
1235
1247
if (goal) {
1236
1248
if (rewardInfo.node == goal.Node && goal.MissionKeyName) levelKeyName = goal.MissionKeyName;
1237
1249
if (goal.ConcurrentNodes && goal.ConcurrentMissionKeyNames) {
@@ -1463,7 +1475,7 @@ export const addMissionRewards = async (
1463
1475
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
1464
1476
].some(prefix => jobType.startsWith(prefix))
1465
1477
) {
1466
const { Goals } = getWorldState(undefined);
1478
const { Goals } = getWorldState(account.BuildLabel);
1467
1479
syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId);
1468
1480
if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!;
1469
1481
}
@@ -1567,7 +1579,6 @@ export const addMissionRewards = async (
1567
1579
}
1568
1580
1569
1581
if (rewardInfo.missionType == "MT_DESCENT") {
1570
const isSteelPath = missions?.Tier;
1571
1582
inventory.DescentRewards ??= [];
1572
1583
const entry = inventory.DescentRewards.find(x => x.Category == (isSteelPath ? "DM_COH_HARD" : "DM_COH_NORMAL"));
1573
1584
if (!entry) {
@@ -1590,7 +1601,6 @@ export const addMissionRewards = async (
1590
1601
const [syndicateTag, tierStr, chemistryBuddyStr] = rewardInfo.challengeMissionId.split("_");
1591
1602
const tier = Number(tierStr);
1592
1603
const chemistryBuddy = Number(chemistryBuddyStr);
1593
const isSteelPath = missions?.Tier;
1594
1604
if (syndicateTag === "ZarimanSyndicate") {
1595
1605
let medallionAmount = tier + 1;
1596
1606
if (isSteelPath) medallionAmount = Math.round(medallionAmount * 1.5);
@@ -1841,6 +1851,7 @@ function getLevelCreditRewards(node: IRegion): number {
1841
1851
}
1842
1852
1843
1853
function getRandomMissionDrops(
1854
account: TAccountDocument,
1844
1855
inventory: TInventoryDatabaseDocument,
1845
1856
RewardInfo: IRewardInfo,
1846
1857
levelKeyName: string | undefined,
@@ -2027,7 +2038,7 @@ function getRandomMissionDrops(
2027
2038
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
2028
2039
].some(prefix => jobType.startsWith(prefix))
2029
2040
) {
2030
const { Goals } = getWorldState(undefined);
2041
const { Goals } = getWorldState(account.BuildLabel);
2031
2042
syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId);
2032
2043
if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!;
2033
2044
}
@@ -2376,7 +2387,7 @@ function getRandomMissionDrops(
2376
2387
2377
2388
if (RewardInfo.EnemyCachesFound) {
2378
2389
if (RewardInfo.goalId) {
2379
const goal = getWorldState().Goals.find(x => x._id.$oid == RewardInfo.goalId);
2390
const goal = getWorldState(account.BuildLabel).Goals.find(x => x._id.$oid == RewardInfo.goalId);
2380
2391
if (goal) {
2381
2392
let currentMissionKey: string | undefined;
2382
2393
if (RewardInfo.node == goal.Node) {
@@ -2407,7 +2418,7 @@ function getRandomMissionDrops(
2407
2418
}
2408
2419
}
2409
2420
} else if (RewardInfo.alertId) {
2410
const alert = getWorldState().Alerts.find(x => x._id.$oid == RewardInfo.alertId);
2421
const alert = getWorldState(account.BuildLabel).Alerts.find(x => x._id.$oid == RewardInfo.alertId);
2411
2422
if (alert && alert.MissionInfo.enemyCacheOverride) {
2412
2423
const deck = ExportRewards[alert.MissionInfo.enemyCacheOverride];
2413
2424
for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) {
@@ -13,8 +13,8 @@ import { buildConfig } from "./buildConfigService.ts";
13
13
import { EPOCH, unixTimesInMs } from "../constants/timeConstants.ts";
14
14
import { config } from "./configService.ts";
15
15
import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "./rngService.ts";
16
import type { IMissionReward, IRegion, TFaction } from "warframe-public-export-plus";
17
import { ExportRegions, ExportSyndicates } from "warframe-public-export-plus";
16
import type { IMissionReward, IRegion, ITilesetMission, TFaction, TMissionType } from "warframe-public-export-plus";
17
import { ExportRegions, ExportSyndicates, ExportTilesets } from "warframe-public-export-plus";
18
18
import type {
19
19
ICalendarDay,
20
20
ICalendarEvent,
@@ -36,7 +36,8 @@ import type {
36
36
IVoidTraderOffer,
37
37
IWorldState,
38
38
TCircuitGameMode,
39
IFlashSale
39
IFlashSale,
40
IAlertMissionInfo
40
41
} from "../types/worldStateTypes.ts";
41
42
import { toMongoDate, toMongoDate2, toOid, toOid2, version_compare } from "../helpers/inventoryHelpers.ts";
42
43
import { logger } from "../utils/logger.ts";
@@ -44,6 +45,7 @@ import { DailyDeal, Fissure } from "../models/worldStateModel.ts";
44
45
import { factionToInt, getConquest, getMissionTypeForLegacyOverride } from "./conquestService.ts";
45
46
import gameToBuildVersion from "../constants/gameToBuildVersion.ts";
46
47
import { getDescent } from "./descentService.ts";
48
import { catBreadHash } from "../helpers/stringHelpers.ts";
47
49
48
50
const sortieBosses = [
49
51
"SORTIE_BOSS_HYENA",
@@ -202,6 +204,70 @@ const configAlerts: Record<string, IAlert> = {
202
204
}
203
205
};
204
206
207
const alertGeneratorConfig: Record<
208
string,
209
{
210
alertInterval: number;
211
alertLength: number;
212
allowedSystemIndexes: number[];
213
baseAlert: Partial<IAlert>;
214
baseMissionInfo: Partial<IAlertMissionInfo>;
215
}
216
> = {
217
"12MinWarEvent": {
218
alertInterval: unixTimesInMs.hour / 2,
219
alertLength: unixTimesInMs.hour * 1.5,
220
allowedSystemIndexes: [14],
221
baseAlert: {
222
Tag: "12MinWarEvent",
223
Icon: "/Lotus/Interface/Icons/WorldStatePanel/BloodOfPeritaEventBadge.png"
224
},
225
baseMissionInfo: {
226
minEnemyLevel: 65,
227
maxEnemyLevel: 70,
228
maxRotations: 1,
229
missionReward: {
230
credits: 8900, // should be random
231
countedItems: [
232
{
233
ItemType: "/Lotus/Types/Gameplay/Tau/Resources/TwelveResourceCurrencyItem",
234
ItemCount: 20
235
}
236
]
237
},
238
descText: "/Lotus/Language/TauPrequel/TauPrequelFinal/TauPrequelEventAlertTitle",
239
questReq: "/Lotus/Types/Keys/TauPrequel/TauPrequelQuestKeyChain",
240
leadersAlwaysAllowed: true
241
}
242
},
243
JadeShadows: {
244
alertInterval: unixTimesInMs.hour / 2,
245
alertLength: unixTimesInMs.hour * 2,
246
allowedSystemIndexes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18],
247
baseAlert: {
248
Tag: "JadeShadows",
249
Icon: "/Lotus/Interface/Icons/WorldStatePanel/JadeShadowsEventBadge.png"
250
},
251
baseMissionInfo: {
252
minEnemyLevel: 50,
253
maxEnemyLevel: 50,
254
maxRotations: 1,
255
missionReward: {
256
credits: 8900, // should be random
257
countedItems: [
258
{
259
ItemType: "/Lotus/Types/Gameplay/JadeShadows/Resources/AscensionEventResourceItem",
260
ItemCount: 10
261
}
262
]
263
},
264
descText: "/Lotus/Language/JadeShadows/EventAlertTitle",
265
questReq: "/Lotus/Types/Keys/JadeShadows/JadeShadowQuestKeyChain",
266
leadersAlwaysAllowed: true
267
}
268
}
269
};
270
205
271
const eidolonJobs: readonly string[] = [
206
272
"/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss",
207
273
"/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap",
@@ -2999,26 +3065,29 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
2999
3065
}
3000
3066
3001
3067
if (config.worldState?.bellyOfTheBeast) {
3002
worldState.Goals.push({
3003
_id: { $oid: "67a5035c2a198564d62e165e" },
3004
Activation: { $date: { $numberLong: "1738868400000" } },
3005
Expiry: { $date: { $numberLong: "2000000000000" } },
3006
Count: config.worldState.bellyOfTheBeastProgressOverride ?? 0,
3007
HealthPct: (config.worldState.bellyOfTheBeastProgressOverride ?? 0) / 100,
3008
Goal: 0,
3009
Personal: true,
3010
Community: true,
3011
ClanGoal: [72, 216, 648, 1944, 5832],
3012
Tag: "JadeShadowsEvent",
3013
Faction: "FC_MITW",
3014
Desc: "/Lotus/Language/JadeShadows/JadeShadowsEventName",
3015
ToolTip: "/Lotus/Language/JadeShadows/JadeShadowsShortEventDesc",
3016
Icon: "/Lotus/Interface/Icons/WorldStatePanel/JadeShadowsEventBadge.png",
3017
ScoreLocTag: "/Lotus/Language/JadeShadows/JadeShadowsEventScore",
3018
Node: "SolNode723",
3019
MissionKeyName: "/Lotus/Types/Keys/JadeShadowsEventMission",
3020
ItemType: "/Lotus/Types/Gameplay/JadeShadows/Resources/AscensionEventResourceItem"
3021
});
3068
if (buildLabel && version_compare(buildLabel, "2024.06.12.18.42") >= 0) {
3069
worldState.Goals.push({
3070
_id: { $oid: "67a5035c2a198564d62e165e" },
3071
Activation: { $date: { $numberLong: "1738868400000" } },
3072
Expiry: { $date: { $numberLong: "2000000000000" } },
3073
Count: config.worldState.bellyOfTheBeastProgressOverride ?? 0,
3074
HealthPct: (config.worldState.bellyOfTheBeastProgressOverride ?? 0) / 100,
3075
Goal: 0,
3076
Personal: true,
3077
Community: true,
3078
ClanGoal: [72, 216, 648, 1944, 5832],
3079
Tag: "JadeShadowsEvent",
3080
Faction: "FC_MITW",
3081
Desc: "/Lotus/Language/JadeShadows/JadeShadowsEventName",
3082
ToolTip: "/Lotus/Language/JadeShadows/JadeShadowsShortEventDesc",
3083
Icon: "/Lotus/Interface/Icons/WorldStatePanel/JadeShadowsEventBadge.png",
3084
ScoreLocTag: "/Lotus/Language/JadeShadows/JadeShadowsEventScore",
3085
Node: "SolNode723",
3086
MissionKeyName: "/Lotus/Types/Keys/JadeShadowsEventMission",
3087
ItemType: "/Lotus/Types/Gameplay/JadeShadows/Resources/AscensionEventResourceItem"
3088
});
3089
pushGoalAlerts(worldState, "JadeShadows", buildLabel);
3090
}
3022
3091
}
3023
3092
if (config.worldState?.eightClaw) {
3024
3093
worldState.Goals.push({
@@ -3136,6 +3205,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
3136
3205
Tag: "12MinWarEvent",
3137
3206
Node: "SolNode251"
3138
3207
});
3208
pushGoalAlerts(worldState, "12MinWarEvent", buildLabel);
3139
3209
}
3140
3210
}
3141
3211
@@ -4163,3 +4233,83 @@ const pushConclaveWeakly = (activeChallenges: IPVPChallengeInstance[], week: num
4163
4233
}
4164
4234
);
4165
4235
};
4236
4237
const pushGoalAlerts = (ws: IWorldState, tag: string, buildLabel: string): void => {
4238
const genMeta = alertGeneratorConfig[tag];
4239
const neededAlerts = genMeta.alertLength / genMeta.alertInterval;
4240
const currentAlertIndex = Math.floor((ws.Time * 1000 - EPOCH) / genMeta.alertInterval);
4241
for (let i = 0; i <= neededAlerts; i++) {
4242
ws.Alerts.push(generateGoalAlert(tag, currentAlertIndex - i, ws.Alerts, buildLabel));
4243
}
4244
};
4245
4246
const generateGoalAlert = (tag: string, alertIdx: number, wsAlerts: IAlert[], buildLabel?: string): IAlert => {
4247
const genMeta = alertGeneratorConfig[tag];
4248
const defenseWavesPerRotation = buildLabel && version_compare(buildLabel, gameToBuildVersion["38.5.0"]) < 0 ? 5 : 3;
4249
const activation = EPOCH + alertIdx * genMeta.alertInterval;
4250
const expiry = activation + genMeta.alertLength;
4251
const tagHash = catBreadHash(tag);
4252
const rng = new SRng(alertIdx ^ tagHash);
4253
const allowedNodes = Object.fromEntries(
4254
Object.entries(ExportRegions).filter(
4255
([nodeTag, node]) =>
4256
genMeta.allowedSystemIndexes.includes(node.systemIndex) &&
4257
node.nodeType != 3 && // not hub
4258
node.nodeType != 7 && // not junction
4259
!["MT_ASSASSINATION", "MT_LANDSCAPE", "MT_RAILJACK", "MT_PVPVE", "MT_PVP", "MT_ARENA"].includes(
4260
node.missionType
4261
) &&
4262
node.name.indexOf("1999NodeI") == -1 && // not stage defense
4263
node.name.indexOf("1999NodeJ") == -1 && // not lich bounty
4264
node.name.indexOf("SolarMapEntratiNode") == -1 && // not albrecht lab mission
4265
!isArchwingMission(node) &&
4266
!wsAlerts.map(a => a.MissionInfo.location).includes(nodeTag)
4267
)
4268
);
4269
let nodeTag: string;
4270
let node: IRegion;
4271
do {
4272
[nodeTag, node] = rng.randomElement(Object.entries(allowedNodes))!;
4273
if (!node.tileset) logger.debug(`node ${nodeTag} without tileset - reroll it`);
4274
else if (!node.faction) logger.debug(`node ${nodeTag} without faction - reroll it`);
4275
} while (!node.tileset || !node.faction);
4276
let missionType: string;
4277
let mission: ITilesetMission;
4278
do {
4279
[missionType, mission] = rng.randomElement(Object.entries(ExportTilesets[node.tileset].missions))!;
4280
} while (
4281
["MT_ASSASSINATION", "MT_LANDSCAPE", "MT_RAILJACK", "MT_PVPVE", "MT_PVP", "MT_ARENA"].includes(missionType)
4282
);
4283
const oid =
4284
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
4285
tagHash.toString(16).padEnd(8, "0") +
4286
(alertIdx & 0xffffffff).toString(16).padStart(8, "0");
4287
const alert: IAlert = {
4288
_id: { $oid: oid },
4289
Activation: { $date: { $numberLong: activation.toString() } },
4290
Expiry: { $date: { $numberLong: expiry.toString() } },
4291
MissionInfo: {
4292
location: nodeTag,
4293
missionType: missionType as TMissionType,
4294
faction: node.faction,
4295
difficulty: 1,
4296
seed: alertIdx,
4297
levelOverride: mission.procLevel,
4298
...genMeta.baseMissionInfo
4299
},
4300
...genMeta.baseAlert
4301
};
4302
if (mission.enemySpecs) alert.MissionInfo.enemySpec = rng.randomElement(mission.enemySpecs);
4303
if (mission.extraEnemySpecs) alert.MissionInfo.extraEnemySpec = rng.randomElement(mission.extraEnemySpecs);
4304
if (mission.vipAgent) alert.MissionInfo.vipAgent = mission.vipAgent;
4305
if (mission.advancedSpawners) alert.MissionInfo.customAdvancedSpawners = mission.advancedSpawners;
4306
if (["MT_INTEL", "MT_TERRITORY", "MT_EXCAVATE"].includes(missionType)) {
4307
alert.MissionInfo.maxWaveNum = 2;
4308
} else if (missionType == "MT_SURVIVAL") {
4309
alert.MissionInfo.maxWaveNum = 5 * genMeta.baseMissionInfo.maxRotations!;
4310
} else if (missionType == "MT_DEFENSE") {
4311
alert.MissionInfo.maxWaveNum = defenseWavesPerRotation * genMeta.baseMissionInfo.maxRotations!;
4312
}
4313
delete alert.MissionInfo.maxRotations;
4314
return alert;
4315
};
@@ -78,6 +78,7 @@ export type IMissionInventoryUpdateRequest = {
78
78
AliveTime: number;
79
79
MissionTime: number;
80
80
Missions?: IMission;
81
Alerts?: IMission;
81
82
LastRegionPlayed?: TSolarMapRegion;
82
83
GameModeId: number;
83
84
hosts: string[];
@@ -66,6 +66,7 @@ export interface IAlert {
66
66
Expiry: IMongoDate;
67
67
MissionInfo: IAlertMissionInfo;
68
68
Tag?: string;
69
Icon?: string;
69
70
ForceUnlock?: true;
70
71
}
71
72
@@ -83,6 +84,10 @@ export interface IAlertMissionInfo {
83
84
maxEnemyLevel?: number;
84
85
maxWaveNum?: number;
85
86
descText?: string;
87
questReq?: string;
88
vipAgent?: string;
89
leadersAlwaysAllowed?: true;
90
seed?: number;
86
91
enemyCacheOverride?: string;
87
92
88
93
maxRotations?: number; // SNS specific field