feat(combat): backend-side lifetime accumulation from session deltas
Plugin now sends only session data (lifetime=null). Backend computes the delta between consecutive session snapshots (new - previous) and merges it into a persisted lifetime record in the combat_stats table. - _combat_last_session: tracks last-seen session per char:session_id - _combat_lifetime_cache: in-memory lifetime per character - _combat_session_delta(): computes diff between two cumulative snapshots - _combat_merge_into_lifetime(): adds delta totals into lifetime record - First snapshot for a new session = entire session is the delta - Lifetime loaded from DB on first message, cached in memory after - Broadcast enriched with computed lifetime so frontend gets both This means session resets on login but lifetime persists in DB across all sessions. The two will now show different data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2cd68d0368
commit
0a0fdc5b3d
1 changed files with 118 additions and 22 deletions
140
main.py
140
main.py
|
|
@ -2472,6 +2472,74 @@ _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] = {}
|
||||
_combat_last_session: Dict[str, dict] = {} # key = "char:session_id" → last session snapshot
|
||||
_combat_lifetime_cache: Dict[str, dict] = {} # char → accumulated lifetime stats
|
||||
|
||||
|
||||
def _combat_session_delta(new: dict, old: dict) -> dict:
|
||||
"""Compute the difference between two cumulative session snapshots."""
|
||||
delta = {
|
||||
"total_damage_given": (new.get("total_damage_given", 0) or 0) - (old.get("total_damage_given", 0) or 0),
|
||||
"total_damage_received": (new.get("total_damage_received", 0) or 0) - (old.get("total_damage_received", 0) or 0),
|
||||
"total_kills": (new.get("total_kills", 0) or 0) - (old.get("total_kills", 0) or 0),
|
||||
"total_aetheria_surges": (new.get("total_aetheria_surges", 0) or 0) - (old.get("total_aetheria_surges", 0) or 0),
|
||||
"total_cloak_surges": (new.get("total_cloak_surges", 0) or 0) - (old.get("total_cloak_surges", 0) or 0),
|
||||
"monsters": {},
|
||||
}
|
||||
new_monsters = new.get("monsters", {})
|
||||
old_monsters = old.get("monsters", {})
|
||||
for name, nm in new_monsters.items():
|
||||
om = old_monsters.get(name, {})
|
||||
dm = {
|
||||
"name": name,
|
||||
"kill_count": (nm.get("kill_count", 0) or 0) - (om.get("kill_count", 0) or 0),
|
||||
"damage_given": (nm.get("damage_given", 0) or 0) - (om.get("damage_given", 0) or 0),
|
||||
"damage_received": (nm.get("damage_received", 0) or 0) - (om.get("damage_received", 0) or 0),
|
||||
"aetheria_surges": (nm.get("aetheria_surges", 0) or 0) - (om.get("aetheria_surges", 0) or 0),
|
||||
"cloak_surges": (nm.get("cloak_surges", 0) or 0) - (om.get("cloak_surges", 0) or 0),
|
||||
"offense": nm.get("offense", {}), # offense/defense are complex nested — just use latest
|
||||
"defense": nm.get("defense", {}),
|
||||
}
|
||||
# Only include if there's actual change
|
||||
if dm["kill_count"] > 0 or dm["damage_given"] > 0 or dm["damage_received"] > 0:
|
||||
delta["monsters"][name] = dm
|
||||
return delta
|
||||
|
||||
|
||||
def _combat_merge_into_lifetime(lifetime: dict, delta: dict) -> dict:
|
||||
"""Merge a session delta into the accumulated lifetime stats."""
|
||||
if not lifetime:
|
||||
lifetime = {
|
||||
"total_damage_given": 0, "total_damage_received": 0,
|
||||
"total_kills": 0, "total_aetheria_surges": 0, "total_cloak_surges": 0,
|
||||
"monsters": {},
|
||||
}
|
||||
lifetime["total_damage_given"] = (lifetime.get("total_damage_given", 0) or 0) + (delta.get("total_damage_given", 0) or 0)
|
||||
lifetime["total_damage_received"] = (lifetime.get("total_damage_received", 0) or 0) + (delta.get("total_damage_received", 0) or 0)
|
||||
lifetime["total_kills"] = (lifetime.get("total_kills", 0) or 0) + (delta.get("total_kills", 0) or 0)
|
||||
lifetime["total_aetheria_surges"] = (lifetime.get("total_aetheria_surges", 0) or 0) + (delta.get("total_aetheria_surges", 0) or 0)
|
||||
lifetime["total_cloak_surges"] = (lifetime.get("total_cloak_surges", 0) or 0) + (delta.get("total_cloak_surges", 0) or 0)
|
||||
|
||||
lt_monsters = lifetime.setdefault("monsters", {})
|
||||
for name, dm in delta.get("monsters", {}).items():
|
||||
if name not in lt_monsters:
|
||||
lt_monsters[name] = {
|
||||
"name": name, "kill_count": 0, "damage_given": 0, "damage_received": 0,
|
||||
"aetheria_surges": 0, "cloak_surges": 0, "offense": {}, "defense": {},
|
||||
}
|
||||
lm = lt_monsters[name]
|
||||
lm["kill_count"] = (lm.get("kill_count", 0) or 0) + (dm.get("kill_count", 0) or 0)
|
||||
lm["damage_given"] = (lm.get("damage_given", 0) or 0) + (dm.get("damage_given", 0) or 0)
|
||||
lm["damage_received"] = (lm.get("damage_received", 0) or 0) + (dm.get("damage_received", 0) or 0)
|
||||
lm["aetheria_surges"] = (lm.get("aetheria_surges", 0) or 0) + (dm.get("aetheria_surges", 0) or 0)
|
||||
lm["cloak_surges"] = (lm.get("cloak_surges", 0) or 0) + (dm.get("cloak_surges", 0) or 0)
|
||||
# For offense/defense, use latest from delta (nested merge is complex and not needed
|
||||
# since the backend doesn't need per-element lifetime accuracy — session view has that)
|
||||
if dm.get("offense"):
|
||||
lm["offense"] = dm["offense"]
|
||||
if dm.get("defense"):
|
||||
lm["defense"] = dm["defense"]
|
||||
return lifetime
|
||||
|
||||
|
||||
async def _broadcast_share_to_plugin_clients(data: dict, origin: str) -> None:
|
||||
|
|
@ -2937,11 +3005,37 @@ async def ws_receive_snapshots(
|
|||
if msg_type == "combat_stats":
|
||||
char = data.get("character_name")
|
||||
if char:
|
||||
live_combat_stats[char] = data
|
||||
# Upsert lifetime stats into DB
|
||||
session_data = data.get("session")
|
||||
session_id = data.get("session_id") or ""
|
||||
try:
|
||||
lifetime = data.get("lifetime")
|
||||
if lifetime:
|
||||
if session_data:
|
||||
# ── Compute delta and accumulate into lifetime ──
|
||||
# The plugin sends cumulative session totals. We track
|
||||
# the last-seen totals and add the difference to lifetime.
|
||||
prev_key = f"{char}:{session_id}"
|
||||
prev = _combat_last_session.get(prev_key)
|
||||
_combat_last_session[prev_key] = session_data
|
||||
|
||||
if prev is not None:
|
||||
delta = _combat_session_delta(session_data, prev)
|
||||
else:
|
||||
# First snapshot for this session — entire session is the delta
|
||||
delta = session_data
|
||||
|
||||
# Load existing lifetime from DB (or in-memory cache)
|
||||
if char not in _combat_lifetime_cache:
|
||||
row = await database.fetch_one(
|
||||
"SELECT stats_data FROM combat_stats WHERE character_name = :n",
|
||||
{"n": char},
|
||||
)
|
||||
_combat_lifetime_cache[char] = row["stats_data"] if row else {}
|
||||
|
||||
lifetime = _combat_merge_into_lifetime(
|
||||
_combat_lifetime_cache[char], delta
|
||||
)
|
||||
_combat_lifetime_cache[char] = lifetime
|
||||
|
||||
# Persist lifetime to DB
|
||||
await database.execute(
|
||||
combat_stats.delete().where(
|
||||
combat_stats.c.character_name == char
|
||||
|
|
@ -2954,28 +3048,30 @@ async def ws_receive_snapshots(
|
|||
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)
|
||||
|
||||
# Store session snapshot
|
||||
if session_id:
|
||||
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,
|
||||
await database.execute(
|
||||
combat_stats_sessions.insert().values(
|
||||
character_name=char,
|
||||
session_id=session_id,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
stats_data=session_data,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Enrich the broadcast with computed lifetime
|
||||
data["lifetime"] = lifetime
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store combat stats for {char}: {e}")
|
||||
# Broadcast to browser clients for live display
|
||||
|
||||
live_combat_stats[char] = data
|
||||
await _broadcast_to_browser_clients(data)
|
||||
continue
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue