feat(go-services): Phase 2 — combat_stats accumulator (cross-language exact)
Ports main.py's _combat_session_delta / _combat_merge_into_lifetime (incl. the documented "offense/defense use latest, additively" quirk) and the combat_stats handler (session delta -> DB-backed lifetime merge -> delete-then-insert of combat_stats + combat_stats_sessions). Read handlers gain the live combat overlay (union live + DB), like Python. Validation: - combat.go `combat-merge` CLI folds snapshots through the accumulator; diffed against the Python functions on identical input -> byte-IDENTICAL. - combat_test.go golden test runs in the build (go test now part of the tracker Dockerfile). - Live: 40 combat lifetime rows + 40 session snapshots + rare_events flowing in dereth_go via the shadow consumer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a5d69ba88d
commit
7350b00341
6 changed files with 347 additions and 4 deletions
|
|
@ -32,7 +32,9 @@ type Ingestor struct {
|
|||
liveCombatStats map[string]map[string]any
|
||||
dungeonMapCache map[string]map[string]any
|
||||
questStatus map[string]map[string]string
|
||||
lastKills map[string]int // "session_id|character_name" -> kills
|
||||
lastKills map[string]int // "session_id|character_name" -> kills
|
||||
combatLastSession map[string]map[string]any // "char:session_id" -> last cumulative session
|
||||
combatLifetimeCache map[string]map[string]any // character_name -> accumulated lifetime
|
||||
}
|
||||
|
||||
func newIngestor(pool *pgxpool.Pool, log *slog.Logger, broadcast func(map[string]any)) *Ingestor {
|
||||
|
|
@ -49,6 +51,8 @@ func newIngestor(pool *pgxpool.Pool, log *slog.Logger, broadcast func(map[string
|
|||
dungeonMapCache: map[string]map[string]any{},
|
||||
questStatus: map[string]map[string]string{},
|
||||
lastKills: map[string]int{},
|
||||
combatLastSession: map[string]map[string]any{},
|
||||
combatLifetimeCache: map[string]map[string]any{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,10 +82,12 @@ func (i *Ingestor) dispatch(ctx context.Context, data map[string]any) {
|
|||
i.handleNearbyObjects(data)
|
||||
case t == "dungeon_map":
|
||||
i.handleDungeonMap(data)
|
||||
case t == "combat_stats":
|
||||
i.handleCombatStats(ctx, data)
|
||||
case t == "register":
|
||||
// no DB / no broadcast; plugin_conns belongs to the /ws/position server
|
||||
case t == "combat_stats", strings.HasPrefix(t, "share_"), t == "chat":
|
||||
// combat_stats + share_* handled in a later pass; chat is broadcast-only
|
||||
case strings.HasPrefix(t, "share_"), t == "chat":
|
||||
// share_* handled in a later pass; chat is broadcast-only
|
||||
}
|
||||
if i.broadcast != nil {
|
||||
i.broadcast(data)
|
||||
|
|
@ -366,6 +372,18 @@ func (i *Ingestor) getCharacterStats(name string) (map[string]any, bool) {
|
|||
func (i *Ingestor) getEquipmentCantrip(name string) (map[string]any, bool) {
|
||||
return i.snapshot(i.liveEquipmentCantrip, name)
|
||||
}
|
||||
func (i *Ingestor) getCombatStats(name string) (map[string]any, bool) {
|
||||
return i.snapshot(i.liveCombatStats, name)
|
||||
}
|
||||
func (i *Ingestor) allCombatStats() map[string]map[string]any {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
out := make(map[string]map[string]any, len(i.liveCombatStats))
|
||||
for k, v := range i.liveCombatStats {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
func (i *Ingestor) questData() (map[string]map[string]string, int) {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue