Skip to content

Commit 427e7a4

Browse files
feat(pantheon): Pantheon 2.0 modes and manifest-driven version lookup (#132)
* feat(pantheon): manifest-driven Pantheon 2.0 modes and gauntlet Add pantheon manifest helpers, gauntletVersionIds, path-based leaderboard lookup, and sunset-aware activity examples without hardcoding activity 101. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove Pantheon Gauntlet placeholder version 134 from examples and tests. Gauntlet manifest infrastructure remains; only the hardcoded id 134 definition was removed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(pantheon): support thepantheon path and fix CI Query both pantheon activity paths for manifest discovery and leaderboard version lookup. Remove calus integration test until Services data is seeded. Co-authored-by: Cursor <cursoragent@cursor.com> * Support dual pantheon activity paths while keeping gauntlet manifest field. Recognize both pantheon and thepantheon activity paths for manifest IDs and version lookup; gauntletVersionIds remains manifest-driven with no placeholder version 134. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove gauntlet placeholder from pantheon manifest. Gauntlet mode details are unknown, so drop gauntletVersionIds and related helpers until Bungie data exists. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(pantheon): restore gauntlet manifest field and fix CI Reinstate getGauntletVersionIds in manifest response after accidental removal. Drop calus integration test until Services data is seeded. Regenerate OpenAPI. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 45741d2 commit 427e7a4

8 files changed

Lines changed: 310 additions & 30 deletions

File tree

open-api/openapi.json

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,19 +1356,47 @@
13561356
],
13571357
"additionalProperties": false,
13581358
"description": "The definition of an activity in the RaidHub database.",
1359-
"example": {
1360-
"id": 9,
1361-
"name": "Vault of Glass",
1362-
"path": "vaultofglass",
1363-
"isSunset": false,
1364-
"isRaid": true,
1365-
"releaseDate": "2021-05-22T00:00:00.000Z",
1366-
"dayOneEnd": "2021-05-23T00:00:00.000Z",
1367-
"contestEnd": "2021-05-23T00:00:00.000Z",
1368-
"weekOneEnd": "2021-05-25T00:00:00.000Z",
1369-
"milestoneHash": 1888320892,
1370-
"splashSlug": "vog"
1371-
}
1359+
"examples": [
1360+
{
1361+
"id": 9,
1362+
"name": "Vault of Glass",
1363+
"path": "vaultofglass",
1364+
"isSunset": false,
1365+
"isRaid": true,
1366+
"releaseDate": "2021-05-22T00:00:00.000Z",
1367+
"dayOneEnd": "2021-05-23T00:00:00.000Z",
1368+
"contestEnd": "2021-05-23T00:00:00.000Z",
1369+
"weekOneEnd": "2021-05-25T00:00:00.000Z",
1370+
"milestoneHash": 1888320892,
1371+
"splashSlug": "vog"
1372+
},
1373+
{
1374+
"id": 101,
1375+
"name": "The Pantheon",
1376+
"path": "thepantheon",
1377+
"isSunset": true,
1378+
"isRaid": false,
1379+
"releaseDate": "2024-04-30T17:00:00.000Z",
1380+
"dayOneEnd": null,
1381+
"contestEnd": null,
1382+
"weekOneEnd": null,
1383+
"milestoneHash": null,
1384+
"splashSlug": "pantheon"
1385+
},
1386+
{
1387+
"id": 102,
1388+
"name": "Pantheon",
1389+
"path": "pantheon",
1390+
"isSunset": false,
1391+
"isRaid": false,
1392+
"releaseDate": "2026-06-09T17:00:00.000Z",
1393+
"dayOneEnd": null,
1394+
"contestEnd": null,
1395+
"weekOneEnd": null,
1396+
"milestoneHash": null,
1397+
"splashSlug": "pantheon"
1398+
}
1399+
]
13721400
},
13731401
"FeatDefinition": {
13741402
"type": "object",
@@ -1502,6 +1530,20 @@
15021530
"path": "oryx",
15031531
"associatedActivityId": 101,
15041532
"isChallengeMode": false
1533+
},
1534+
{
1535+
"id": 132,
1536+
"name": "Calus Resplendent",
1537+
"path": "calus",
1538+
"associatedActivityId": 102,
1539+
"isChallengeMode": false
1540+
},
1541+
{
1542+
"id": 133,
1543+
"name": "Morgeth Surpassing",
1544+
"path": "morgeth",
1545+
"associatedActivityId": 102,
1546+
"isChallengeMode": false
15051547
}
15061548
]
15071549
},
@@ -2199,6 +2241,15 @@
21992241
"minItems": 1,
22002242
"description": "The list of activityId for Pantheon"
22012243
},
2244+
"gauntletVersionIds": {
2245+
"type": "array",
2246+
"items": {
2247+
"type": "integer",
2248+
"minimum": 0,
2249+
"exclusiveMinimum": true
2250+
},
2251+
"description": "The list of versionId for Pantheon gauntlet modes (full boss lineup)"
2252+
},
22022253
"versionsForActivity": {
22032254
"type": "object",
22042255
"additionalProperties": {
@@ -2262,6 +2313,7 @@
22622313
"resprisedRaidIds",
22632314
"resprisedChallengeVersionIds",
22642315
"pantheonIds",
2316+
"gauntletVersionIds",
22652317
"versionsForActivity",
22662318
"rankingTiers",
22672319
"feats",

src/routes/leaderboard/individual/pantheon.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
individualPantheonLeaderboardSortColumns,
1010
searchIndividualPantheonLeaderboard
1111
} from "@/services/leaderboard/individual/pantheon"
12-
import { getVersionId } from "@/services/manifest/definitions"
12+
import { getPantheonVersionId } from "@/services/manifest/pantheon"
1313
import { z } from "zod"
1414

1515
const zCategory = z.enum(["clears", "freshClears", "score"])
@@ -59,7 +59,7 @@ export const leaderboardIndividualPantheonRoute = new RaidHubRoute({
5959

6060
const { page, count, search } = req.query
6161

62-
const versionDefinition = await getVersionId(version, 101)
62+
const versionDefinition = await getPantheonVersionId(version)
6363

6464
if (!versionDefinition) {
6565
return RaidHubRoute.fail(ErrorCode.PantheonVersionNotFoundError, {

src/routes/manifest.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import {
1111
listHashes,
1212
listVersionDefinitions
1313
} from "@/services/manifest/definitions"
14+
import {
15+
getGauntletVersionIds,
16+
getPantheonActivityIds,
17+
sortPantheonActivityIds
18+
} from "@/services/manifest/pantheon"
1419
import { TierBreaks } from "@/services/manifest/tiers"
1520
import { generateSplashUrls } from "@/services/manifest/urls"
1621
import { z } from "zod"
@@ -87,6 +92,10 @@ export const manifestRoute = new RaidHubRoute({
8792
description: "The list of activityId for Pantheon"
8893
})
8994
.min(1),
95+
gauntletVersionIds: z.array(zNaturalNumber()).openapi({
96+
description:
97+
"The list of versionId for Pantheon gauntlet modes (full boss lineup)"
98+
}),
9099
versionsForActivity: z.record(z.array(zNaturalNumber()).min(1)).openapi({
91100
description: "The set of versionId for each activityId"
92101
}),
@@ -120,7 +129,8 @@ export const manifestRoute = new RaidHubRoute({
120129
activitiesPromise.then(generateSplashUrls)
121130
])
122131
const raids = activities.filter(a => a.isRaid)
123-
const pantheonId = 101
132+
const pantheonIds = sortPantheonActivityIds(activities, getPantheonActivityIds(activities))
133+
const gauntletVersionIds = getGauntletVersionIds(versions, pantheonIds)
124134
const versionsSetForActivity: Record<number, Set<number>> = {}
125135
for (const { activityId, versionId } of hashes) {
126136
if (!versionsSetForActivity[activityId]) {
@@ -166,7 +176,8 @@ export const manifestRoute = new RaidHubRoute({
166176
.filter(v => v.associatedActivityId && v.associatedActivityId < 100)
167177
.map(a => a.associatedActivityId!),
168178
resprisedChallengeVersionIds: versions.filter(v => v.isChallengeMode).map(v => v.id),
169-
pantheonIds: [pantheonId],
179+
pantheonIds,
180+
gauntletVersionIds,
170181
versionsForActivity: versionsForActivity,
171182
rankingTiers: TierBreaks,
172183
feats: feats,

src/schema/components/ActivityDefinition.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,46 @@ export const zActivityDefinition = registry.register(
2222
.strict()
2323
.openapi({
2424
description: "The definition of an activity in the RaidHub database.",
25-
example: {
26-
id: 9,
27-
name: "Vault of Glass",
28-
path: "vaultofglass",
29-
isSunset: false,
30-
isRaid: true,
31-
releaseDate: new Date("2021-05-22T00:00:00Z"),
32-
dayOneEnd: new Date("2021-05-23T00:00:00Z"),
33-
contestEnd: new Date("2021-05-23T00:00:00Z"),
34-
weekOneEnd: new Date("2021-05-25T00:00:00Z"),
35-
milestoneHash: 1888320892,
36-
splashSlug: "vog"
37-
}
25+
examples: [
26+
{
27+
id: 9,
28+
name: "Vault of Glass",
29+
path: "vaultofglass",
30+
isSunset: false,
31+
isRaid: true,
32+
releaseDate: new Date("2021-05-22T00:00:00Z"),
33+
dayOneEnd: new Date("2021-05-23T00:00:00Z"),
34+
contestEnd: new Date("2021-05-23T00:00:00Z"),
35+
weekOneEnd: new Date("2021-05-25T00:00:00Z"),
36+
milestoneHash: 1888320892,
37+
splashSlug: "vog"
38+
},
39+
{
40+
id: 101,
41+
name: "The Pantheon",
42+
path: "thepantheon",
43+
isSunset: true,
44+
isRaid: false,
45+
releaseDate: new Date("2024-04-30T17:00:00Z"),
46+
dayOneEnd: null,
47+
contestEnd: null,
48+
weekOneEnd: null,
49+
milestoneHash: null,
50+
splashSlug: "pantheon"
51+
},
52+
{
53+
id: 102,
54+
name: "Pantheon",
55+
path: "pantheon",
56+
isSunset: false,
57+
isRaid: false,
58+
releaseDate: new Date("2026-06-09T17:00:00Z"),
59+
dayOneEnd: null,
60+
contestEnd: null,
61+
weekOneEnd: null,
62+
milestoneHash: null,
63+
splashSlug: "pantheon"
64+
}
65+
]
3866
})
3967
)

src/schema/components/VersionDefinition.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ export const zVersionDefinition = registry.register(
3030
path: "oryx",
3131
associatedActivityId: 101,
3232
isChallengeMode: false
33+
},
34+
{
35+
id: 132,
36+
name: "Calus Resplendent",
37+
path: "calus",
38+
associatedActivityId: 102,
39+
isChallengeMode: false
40+
},
41+
{
42+
id: 133,
43+
name: "Morgeth Surpassing",
44+
path: "morgeth",
45+
associatedActivityId: 102,
46+
isChallengeMode: false
3347
}
3448
]
3549
})

src/services/manifest/definitions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import { ActivityDefinition } from "@/schema/components/ActivityDefinition"
44
import { FeatDefinition } from "@/schema/components/FeatDefinition"
55
import { VersionDefinition } from "@/schema/components/VersionDefinition"
66

7+
export const getActivityIdByPath = async (activityPath: string) => {
8+
return await pgReader.queryRow<{ id: number }>(
9+
`SELECT id::int FROM activity_definition WHERE path = $1`,
10+
{ params: [activityPath] }
11+
)
12+
}
13+
714
export const getRaidId = async (raidPath: string) => {
815
return await pgReader.queryRow<{ id: number }>(
916
`SELECT id::int FROM activity_definition WHERE path = $1 AND is_raid`,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
GAUNTLET_VERSION_PATH,
3+
getGauntletVersionIds,
4+
getPantheonActivityIds,
5+
sortPantheonActivityIds
6+
} from "@/services/manifest/pantheon"
7+
import { describe, expect, test } from "bun:test"
8+
9+
const pantheonActivity = {
10+
isRaid: false,
11+
releaseDate: null,
12+
dayOneEnd: null,
13+
contestEnd: null,
14+
weekOneEnd: null,
15+
milestoneHash: null,
16+
splashSlug: "pantheon"
17+
} as const
18+
19+
describe("getPantheonActivityIds", () => {
20+
test("returns activities with pantheon paths", () => {
21+
expect(
22+
getPantheonActivityIds([
23+
{
24+
id: 9,
25+
name: "Vault of Glass",
26+
path: "vaultofglass",
27+
isSunset: false,
28+
isRaid: true,
29+
releaseDate: null,
30+
dayOneEnd: null,
31+
contestEnd: null,
32+
weekOneEnd: null,
33+
milestoneHash: null,
34+
splashSlug: "vog"
35+
},
36+
{
37+
id: 101,
38+
name: "The Pantheon",
39+
path: "thepantheon",
40+
isSunset: true,
41+
...pantheonActivity
42+
},
43+
{
44+
id: 102,
45+
name: "Pantheon",
46+
path: "pantheon",
47+
isSunset: false,
48+
...pantheonActivity
49+
}
50+
])
51+
).toEqual([101, 102])
52+
})
53+
})
54+
55+
describe("sortPantheonActivityIds", () => {
56+
test("lists active pantheon activities before sunset activities", () => {
57+
expect(
58+
sortPantheonActivityIds(
59+
[
60+
{
61+
id: 101,
62+
name: "The Pantheon",
63+
path: "thepantheon",
64+
isSunset: true,
65+
...pantheonActivity
66+
},
67+
{
68+
id: 102,
69+
name: "Pantheon",
70+
path: "pantheon",
71+
isSunset: false,
72+
...pantheonActivity
73+
}
74+
],
75+
[101, 102]
76+
)
77+
).toEqual([102, 101])
78+
})
79+
})
80+
81+
describe("getGauntletVersionIds", () => {
82+
test("returns gauntlet versions associated with pantheon activities", () => {
83+
expect(
84+
getGauntletVersionIds(
85+
[
86+
{
87+
id: 129,
88+
name: "Oryx Exalted",
89+
path: "oryx",
90+
associatedActivityId: 101,
91+
isChallengeMode: false
92+
},
93+
{
94+
id: 200,
95+
name: "Gauntlet Mode",
96+
path: GAUNTLET_VERSION_PATH,
97+
associatedActivityId: 102,
98+
isChallengeMode: false
99+
}
100+
],
101+
[101, 102]
102+
)
103+
).toEqual([200])
104+
})
105+
})

0 commit comments

Comments
 (0)