# 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)