feat(rust): Backpacks — staff and player views, the Bigger backpack step (PLAN_REDESIGNS §11.3)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 37s
PR Checks / frozen-manifest (pull_request) Successful in 1m0s
PR Checks / client-build (pull_request) Successful in 7m23s

The site half of §11.3.2, on protocol 14:

- `rust.backpack.size` ("Bigger backpack"): a kit reward with a different
  permission (D337) — one rust_perm_run_grants row per website user with
  backpacks.size.N, no kit, no credit; the perm mirror pushes it and the run's
  revert deletes it. risk change, reversible ledger, a new `rust.backpacks`
  budget, rust.kit.entitle's recipient modes including `linked`. Refused with
  "Backpacks is not installed on this server" in the dry run too (D199), and a
  size the server does not offer is refused by name. Its message is never
  silent: empty is the take-back warning (D338).
- `rust.options.backpacksizes`: each server's sizes above its smallest, from
  the last hello, grouped by server.
- GET /player/rust/backpacks (own links only, 20 reads a minute) and
  GET /admin/users/:id/rust/backpacks (that user's links only). Live, never
  stored; a stranger's id is a 404 and never reaches the game.
- Client: a shared Backpack panel (pick server and account, open, read once)
  on the account page and the admin user page; the grid draws pages of
  pageSize, each slot named by the game (D340, no icons), blueprints by what
  they make (D341), overflow marked. Backpacks joins the server's mod line.

Server 548/548 (13 new in test/backpacks.test.js), client 79/79 (5 new),
routes.manifest regenerated against the pinned core (89 routes), swagger
fragment regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-12 14:35:55 -05:00
parent 69ec0f6c46
commit e4591af906
22 changed files with 1786 additions and 7 deletions

View File

@@ -0,0 +1,62 @@
// ── A backpack as a grid (PLAN_REDESIGNS §11.3.2) ─────────────────────────
//
// The grid is drawn from these, so each test is a backpack that could be drawn
// wrong: a two-page one, one with overflow past its capacity, one that never
// existed, and the labels a slot can carry (D340, D341).
import test from 'node:test'
import assert from 'node:assert/strict'
import { backpackPages, overflowCount, stackLabel } from '../src/lib/backpack.js'
test('a backpack that does not exist is no pages', () => {
assert.deepEqual(backpackPages(null), [])
assert.deepEqual(backpackPages({ exists: false }), [])
})
test('slots run across pages of the page size', () => {
const pages = backpackPages({
exists: true,
capacity: 96,
pageSize: 48,
items: [{ slot: 0, shortname: 'wood', amount: 1000 }, { slot: 50, shortname: 'note', amount: 1 }],
})
assert.equal(pages.length, 2)
assert.equal(pages[0].cells.length, 48)
assert.equal(pages[1].cells.length, 48)
assert.equal(pages[0].cells[0].stack.shortname, 'wood')
assert.equal(pages[1].cells[2].slot, 50)
assert.equal(pages[1].cells[2].stack.shortname, 'note')
assert.ok(pages.every((p) => p.cells.every((c) => !c.overflow)))
})
test('stacks past the capacity are drawn after it, marked overflow', () => {
// A size revoked at the end of a run: capacity 6, five notes still on what was page two.
const backpack = {
exists: true,
capacity: 6,
pageSize: 48,
items: [{ slot: 0, shortname: 'wood', amount: 1 }, ...[48, 49, 50, 51, 52].map((slot) => ({ slot, shortname: 'note', amount: 1, overflow: true }))],
}
const pages = backpackPages(backpack)
assert.equal(pages.length, 2)
assert.equal(pages[0].cells.slice(0, 6).filter((c) => c.overflow).length, 0)
assert.ok(pages[0].cells.slice(6).every((c) => c.overflow))
assert.equal(pages[1].cells.length, 5)
assert.equal(overflowCount(backpack), 5)
})
test('an empty backpack still draws its capacity', () => {
const pages = backpackPages({ exists: true, capacity: 12, pageSize: 48, items: [] })
assert.equal(pages.length, 1)
assert.equal(pages[0].cells.length, 12)
assert.ok(pages[0].cells.every((c) => c.stack === null))
})
test('a slot is named by the game, a blueprint by what it makes', () => {
assert.equal(stackLabel({ shortname: 'rifle.ak', display: 'Assault Rifle' }), 'Assault Rifle')
assert.equal(stackLabel({ shortname: 'blueprintbase', display: 'Assault Rifle', blueprint: 'rifle.ak' }), 'Assault Rifle blueprint')
assert.equal(stackLabel({ shortname: 'wood' }), 'wood')
assert.equal(stackLabel({ shortname: null, itemId: 999 }), 'item 999')
assert.equal(stackLabel(null), '')
})