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

代码差异

4 个文件 +199 -32
Modified src/services/missionInventoryUpdateService.ts +20 -9
@@ -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) {
Modified src/services/worldStateService.ts +173 -23
@@ -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 };
Modified src/types/requestTypes.ts +1 -0
@@ -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[];
Modified src/types/worldStateTypes.ts +5 -0
@@ -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