feat: initial support for multiple nightwave seasons #2096

Merged
Sainan merged 3 commits from multiwave into main 2025-05-23 06:12:54 -07:00
9 changed files with 150 additions and 99 deletions
Showing only changes of commit 7b0f864599 - Show all commits

8
package-lock.json generated
View File

@ -18,7 +18,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.62", "warframe-public-export-plus": "^0.5.64",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
@ -3703,9 +3703,9 @@
} }
}, },
"node_modules/warframe-public-export-plus": { "node_modules/warframe-public-export-plus": {
"version": "0.5.62", "version": "0.5.64",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.62.tgz", "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.64.tgz",
"integrity": "sha512-D8ZzjkU9rrK/59VqCfpMoV31HVmwHZV1dNZxPO85AOlcjg/G81Fu3kgITQTaw9sdNagLPLQnFaiXY58pxxRwgA==" "integrity": "sha512-JyHRtYumfwQ1Iog2unzlBWfQHJlZER+iUISquyFFv0Qqtv2QsNzFv2AbV7sCaqgDcE8tw6e5/YqGgfI0m403/g=="
}, },
"node_modules/warframe-riven-info": { "node_modules/warframe-riven-info": {
"version": "0.1.2", "version": "0.1.2",

View File

@ -25,7 +25,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.62", "warframe-public-export-plus": "^0.5.64",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"

View File

@ -57,7 +57,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
const firstCompletion = missionReport.SortieId const firstCompletion = missionReport.SortieId
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1 ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false; : false;
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); const inventoryUpdates = await addMissionInventoryUpdates(account, inventory, missionReport);
if ( if (
missionReport.MissionStatus !== "GS_SUCCESS" && missionReport.MissionStatus !== "GS_SUCCESS" &&

View File

@ -1,15 +1,13 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { isStoreItem, toStoreItem } from "@/src/services/itemDataService"; import { fromStoreItem } from "@/src/services/itemDataService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName;
export const syndicateSacrificeController: RequestHandler = async (request, response) => { export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
@ -54,13 +52,6 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
syndicate.Title ??= 0; syndicate.Title ??= 0;
syndicate.Title += 1; syndicate.Title += 1;
if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
syndicate.FreeFavorsEarned ??= [];
if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
syndicate.FreeFavorsEarned.push(syndicate.Title);
}
}
if (reward) { if (reward) {
combineInventoryChanges( combineInventoryChanges(
res.InventoryChanges, res.InventoryChanges,
@ -68,24 +59,37 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
); );
} }
if (data.AffiliationTag == ExportNightwave.affiliationTag) { // Quacks like a nightwave syndicate?
const index = syndicate.Title - 1; if (manifest.dailyChallenges) {
if (index < ExportNightwave.rewards.length) { const title = manifest.titles!.find(x => x.level == syndicate.Title);
if (title) {
res.NewEpisodeReward = true; res.NewEpisodeReward = true;
const reward = ExportNightwave.rewards[index]; let rewardType: string;
let rewardType = reward.uniqueName; let rewardCount: number;
if (!isStoreItem(rewardType)) { if (title.storeItemReward) {
rewardType = toStoreItem(rewardType); rewardType = title.storeItemReward;
rewardCount = 1;
} else {
rewardType = fromStoreItem(title.reward!.ItemType);
rewardCount = title.reward!.ItemCount;
} }
const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)) const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount))
.InventoryChanges; .InventoryChanges;
if (Object.keys(rewardInventoryChanges).length == 0) { if (Object.keys(rewardInventoryChanges).length == 0) {
logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType;
rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }]; rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }];
addMiscItems(inventory, rewardInventoryChanges.MiscItems); addMiscItems(inventory, rewardInventoryChanges.MiscItems);
} }
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
} }
} else {
if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
syndicate.FreeFavorsEarned ??= [];
if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
syndicate.FreeFavorsEarned.push(syndicate.Title);
}
}
} }
await inventory.save(); await inventory.save();

View File

@ -1,18 +1,26 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { addChallenges, getInventory } from "@/src/services/inventoryService"; import { addChallenges, getInventory } from "@/src/services/inventoryService";
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
import { IAffiliationMods } from "@/src/types/purchaseTypes"; import { IAffiliationMods } from "@/src/types/purchaseTypes";
export const updateChallengeProgressController: RequestHandler = async (req, res) => { export const updateChallengeProgressController: RequestHandler = async (req, res) => {
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body)); const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
const accountId = await getAccountIdForRequest(req); const account = await getAccountForRequest(req);
const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations"); const inventory = await getInventory(
account._id.toString(),
"ChallengeProgress SeasonChallengeHistory Affiliations"
);
let affiliationMods: IAffiliationMods[] = []; let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) { if (challenges.ChallengeProgress) {
affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions); affiliationMods = addChallenges(
account,
inventory,
challenges.ChallengeProgress,
challenges.SeasonChallengeCompletions
);
} }
if (challenges.SeasonChallengeHistory) { if (challenges.SeasonChallengeHistory) {
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => { challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {

View File

@ -44,6 +44,7 @@ import {
import { import {
ExportArcanes, ExportArcanes,
ExportBundles, ExportBundles,
ExportChallenges,
ExportCustoms, ExportCustoms,
ExportDrones, ExportDrones,
ExportEmailItems, ExportEmailItems,
@ -53,7 +54,6 @@ import {
ExportGear, ExportGear,
ExportKeys, ExportKeys,
ExportMisc, ExportMisc,
ExportNightwave,
ExportRailjackWeapons, ExportRailjackWeapons,
ExportRecipes, ExportRecipes,
ExportResources, ExportResources,
@ -83,8 +83,9 @@ import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService"; import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService";
import { createMessage } from "./inboxService"; import { createMessage } from "./inboxService";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { getWorldState } from "./worldStateService"; import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService";
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers"; import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
import { TAccountDocument } from "./loginService";
export const createInventory = async ( export const createInventory = async (
accountOwnerId: Types.ObjectId, accountOwnerId: Types.ObjectId,
@ -1716,6 +1717,7 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr:
}; };
export const addChallenges = ( export const addChallenges = (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
ChallengeProgress: IChallengeProgress[], ChallengeProgress: IChallengeProgress[],
SeasonChallengeCompletions: ISeasonChallenge[] | undefined SeasonChallengeCompletions: ISeasonChallenge[] | undefined
@ -1738,26 +1740,32 @@ export const addChallenges = (
continue; continue;
} }
const meta = ExportNightwave.challenges[challenge.challenge]; const meta = ExportChallenges[challenge.challenge];
logger.debug("Completed challenge", meta); const nightwaveSyndicateTag = getNightwaveSyndicateTag(account.BuildLabel);
logger.debug("Completed season challenge", {
let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag); uniqueName: challenge.challenge,
syndicateTag: nightwaveSyndicateTag,
...meta
});
if (nightwaveSyndicateTag) {
let affiliation = inventory.Affiliations.find(x => x.Tag == nightwaveSyndicateTag);
if (!affiliation) { if (!affiliation) {
affiliation = affiliation =
inventory.Affiliations[ inventory.Affiliations[
inventory.Affiliations.push({ inventory.Affiliations.push({
Tag: ExportNightwave.affiliationTag, Tag: nightwaveSyndicateTag,
Standing: 0 Standing: 0
}) - 1 }) - 1
]; ];
} }
affiliation.Standing += meta.standing; affiliation.Standing += meta.standing!;
if (affiliationMods.length == 0) { if (affiliationMods.length == 0) {
affiliationMods.push({ Tag: ExportNightwave.affiliationTag }); affiliationMods.push({ Tag: nightwaveSyndicateTag });
} }
affiliationMods[0].Standing ??= 0; affiliationMods[0].Standing ??= 0;
affiliationMods[0].Standing += meta.standing; affiliationMods[0].Standing += meta.standing!;
}
} }
} }
return affiliationMods; return affiliationMods;

View File

@ -79,6 +79,7 @@ import { config } from "./configService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { ISyndicateMissionInfo } from "../types/worldStateTypes"; import { ISyndicateMissionInfo } from "../types/worldStateTypes";
import { fromOid } from "../helpers/inventoryHelpers"; import { fromOid } from "../helpers/inventoryHelpers";
import { TAccountDocument } from "./loginService";
const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => {
// For Spy missions, e.g. 3 vaults cracked = A, B, C // For Spy missions, e.g. 3 vaults cracked = A, B, C
@ -135,6 +136,7 @@ const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | unde
//const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys //const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys
export const addMissionInventoryUpdates = async ( export const addMissionInventoryUpdates = async (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
inventoryUpdates: IMissionInventoryUpdateRequest inventoryUpdates: IMissionInventoryUpdateRequest
): Promise<IInventoryChanges> => { ): Promise<IInventoryChanges> => {
@ -287,7 +289,7 @@ export const addMissionInventoryUpdates = async (
addRecipes(inventory, value); addRecipes(inventory, value);
break; break;
case "ChallengeProgress": case "ChallengeProgress":
addChallenges(inventory, value, inventoryUpdates.SeasonChallengeCompletions); addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions);
break; break;
case "FusionTreasures": case "FusionTreasures":
addFusionTreasures(inventory, value); addFusionTreasures(inventory, value);

View File

@ -6,7 +6,7 @@ import { buildConfig } from "@/src/services/buildConfigService";
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus"; import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus";
import { import {
ICalendarDay, ICalendarDay,
ICalendarEvent, ICalendarEvent,
@ -344,11 +344,26 @@ export const getSortie = (day: number): ISortie => {
}; };
}; };
const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => interface IRotatingSeasonChallengePools {
x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/") daily: string[];
); weekly: string[];
hardWeekly: string[];
}
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => {
const syndicate = ExportSyndicates[syndicateTag];
return {
daily: syndicate.dailyChallenges!,
weekly: syndicate.weeklyChallenges!.filter(
x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
!x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
),
hardWeekly: syndicate.weeklyChallenges!.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/"))
};
};
const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => {
const dayStart = EPOCH + day * 86400000; const dayStart = EPOCH + day * 86400000;
const dayEnd = EPOCH + (day + 3) * 86400000; const dayEnd = EPOCH + (day + 3) * 86400000;
const rng = new SRng(new SRng(day).randomInt(0, 100_000)); const rng = new SRng(new SRng(day).randomInt(0, 100_000));
@ -357,17 +372,11 @@ const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
Daily: true, Daily: true,
Activation: { $date: { $numberLong: dayStart.toString() } }, Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } },
Challenge: rng.randomElement(dailyChallenges)! Challenge: rng.randomElement(pools.daily)!
}; };
}; };
const weeklyChallenges = Object.keys(ExportNightwave.challenges).filter( const getSeasonWeeklyChallenge = (pools: IRotatingSeasonChallengePools, week: number, id: number): ISeasonChallenge => {
x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
!x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
);
const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => {
const weekStart = EPOCH + week * 604800000; const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000; const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id; const challengeId = week * 7 + id;
@ -376,15 +385,15 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge =>
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } }, Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: rng.randomElement(weeklyChallenges)! Challenge: rng.randomElement(pools.weekly)!
}; };
}; };
const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x => const getSeasonWeeklyHardChallenge = (
x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") pools: IRotatingSeasonChallengePools,
); week: number,
id: number
const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => { ): ISeasonChallenge => {
const weekStart = EPOCH + week * 604800000; const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000; const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id; const challengeId = week * 7 + id;
@ -393,35 +402,39 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } }, Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: rng.randomElement(weeklyHardChallenges)! Challenge: rng.randomElement(pools.hardWeekly)!
}; };
}; };
const pushWeeklyActs = (worldState: IWorldState, week: number): void => { const pushWeeklyActs = (
activeChallenges: ISeasonChallenge[],
pools: IRotatingSeasonChallengePools,
week: number
): void => {
const weekStart = EPOCH + week * 604800000; const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000; const weekEnd = weekStart + 604800000;
worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0)); activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 0));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1)); activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 1));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2)); activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3)); activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3));
worldState.SeasonInfo.ActiveChallenges.push({ activeChallenges.push({
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } }, Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions"
}); });
worldState.SeasonInfo.ActiveChallenges.push({ activeChallenges.push({
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } }, Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus"
}); });
worldState.SeasonInfo.ActiveChallenges.push({ activeChallenges.push({
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } }, Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies"
}); });
}; };
@ -926,15 +939,6 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
LiteSorties: [], LiteSorties: [],
GlobalUpgrades: [], GlobalUpgrades: [],
EndlessXpChoices: [], EndlessXpChoices: [],
SeasonInfo: {
Activation: { $date: { $numberLong: "1715796000000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
AffiliationTag: "RadioLegionIntermission12Syndicate",
Season: 14,
Phase: 0,
Params: "",
ActiveChallenges: []
},
KnownCalendarSeasons: [], KnownCalendarSeasons: [],
...staticWorldState, ...staticWorldState,
SyndicateMissions: [...staticWorldState.SyndicateMissions] SyndicateMissions: [...staticWorldState.SyndicateMissions]
@ -967,17 +971,27 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
} }
// Nightwave Challenges // Nightwave Challenges
// Current nightwave season was introduced in 38.0.8 so omitting challenges before that to avoid UI bugs and even crashes on really old versions. const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel);
if (!buildLabel || version_compare(buildLabel, "2025.02.05.11.19") >= 0) { if (nightwaveSyndicateTag) {
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2)); worldState.SeasonInfo = {
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1)); Activation: { $date: { $numberLong: "1715796000000" } },
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0)); Expiry: { $date: { $numberLong: "2000000000000" } },
AffiliationTag: nightwaveSyndicateTag,
Season: nightwaveTagToSeason[nightwaveSyndicateTag],
Phase: 0,
Params: "",
ActiveChallenges: []
};
const pools = getSeasonChallengePools(nightwaveSyndicateTag);
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1));
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0));
if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) {
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1)); worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1));
} }
pushWeeklyActs(worldState, week); pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week);
if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
pushWeeklyActs(worldState, week + 1); pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1);
} }
} }
@ -1242,3 +1256,18 @@ export const isArchwingMission = (node: IRegion): boolean => {
} }
return false; return false;
}; };
export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => {
if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) {
return "RadioLegionIntermission13Syndicate";
}
if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) {
return "RadioLegionIntermission12Syndicate";
}
return undefined;
};
const nightwaveTagToSeason: Record<string, number> = {
RadioLegionIntermission13Syndicate: 15,
RadioLegionIntermission12Syndicate: 14
};

View File

@ -14,7 +14,7 @@ export interface IWorldState {
NodeOverrides: INodeOverride[]; NodeOverrides: INodeOverride[];
PVPChallengeInstances: IPVPChallengeInstance[]; PVPChallengeInstances: IPVPChallengeInstance[];
EndlessXpChoices: IEndlessXpChoice[]; EndlessXpChoices: IEndlessXpChoice[];
SeasonInfo: { SeasonInfo?: {
Activation: IMongoDate; Activation: IMongoDate;
Expiry: IMongoDate; Expiry: IMongoDate;
AffiliationTag: string; AffiliationTag: string;