[api][econ] add levels, xp storage and basic game rewards

This commit is contained in:
Devin Zuczek
2026-08-10 23:31:40 -04:00
parent 4109317f0e
commit af64327fea
8 changed files with 328 additions and 39 deletions
+12 -6
View File
@@ -1,6 +1,8 @@
import { Hono } from 'hono'
import { describeRoute } from 'hono-openapi'
import { getProgression, getProgressions } from '@repo/domain'
import { parseFormIds, queryIds } from '../http'
import {
BulkIdsRequest,
@@ -69,13 +71,16 @@ export const progressionRoutes = new Hono<App>({ strict: false })
describeRoute({
tags: ['Progression'],
summary: 'A players level and XP',
description: 'Nothing awards XP yet, so everyone is level 1 with 0 XP.',
description:
'The XP banked in `progression` (game rewards pay into it from the `econ` worker). ' +
'A player who has earned none has no row and reads back as level 1 with 0 XP. ' +
'Levelling is not wired up yet, so `Level` is always 1.',
parameters: [idParam('id', 'Account id')],
responses: { 200: json(ProgressionDto, 'The players progression') },
}),
(c) => {
async (c) => {
const id = Number.parseInt(c.req.param('id'), 10)
return c.json({ PlayerId: id, Level: 1, XP: 0 })
return c.json(await getProgression(c.env.DB, id))
}
)
.post(
@@ -160,12 +165,13 @@ export const progressionRoutes = new Hono<App>({ strict: false })
tags: ['Progression'],
summary: 'Progressions in bulk (GET form)',
description:
'What the 2023 client sends. Unlike the POST forms this one does answer — a ' +
'default level-1 progression per requested id, in request order.',
'What the 2023 client sends. Unlike the POST forms this one does answer — one ' +
'progression per requested id, in request order, defaulting to level 1 / 0 XP for ' +
'ids that have earned nothing.',
parameters: BULK_ID_QUERY,
responses: { 200: json(ProgressionDto.array(), 'One progression per requested id') },
}),
(c) => c.json(queryIds(c).map((id) => ({ PlayerId: id, Level: 1, XP: 0 })))
async (c) => c.json(await getProgressions(c.env.DB, queryIds(c)))
)
.post(
'/api/v1/progression/bulk',
+22
View File
@@ -3,9 +3,11 @@ import { exports } from 'cloudflare:workers'
import { beforeAll, describe, expect, test } from 'vitest'
import {
addXp,
GAME_VERSION,
grantInvention,
INVENTORY_INVENTION_SCHEMA_DDL,
PROGRESSION_SCHEMA_DDL,
ROOM_SCHEMA_DDL,
seedRoomWithSubRooms,
SUBROOM_SCHEMA_DDL,
@@ -104,6 +106,7 @@ beforeAll(async () => {
// Bought-invention ownership (owned by the econ worker) — `v2/mine` folds it in.
for (const stmt of INVENTORY_INVENTION_SCHEMA_DDL) await env.DB.prepare(stmt).run()
for (const stmt of PROGRESSION_SCHEMA_DDL) await env.DB.prepare(stmt).run()
// Reports table (owned by the api worker) — player reports are recorded here.
for (const stmt of REPORTS_SCHEMA_DDL) await env.DB.prepare(stmt).run()
@@ -303,6 +306,25 @@ describe('public endpoints', () => {
expect(body[0]).toMatchObject({ Level: 1, XP: 0 })
})
test('progression reads back the XP game rewards banked', async () => {
// What `econ` writes when a game reward is claimed — the two workers share the table.
await addXp(env.DB, 4242, 25)
await addXp(env.DB, 4242, 25)
const single = await exports.default.fetch(`${ORIGIN}/api/players/v1/progression/4242`)
expect(await single.json()).toEqual({ PlayerId: 4242, Level: 1, XP: 50 })
// A player who has earned nothing has no row, and still gets a record — the bulk form
// renders a card per id, so a missing one must not shorten the list.
const bulk = await exports.default.fetch(
`${ORIGIN}/api/players/v2/progression/bulk?id=4242&id=4243`
)
expect(await bulk.json()).toEqual([
{ PlayerId: 4242, Level: 1, XP: 50 },
{ PlayerId: 4243, Level: 1, XP: 0 },
])
})
test('POST /api/players/v2/progression/bulk returns an array', async () => {
const res = await exports.default.fetch(`${ORIGIN}/api/players/v2/progression/bulk`, {
method: 'POST',