diff --git a/src/controllers/api/getPastWeeklyChallengesController.ts b/src/controllers/api/getPastWeeklyChallengesController.ts new file mode 100644 index 00000000..cee6fe50 --- /dev/null +++ b/src/controllers/api/getPastWeeklyChallengesController.ts @@ -0,0 +1,62 @@ +import { RequestHandler } from "express"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getInventory } from "@/src/services/inventoryService"; +import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "@/src/services/worldStateService"; +import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { ISeasonChallenge } from "@/src/types/worldStateTypes"; +import { ExportChallenges } from "warframe-public-export-plus"; + +export const getPastWeeklyChallengesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "SeasonChallengeHistory ChallengeProgress"); + const worldState = getWorldState(undefined); + + if (worldState.SeasonInfo) { + const pools = getSeasonChallengePools(worldState.SeasonInfo.AffiliationTag); + const nightwaveStartTimestamp = Number(worldState.SeasonInfo.Activation.$date.$numberLong); + const nightwaveSeason = worldState.SeasonInfo.Season; + const timeMs = worldState.Time * 1000; + const completedChallengesIds = new Set(); + + inventory.SeasonChallengeHistory.forEach(challengeHistory => { + const entryNightwaveSeason = parseInt(challengeHistory.id.slice(0, 4), 10) - 1; + if (nightwaveSeason == entryNightwaveSeason) { + const meta = Object.entries(ExportChallenges).find( + ([key]) => key.split("/").pop() === challengeHistory.challenge + ); + if (meta) { + const [, challengeMeta] = meta; + const challengeProgress = inventory.ChallengeProgress.find( + c => c.Name === challengeHistory.challenge + ); + + if (challengeProgress && challengeProgress.Progress >= (challengeMeta.requiredCount ?? 1)) { + completedChallengesIds.add(challengeHistory.id); + } + } + } + }); + + const PastWeeklyChallenges: ISeasonChallenge[] = []; + + let week = Math.trunc((timeMs - EPOCH) / unixTimesInMs.week) - 1; + + while (EPOCH + week * unixTimesInMs.week >= nightwaveStartTimestamp && PastWeeklyChallenges.length < 3) { + const tempActs: ISeasonChallenge[] = []; + pushWeeklyActs(tempActs, pools, week, nightwaveStartTimestamp, nightwaveSeason); + + for (const act of tempActs) { + if (!completedChallengesIds.has(act._id.$oid) && PastWeeklyChallenges.length < 3) { + if (act.Challenge.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")) { + act.Permanent = true; + } + PastWeeklyChallenges.push(act); + } + } + + week--; + } + + res.json({ PastWeeklyChallenges: PastWeeklyChallenges }); + } +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 031f63cb..e0d3b712 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -66,6 +66,7 @@ import { getGuildLogController } from "@/src/controllers/api/getGuildLogControll import { getIgnoredUsersController } from "@/src/controllers/api/getIgnoredUsersController"; import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSeedController"; import { getProfileViewingDataPostController } from "@/src/controllers/dynamic/getProfileViewingDataController"; +import { getPastWeeklyChallengesController } from "@/src/controllers/api/getPastWeeklyChallengesController"; import { getShipController } from "@/src/controllers/api/getShipController"; import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController"; import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController"; @@ -195,6 +196,7 @@ apiRouter.get("/getGuildLog.php", getGuildLogController); apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController); apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17 apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController); +apiRouter.get("/getPastWeeklyChallenges.php", getPastWeeklyChallengesController) apiRouter.get("/getShip.php", getShipController); apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8 apiRouter.get("/getVendorInfo.php", getVendorInfoController); diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 804be155..d3527c6e 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -180,7 +180,7 @@ const microplanetEndlessJobs: readonly string[] = [ "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" ]; -const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 +export const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => { return nowMs + 300_000 > thenMs; @@ -365,10 +365,10 @@ interface IRotatingSeasonChallengePools { daily: string[]; weekly: string[]; hardWeekly: string[]; - hasWeeklyPermanent: boolean; + weeklyPermanent: string[]; } -const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => { +export const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => { const syndicate = ExportSyndicates[syndicateTag]; return { daily: syndicate.dailyChallenges!, @@ -380,7 +380,7 @@ const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallenge hardWeekly: syndicate.weeklyChallenges!.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") ), - hasWeeklyPermanent: syndicate.weeklyChallenges!.some(x => + weeklyPermanent: syndicate.weeklyChallenges!.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") ) }; @@ -401,6 +401,7 @@ const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: numb const pushSeasonWeeklyChallenge = ( activeChallenges: ISeasonChallenge[], pool: string[], + nightwaveSeason: number, week: number, id: number ): void => { @@ -413,51 +414,59 @@ const pushSeasonWeeklyChallenge = ( challenge = rng.randomElement(pool)!; } while (activeChallenges.some(x => x.Challenge == challenge)); activeChallenges.push({ - _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + _id: { + $oid: + (nightwaveSeason + 1).toString().padStart(4, "0") + + "bb2d9d00cb47" + + challengeId.toString().padStart(8, "0") + }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, Challenge: challenge }); }; -const pushWeeklyActs = ( +export const pushWeeklyActs = ( activeChallenges: ISeasonChallenge[], pools: IRotatingSeasonChallengePools, - week: number + week: number, + nightwaveStartTimestamp: number, + nightwaveSeason: number ): void => { - const weekStart = EPOCH + week * 604800000; - const weekEnd = weekStart + 604800000; - - pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 0); - pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 1); - if (pools.hasWeeklyPermanent) { - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" - }); - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" - }); - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" - }); - pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, week, 2); - pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, week, 3); + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 0); + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 1); + if (pools.weeklyPermanent.length > 0) { + const weekStart = EPOCH + week * unixTimesInMs.week; + const weekEnd = weekStart + unixTimesInMs.week; + const nightwaveWeekStart = ((): number => { + const nightwaveStartDate = new Date(nightwaveStartTimestamp); + const dayOffset = (nightwaveStartDate.getDay() + 6) % 7; + nightwaveStartDate.setDate(nightwaveStartDate.getDate() - dayOffset); + nightwaveStartDate.setHours(0, 0, 0, 0); + return nightwaveStartDate.getTime(); + })(); + const nightwaveWeek = Math.trunc((weekStart - nightwaveWeekStart) / unixTimesInMs.week); + const weeklyPermanentIndex = (nightwaveWeek * 3) % pools.weeklyPermanent.length; + for (let i = 0; i < 3; i++) { + activeChallenges.push({ + _id: { + $oid: + (nightwaveSeason + 1).toString().padStart(4, "0") + + "b96e9d00cb47" + + (week * 7 + 2 + i).toString().padStart(8, "0") + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: pools.weeklyPermanent[weeklyPermanentIndex + i] + }); + } } else { - pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 2); - pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 3); - pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 4); - pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, week, 5); - pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, week, 6); + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 2); + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 3); + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 4); } + pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 5); + pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 6); }; const generateXpAmounts = (rng: SRng, stageCount: number, minXp: number, maxXp: number): number[] => { @@ -1552,11 +1561,13 @@ export const getWorldState = (buildLabel?: string): IWorldState => { // Nightwave Challenges const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel); if (nightwaveSyndicateTag) { + const nightwaveStartTimestamp = 1747851300000; + const nightwaveSeason = nightwaveTagToSeason[nightwaveSyndicateTag]; worldState.SeasonInfo = { - Activation: { $date: { $numberLong: "1715796000000" } }, + Activation: { $date: { $numberLong: nightwaveStartTimestamp.toString() } }, Expiry: { $date: { $numberLong: "2000000000000" } }, AffiliationTag: nightwaveSyndicateTag, - Season: nightwaveTagToSeason[nightwaveSyndicateTag], + Season: nightwaveSeason, Phase: 0, Params: "", ActiveChallenges: [] @@ -1568,9 +1579,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => { if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) { worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1)); } - pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week); + pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week, nightwaveStartTimestamp, nightwaveSeason); if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { - pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1); + pushWeeklyActs( + worldState.SeasonInfo.ActiveChallenges, + pools, + week + 1, + nightwaveStartTimestamp, + nightwaveSeason + ); } } diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index e52e613f..4ab502c7 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -265,6 +265,7 @@ export interface IEndlessXpChoice { export interface ISeasonChallenge { _id: IOid; Daily?: boolean; + Permanent?: boolean; // only for getPastWeeklyChallenges response Activation: IMongoDate; Expiry: IMongoDate; Challenge: string;