diff --git a/docs/plans/2026-02-26-character-stats-design.md b/docs/plans/2026-02-26-character-stats-design.md new file mode 100644 index 00000000..0efd8967 --- /dev/null +++ b/docs/plans/2026-02-26-character-stats-design.md @@ -0,0 +1,308 @@ +# Character Stats Window - Design Document + +## Overview + +Add a live character stats window to the Dereth Tracker map interface, styled as an Asheron's Call game UI replica. Accessible via a "Char" button on each player in the list, alongside the existing Chat, Stats, and Inventory buttons. + +**Scope:** MosswartOverlord only (database, backend, frontend). The plugin implementation is a separate follow-up with a handoff spec. + +--- + +## Architecture: Single Event + JSONB Table with Indexed Columns + +One new `character_stats` event type from the plugin. Backend stores in a single `character_stats` table with key columns extracted for efficient SQL queries (level, XP, luminance) plus a `stats_data` JSONB column for the full payload. In-memory cache for live display, DB for persistence. + +--- + +## Database Schema + +New table `character_stats`: + +```sql +CREATE TABLE character_stats ( + character_name VARCHAR(255) NOT NULL, + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + level INTEGER, + total_xp BIGINT, + unassigned_xp BIGINT, + luminance_earned BIGINT, + luminance_total BIGINT, + deaths INTEGER, + stats_data JSONB NOT NULL, + PRIMARY KEY (character_name) +); +``` + +Single row per character, upserted on each 10-minute update. + +### JSONB `stats_data` Structure + +```json +{ + "attributes": { + "strength": {"base": 290, "creation": 100}, + "endurance": {"base": 200, "creation": 100}, + "coordination": {"base": 240, "creation": 100}, + "quickness": {"base": 220, "creation": 10}, + "focus": {"base": 250, "creation": 100}, + "self": {"base": 200, "creation": 100} + }, + "vitals": { + "health": {"base": 341}, + "stamina": {"base": 400}, + "mana": {"base": 300} + }, + "skills": { + "war_magic": {"base": 533, "training": "Specialized"}, + "melee_defense": {"base": 488, "training": "Specialized"}, + "life_magic": {"base": 440, "training": "Trained"}, + "arcane_lore": {"base": 10, "training": "Untrained"} + }, + "allegiance": { + "name": "Knights of Dereth", + "monarch": {"name": "KingName", "race": 1, "rank": 0, "gender": 0}, + "patron": {"name": "PatronName", "race": 2, "rank": 5, "gender": 1}, + "rank": 10, + "followers": 5 + }, + "race": "Aluvian", + "gender": "Male", + "birth": "2018-03-15 14:22:33", + "current_title": 42, + "skill_credits": 0 +} +``` + +--- + +## Backend + +### WebSocket Handler (`main.py`) + +New `character_stats` event type in the `/ws/position` handler. Same pattern as vitals: + +1. **Validate** with `CharacterStatsMessage` Pydantic model +2. **Cache** in `live_character_stats: Dict[str, dict]` for instant access +3. **Persist** to `character_stats` table via upsert (`INSERT ... ON CONFLICT (character_name) DO UPDATE`) +4. **Broadcast** to browser clients via `_broadcast_to_browser_clients()` + +### Pydantic Model + +```python +class CharacterStatsMessage(BaseModel): + character_name: str + timestamp: datetime + level: Optional[int] + total_xp: Optional[int] + unassigned_xp: Optional[int] + luminance_earned: Optional[int] + luminance_total: Optional[int] + deaths: Optional[int] + race: Optional[str] + gender: Optional[str] + birth: Optional[str] + current_title: Optional[int] + skill_credits: Optional[int] + attributes: Optional[dict] + vitals: Optional[dict] + skills: Optional[dict] + allegiance: Optional[dict] +``` + +### HTTP Endpoint + +``` +GET /api/character-stats/{name} +``` + +Returns latest stats for a character. Checks in-memory cache first, falls back to DB. Used when a browser opens a character window after the initial broadcast. + +### Test Endpoint (temporary, for development) + +``` +POST /api/character-stats/test +``` + +Accepts a mock `character_stats` payload, processes it through the same pipeline (cache + DB + broadcast). Allows full end-to-end testing without the plugin running. + +--- + +## Frontend + +### Character Button + +New "Char" button in the player list, same pattern as Chat/Stats/Inventory: + +```javascript +const charBtn = document.createElement('button'); +charBtn.className = 'char-btn'; +charBtn.textContent = 'Char'; +// click -> showCharacterWindow(playerData.character_name) +``` + +### `showCharacterWindow(name)` + +Uses existing `createWindow` helper. Window size: 450x650px (tall and narrow like the game panel). + +**Data loading:** +1. On open, fetch `GET /api/character-stats/{name}` +2. Listen for `character_stats` WebSocket broadcasts to update live +3. Vitals bars update from existing `vitals` WebSocket messages (5-second stream) +4. If no data exists, show "Awaiting character data..." placeholder + +### Window Layout + +Stacked vertically, mimicking the AC character panel: + +1. **Header** - Character name, level, race/gender, title. Gold text on dark background. + +2. **Attributes panel** - 3x2 grid: + ``` + Strength 290 Quickness 220 + Endurance 200 Focus 250 + Coordination 240 Self 200 + ``` + Base values shown, creation values in smaller text. + +3. **Vitals bars** - Red (HP), yellow (Stamina), blue (Mana) bars with current/max numbers. Live-updating from existing vitals stream. + +4. **Skills section** - Scrollable, grouped by training level: + - **Specialized** (gold text) + - **Trained** (white text) + - **Untrained** (grey text) + Each shows skill name + level. + +5. **Allegiance section** - Monarch, patron, rank, followers count. + +6. **Footer** - XP, unassigned XP, luminance, deaths, birth date. + +--- + +## Styling: AC Game UI Replica + +Color palette drawn from the Asheron's Call interface: + +```css +--ac-bg: #1a1410; /* Dark brown/black background */ +--ac-panel: #2a2218; /* Panel background */ +--ac-border: #8b7355; /* Gold/brown borders */ +--ac-header: #d4a843; /* Gold header text */ +--ac-text: #c8b89a; /* Parchment-colored body text */ +--ac-text-dim: #7a6e5e; /* Dimmed/secondary text */ +--ac-specialized: #d4a843; /* Gold for specialized skills */ +--ac-trained: #c8b89a; /* Light for trained */ +--ac-untrained: #5a5248; /* Grey for untrained */ +``` + +Vitals bar colors: +- Health: `#8b1a1a` bg, `#cc3333` fill (red) +- Stamina: `#8b7a1a` bg, `#ccaa33` fill (yellow) +- Mana: `#1a3a8b` bg, `#3366cc` fill (blue) + +Panel styling: +- Subtle inner border with gold/brown +- CSS gradient background to simulate parchment grain (no image files) +- Section dividers as thin gold lines +- Skill rows with subtle hover highlight +- Compact padding (information-dense like the game UI) + +--- + +## Plugin Event Contract + +The plugin will send a `character_stats` message via the existing WebSocket connection: + +- **Frequency:** On login + every 10 minutes +- **Channel:** Existing `/ws/position` WebSocket + +```json +{ + "type": "character_stats", + "timestamp": "2026-02-26T12:34:56Z", + "character_name": "Barris", + "level": 275, + "race": "Aluvian", + "gender": "Male", + "birth": "2018-03-15 14:22:33", + "total_xp": 191226310247, + "unassigned_xp": 0, + "skill_credits": 0, + "luminance_earned": 500000, + "luminance_total": 1500000, + "deaths": 3175, + "current_title": 42, + "attributes": { + "strength": {"base": 290, "creation": 100}, + "endurance": {"base": 200, "creation": 100}, + "coordination": {"base": 240, "creation": 100}, + "quickness": {"base": 220, "creation": 10}, + "focus": {"base": 250, "creation": 100}, + "self": {"base": 200, "creation": 100} + }, + "vitals": { + "health": {"base": 341}, + "stamina": {"base": 400}, + "mana": {"base": 300} + }, + "skills": { + "war_magic": {"base": 533, "training": "Specialized"}, + "melee_defense": {"base": 488, "training": "Specialized"}, + "life_magic": {"base": 440, "training": "Trained"}, + "arcane_lore": {"base": 10, "training": "Untrained"} + }, + "allegiance": { + "name": "Knights of Dereth", + "monarch": {"name": "KingName", "race": 1, "rank": 0, "gender": 0}, + "patron": {"name": "PatronName", "race": 2, "rank": 5, "gender": 1}, + "rank": 10, + "followers": 5 + } +} +``` + +--- + +## Data Flow + +``` +Plugin (every 10 min + on login) + │ character_stats JSON via /ws/position + ▼ +Backend handler + │ Pydantic validation + ├──▶ live_character_stats cache (in-memory) + ├──▶ character_stats table (upsert) + └──▶ _broadcast_to_browser_clients() + │ + ▼ +/ws/live → Browser + │ message.type === 'character_stats' + ▼ +Character window updates live + +Browser can also fetch on demand: +GET /api/character-stats/{name} → cache → DB fallback +``` + +Vitals (HP/Stam/Mana) update separately via existing 5-second vitals stream. + +--- + +## Files Modified + +| File | Changes | +|------|---------| +| `main.py` | New `character_stats` handler, Pydantic model, in-memory cache, HTTP endpoint, test endpoint | +| `db_async.py` | New `character_stats` table definition | +| `static/script.js` | New "Char" button, `showCharacterWindow()`, WebSocket listener for `character_stats` | +| `static/style.css` | AC-themed character window styles | + +--- + +## What's NOT in Scope + +- Plugin implementation (separate follow-up with handoff spec) +- Historical stat tracking over time (table supports it but no UI yet) +- Skill icons from the game (text-only for v1) +- Title name resolution (show title ID, not name) +- Vassal list display (just monarch/patron/rank/followers)