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

308 lines
9.1 KiB
Markdown

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