Add character stats window design document
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
40198fa0cf
commit
7d52ac2fe4
1 changed files with 308 additions and 0 deletions
308
docs/plans/2026-02-26-character-stats-design.md
Normal file
308
docs/plans/2026-02-26-character-stats-design.md
Normal file
|
|
@ -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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue