MosswartOverlord/docs/plans/2026-02-26-character-stats-design.md
erik 7d52ac2fe4 Add character stats window design document
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:52:32 +00:00

9.1 KiB

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:

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

{
  "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

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:

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:

--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
{
  "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)