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