feat: combat stats backend + frontend (Mag-Tools style)
Backend:
- db_async.py: new combat_stats + combat_stats_sessions tables
- main.py: combat_stats message handler with DB upsert (lifetime +
session snapshots), in-memory live_combat_stats dict, broadcast
to browser clients.
- REST: GET /combat-stats and GET /combat-stats/{character_name}
Frontend:
- index.html: new "Combat Stats" sidebar link
- script.js: full Combat Stats window with two panels:
- Top: monster list (name, kills, dmg recv, dmg given) with
clickable rows and "All" aggregate, matching CombatTrackerGUI.cs
- Bottom: damage breakdown grid matching CombatTrackerGUIInfo.cs
layout — element × attack type matrix (Mel/Msl + Magic columns),
Attacks (hit%), Evades (%), Resists (%), A.Surges (%), C.Surges (%),
normal Avg/Max, Crits (%), Crit Avg/Max, Total Damage.
- Session / Lifetime toggle button
- style.css: combat-stats-toggle styles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da4e840581
commit
c03b1c19f2
5 changed files with 509 additions and 0 deletions
102
main.py
102
main.py
|
|
@ -62,6 +62,8 @@ from db_async import (
|
|||
portals,
|
||||
server_health_checks,
|
||||
server_status,
|
||||
combat_stats,
|
||||
combat_stats_sessions,
|
||||
users,
|
||||
init_db_async,
|
||||
cleanup_old_portals,
|
||||
|
|
@ -1733,6 +1735,56 @@ async def get_vital_sharing_peers():
|
|||
}
|
||||
|
||||
|
||||
@app.get("/combat-stats/{character_name}")
|
||||
async def get_combat_stats(character_name: str):
|
||||
"""Get lifetime combat stats for a character."""
|
||||
# Prefer live in-memory data (more up-to-date), fall back to DB
|
||||
live = live_combat_stats.get(character_name)
|
||||
if live:
|
||||
return {
|
||||
"character_name": character_name,
|
||||
"session": live.get("session"),
|
||||
"lifetime": live.get("lifetime"),
|
||||
}
|
||||
row = await database.fetch_one(
|
||||
"SELECT stats_data FROM combat_stats WHERE character_name = :name",
|
||||
{"name": character_name},
|
||||
)
|
||||
if not row:
|
||||
return {"character_name": character_name, "session": None, "lifetime": None}
|
||||
return {
|
||||
"character_name": character_name,
|
||||
"session": None,
|
||||
"lifetime": row["stats_data"],
|
||||
}
|
||||
|
||||
|
||||
@app.get("/combat-stats")
|
||||
async def get_all_combat_stats():
|
||||
"""Get combat stats for all characters with live data."""
|
||||
results = []
|
||||
seen = set()
|
||||
# Live data first (most current)
|
||||
for char, data in live_combat_stats.items():
|
||||
seen.add(char)
|
||||
results.append({
|
||||
"character_name": char,
|
||||
"session": data.get("session"),
|
||||
"lifetime": data.get("lifetime"),
|
||||
})
|
||||
# Fill in from DB for characters not currently live
|
||||
rows = await database.fetch_all("SELECT character_name, stats_data FROM combat_stats")
|
||||
for r in rows:
|
||||
if r["character_name"] not in seen:
|
||||
results.append({
|
||||
"character_name": r["character_name"],
|
||||
"session": None,
|
||||
"lifetime": r["stats_data"],
|
||||
})
|
||||
results.sort(key=lambda x: x["character_name"])
|
||||
return {"stats": results}
|
||||
|
||||
|
||||
@app.get("/server-health")
|
||||
async def get_server_health():
|
||||
"""Return current server health status."""
|
||||
|
|
@ -2417,6 +2469,10 @@ _vital_sharing_subscribers: set[str] = set()
|
|||
# and so the browser NetworkUI can populate without waiting for the next tick.
|
||||
_vital_sharing_peer_state: Dict[str, dict] = {}
|
||||
|
||||
# --- Combat stats (Mag-Tools style per-character combat tracking) ----------
|
||||
# Latest combat_stats payload per character for real-time display.
|
||||
live_combat_stats: Dict[str, dict] = {}
|
||||
|
||||
|
||||
async def _broadcast_share_to_plugin_clients(data: dict, origin: str) -> None:
|
||||
"""Forward a share_* message to all opted-in plugin clients except origin.
|
||||
|
|
@ -2877,6 +2933,52 @@ async def ws_receive_snapshots(
|
|||
f"Broadcasted chat message from {data.get('character_name', 'unknown')}"
|
||||
)
|
||||
continue
|
||||
# --- Combat stats: store + broadcast Mag-Tools style combat data ---
|
||||
if msg_type == "combat_stats":
|
||||
char = data.get("character_name")
|
||||
if char:
|
||||
live_combat_stats[char] = data
|
||||
# Upsert lifetime stats into DB
|
||||
try:
|
||||
lifetime = data.get("lifetime")
|
||||
if lifetime:
|
||||
await database.execute(
|
||||
combat_stats.delete().where(
|
||||
combat_stats.c.character_name == char
|
||||
)
|
||||
)
|
||||
await database.execute(
|
||||
combat_stats.insert().values(
|
||||
character_name=char,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
stats_data=lifetime,
|
||||
)
|
||||
)
|
||||
# Store session snapshot (latest per session)
|
||||
session_data = data.get("session")
|
||||
session_id = data.get("session_id")
|
||||
if session_data and session_id:
|
||||
# Delete old snapshot for this session, then insert fresh
|
||||
await database.execute(
|
||||
combat_stats_sessions.delete().where(
|
||||
(combat_stats_sessions.c.character_name == char)
|
||||
& (combat_stats_sessions.c.session_id == session_id)
|
||||
)
|
||||
)
|
||||
await database.execute(
|
||||
combat_stats_sessions.insert().values(
|
||||
character_name=char,
|
||||
session_id=session_id,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
stats_data=session_data,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store combat stats for {char}: {e}")
|
||||
# Broadcast to browser clients for live display
|
||||
await _broadcast_to_browser_clients(data)
|
||||
continue
|
||||
|
||||
# --- Full inventory message: store complete inventory snapshot ---
|
||||
if msg_type == "full_inventory":
|
||||
payload = data.copy()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue