248 lines
9.6 KiB
Markdown
248 lines
9.6 KiB
Markdown
# Campaign Manager — CLAUDE.md
|
|
|
|
This project is in the **architecture/design phase**. No implementation code exists yet. The source of truth is `campaign_manager_design_v7.md`. `Symbiotes Documentation.md` is the TaleSpire platform API reference.
|
|
|
|
---
|
|
|
|
## What We're Building
|
|
|
|
A TaleSpire **Campaign Manager Symbiote** — a mod embedded inside TaleSpire (Chromium-based) that provides authenticated multi-group campaign management with ruleset-agnostic character sheets and native dice integration.
|
|
|
|
Three clients, one backend:
|
|
1. **TaleSpire Symbiote** — primary interface, runs inside the game
|
|
2. **Companion Web App** — for out-of-game character/group management
|
|
3. **Backend API** — shared by both clients
|
|
|
|
---
|
|
|
|
## Technology Stack
|
|
|
|
| Layer | Technology | Notes |
|
|
|---|---|---|
|
|
| Symbiote UI | Svelte + Vite (no Kit) | Plain Svelte for minimal bundle size; TaleSpire runs locally and the Symbiote shares resources with the game. Programmatic view-switching via a `$currentView` store — no router. |
|
|
| Web App | SvelteKit | Full SvelteKit with `adapter-static` for CDN deploy; SvelteKit's router and SSR are appropriate here since it runs in the user's own browser. Shares ruleset plugin code with the Symbiote. |
|
|
| Backend | Rust + Axum + Tokio | Async, memory-safe, high-throughput |
|
|
| DB queries | sqlx | Compile-time checked SQL, no ORM |
|
|
| Database | PostgreSQL 16 | JSONB for sheet_data; strong relational integrity |
|
|
| Auth | JWT + argon2id | argon2id preferred over bcrypt for new code |
|
|
| Object Storage | S3-compatible (Cloudflare R2) | Portrait images and .cmchar exports; keeps k8s pods stateless |
|
|
| Hosting | Kubernetes (no PVC) | Stateless pods; all persistence in Postgres + S3 |
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
### Three-Layer System
|
|
|
|
```
|
|
TaleSpire Symbiote (Chromium) ←→ Backend (Rust/Axum) ←→ PostgreSQL
|
|
Companion Web App ←→ (same API)
|
|
```
|
|
|
|
- Symbiote communicates over HTTPS; upgrades to WebSocket for live session features
|
|
- Web app and Symbiote share the same `/auth/*` endpoints and user accounts — no separate auth
|
|
|
|
### Symbiote Constraints (from TaleSpire API)
|
|
|
|
- Single-page application — no real page navigation; views are swapped programmatically to preserve the injected TS API
|
|
- **No SvelteKit for the Symbiote** — the Symbiote runs inside TaleSpire's local Chromium process and shares system resources with the game. SvelteKit's router/hydration bundle is unnecessary overhead. Use plain Svelte + Vite: one `main.ts` entry point, a `$currentView` store drives which component is rendered.
|
|
- All persistence via `TS.storage` (not `localStorage`, which is unreliable across restarts)
|
|
- Must suppress API calls during board transitions (client leave/join events)
|
|
- `manifest.json` extras: `colorStyles`, `fonts`, `diceFinder`
|
|
|
|
### Views (Symbiote)
|
|
|
|
`Login/Register` → `Group List` → `Group Detail` → `Character Sheet` ↔ `Roll History panel`
|
|
|
|
### Web App Routes
|
|
|
|
| Route | Access |
|
|
|---|---|
|
|
| `/groups` | All members |
|
|
| `/groups/:id` | Members |
|
|
| `/groups/:id/settings` | DM only |
|
|
| `/characters` | Owner |
|
|
| `/characters/:id` | Owner |
|
|
|
|
---
|
|
|
|
## Data Model
|
|
|
|
### Key Schema Decisions
|
|
|
|
- `sheet_data` is **opaque JSONB** — the backend never reads it; only ruleset plugins do. Adding a new ruleset requires **zero DB migrations**.
|
|
- Characters are **portable**: one character can belong to multiple groups via `group_characters`.
|
|
- Exactly **one DM per group** (`groups.dm_user_id` + `group_members.role`).
|
|
- `email_verified = true` is required before joining or creating groups.
|
|
|
|
### Core Tables
|
|
|
|
```
|
|
users id (UUID PK), email, password_hash, display_name, email_verified, created_at
|
|
groups id (UUID PK), name, dm_user_id (FK), ruleset_id, description, created_at
|
|
group_members group_id + user_id (composite PK), role ENUM(player, dm), joined_at
|
|
characters id (UUID PK), owner_user_id (FK), ruleset_id, name, sheet_data (JSONB),
|
|
portrait_url, created_at, updated_at
|
|
group_characters group_id + character_id (composite PK), user_id (FK), is_active, assigned_at
|
|
```
|
|
|
|
---
|
|
|
|
## REST API
|
|
|
|
Base URL: `https://api.campaign-manager.example.com/v1`
|
|
All endpoints require `Authorization: Bearer <accessToken>` unless marked public.
|
|
|
|
### Auth
|
|
|
|
| Method + Path | Auth | Description |
|
|
|---|---|---|
|
|
| `POST /auth/register` | Public | Create account |
|
|
| `POST /auth/login` | Public | Returns JWT pair |
|
|
| `POST /auth/refresh` | Refresh token | Rotate refresh token |
|
|
| `POST /auth/logout` | Bearer | Revoke refresh token |
|
|
|
|
### Groups
|
|
|
|
`GET /groups` · `POST /groups` · `GET /groups/:id` · `PATCH /groups/:id` · `DELETE /groups/:id`
|
|
`POST /groups/:id/invite` · `POST /groups/:id/join/:token` · `DELETE /groups/:id/members/:userId`
|
|
|
|
### Characters
|
|
|
|
`GET /characters` · `POST /characters` · `GET /characters/:id`
|
|
`PUT /characters/:id` · `PATCH /characters/:id` · `DELETE /characters/:id`
|
|
`POST /groups/:id/characters` · `DELETE /groups/:gid/characters/:cid` · `GET /groups/:id/characters`
|
|
`GET /characters/:id/history` (last 10 versions of sheet_data)
|
|
|
|
### Dice / Sessions
|
|
|
|
`POST /groups/:id/rolls` · `GET /groups/:id/rolls`
|
|
|
|
### WebSocket
|
|
|
|
One room per group ID, authenticated with the same JWT. Used for HP updates and roll history push.
|
|
|
|
---
|
|
|
|
## Ruleset Plugin Interface
|
|
|
|
Game logic is fully isolated behind a `Ruleset` interface. The core app never touches system-specific rules.
|
|
|
|
```typescript
|
|
interface Ruleset {
|
|
id: string // e.g. 'dsa5e'
|
|
name: string
|
|
version: string // semver
|
|
defaultSheet(): SheetData
|
|
validate(sheet: SheetData): ValidationResult
|
|
getStatBlocks(sheet: SheetData): StatBlock[]
|
|
getDiceActions(sheet: SheetData): DiceAction[]
|
|
renderSheet(sheet: SheetData): VNode
|
|
}
|
|
|
|
interface DiceAction {
|
|
label: string // 'Attack Roll', 'Perception Check', etc.
|
|
diceString: string // TaleSpire format: '1d20+5', '2d6'
|
|
category: string // 'attack' | 'skill' | 'save' | 'damage' | 'custom'
|
|
}
|
|
```
|
|
|
|
Plugins live in a `rulesets/` directory. Adding one = dropping in a file + registering its id. No DB changes.
|
|
|
|
**Bundled rulesets (v1):**
|
|
- `dsa5e` — Das Schwarze Auge 5e (The Dark Eye): attributes, derived values, talents, combat techniques, special abilities, spells/liturgies
|
|
- `generic` — Minimal fallback: name, HP, arbitrary key-value pairs
|
|
|
|
---
|
|
|
|
## Key Conventions
|
|
|
|
### Auth Flow
|
|
|
|
- **Symbiote**: tokens stored in `TS.storage`. On boot, check stored tokens; silently refresh via `POST /auth/refresh` if access token is expired. On any 401, attempt silent refresh before surfacing an error.
|
|
- **Web App**: access token in memory only; refresh token in `HttpOnly; Secure; SameSite=Strict` cookie. On hard refresh, silently call `POST /auth/refresh` via cookie before rendering protected routes.
|
|
- JWT access tokens: 15-minute lifetime. Refresh tokens: 30-day lifetime, **single-use with rotation**.
|
|
- Rate limit on auth endpoints: 10 req/min per IP.
|
|
|
|
### Sync Strategy
|
|
|
|
- **`TS.sync`** — lightweight broadcast to all clients on the same board running the same Symbiote. Use for transient state (e.g. initiative announcements). No backend needed.
|
|
- **WebSocket room (group-keyed)** — for persistent data (HP changes, saved rolls). Backend pushes updates to other connected clients.
|
|
- **Conflict resolution**: last-write-wins for most fields. HP uses **delta updates** (`+5 HP` not `set HP to 45`) to reduce conflicts during combat.
|
|
|
|
### Character Sheet Saves (Web App)
|
|
|
|
- Optimistic UI: changes applied locally immediately; rollback with toast on failure.
|
|
- Auto-save draft to backend every 30 seconds.
|
|
|
|
### Dice Integration
|
|
|
|
```javascript
|
|
// Programmatic roll from character sheet button
|
|
await TS.dice.putDiceInHand(action.diceString);
|
|
|
|
// Listen for resolved results and log to backend
|
|
TS.dice.onDiceResult.addListener(async (result) => {
|
|
await api.post(`/groups/${currentGroup.id}/rolls`, { ... });
|
|
});
|
|
```
|
|
|
|
`diceFinder` extra automatically makes dice-notation text clickable on any page loaded in the Symbiote — no extra code.
|
|
|
|
### Character Import (.cmchar format)
|
|
|
|
```json
|
|
{
|
|
"cm_version": "1.0",
|
|
"ruleset_id": "dsa5e",
|
|
"name": "Character Name",
|
|
"sheet_data": { }
|
|
}
|
|
```
|
|
|
|
Importing runs `ruleset.validate(sheet_data)` before creating any record. Field-level errors are surfaced in the UI.
|
|
|
|
### Portrait Upload
|
|
|
|
Client validates MIME type and size (max 2 MB) before upload. Backend issues a presigned S3 URL; client uploads directly to object storage. Backend stores only the resulting URL. k8s pods remain stateless.
|
|
|
|
### Styling
|
|
|
|
Use TaleSpire CSS variables (`--ts-background-primary`, `--ts-color-primary`, etc.) via the `colorStyles` extra. Use `OptimusPrinceps` font (via `fonts` extra) for headings. Use `ts-icon-*` classes for action buttons.
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
campaign-manager/
|
|
symbiote/ # Plain Svelte + Vite — TaleSpire Symbiote (lean bundle)
|
|
vite.config.ts
|
|
src/
|
|
main.ts # mounts <App />, waits for TS.hasInitialized
|
|
App.svelte # $currentView store drives view switching
|
|
views/ # Login, GroupList, GroupDetail, CharacterSheet, RollHistory
|
|
lib/
|
|
api.ts # shared fetch client (auto-refresh on 401)
|
|
store.ts # auth + session state
|
|
dist/ # Symbiote file root (index.html + manifest.json go here)
|
|
|
|
web/ # SvelteKit — Companion web app
|
|
svelte.config.js # adapter-static for CDN deploy
|
|
src/
|
|
routes/
|
|
lib/
|
|
|
|
backend/ # Rust + Axum
|
|
rulesets/ # Shared ruleset plugins (plain TS — imported by both frontends)
|
|
```
|
|
|
|
---
|
|
|
|
## Out of Scope (v1)
|
|
|
|
- Board state editing (tiles, minis) — TaleSpire API doesn't expose this
|
|
- Real-time collaborative editing of the same sheet simultaneously
|
|
- Mobile app
|
|
- PDF / D&D Beyond imports
|