feat: capture in-game battle damage statistics & add battle_report API endpoints
This commit is contained in:
1 parent
5c4d0d4149
commit
365e66ffe0
5 files changed
+415
No files matched your search
@@ -0,0 +1,219 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
var battleReportMu sync.RWMutex
|
||||
|
||||
const BattleReportFile = "battle_report.json"
|
||||
|
||||
type BattleSkillReport struct {
|
||||
SkillID uint32 `json:"skill_id"`
|
||||
SkillType string `json:"skill_type"`
|
||||
SkillLevel uint32 `json:"skill_level"`
|
||||
SkillUseCount uint32 `json:"skill_use_count"`
|
||||
}
|
||||
|
||||
type BattleAvatarReport struct {
|
||||
AvatarID uint32 `json:"avatar_id"`
|
||||
AvatarLevel uint32 `json:"level"`
|
||||
AvatarRank uint32 `json:"rank"`
|
||||
TotalDamage float64 `json:"total_damage"`
|
||||
DamagePercent float64 `json:"damage_percent"`
|
||||
TotalDamageTaken float64 `json:"total_damage_taken"`
|
||||
TotalHeal float64 `json:"total_heal"`
|
||||
TotalHpRecover float64 `json:"total_hp_recover"`
|
||||
TotalShield float64 `json:"total_shield"`
|
||||
TotalShieldTaken float64 `json:"total_shield_taken"`
|
||||
TotalBreakDamage float64 `json:"total_break_damage"`
|
||||
TotalTurns uint32 `json:"total_turns"`
|
||||
ExtraTurns uint32 `json:"extra_turns"`
|
||||
DieTimes uint32 `json:"die_times"`
|
||||
ReviveTimes uint32 `json:"revive_times"`
|
||||
BreakTimes uint32 `json:"break_times"`
|
||||
AttackTypeDamage map[string]float64 `json:"attack_type_damage"`
|
||||
AttackTypeBreakDamage map[string]float64 `json:"attack_type_break_damage"`
|
||||
AttackTypeMaxDamage map[string]float64 `json:"attack_type_max_damage"`
|
||||
SkillTimes []*BattleSkillReport `json:"skill_times"`
|
||||
}
|
||||
|
||||
type BattleMonsterReport struct {
|
||||
EntityID uint32 `json:"entity_id"`
|
||||
MonsterID uint32 `json:"monster_id"`
|
||||
TotalTurns uint32 `json:"total_turns"`
|
||||
TotalDamage float64 `json:"total_damage"`
|
||||
TotalDamageTaken float64 `json:"total_damage_taken"`
|
||||
Wave uint32 `json:"wave"`
|
||||
AttackTypeDamage map[string]float64 `json:"attack_type_damage"`
|
||||
}
|
||||
|
||||
type BattleReport struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
BattleID uint32 `json:"battle_id"`
|
||||
StageID uint32 `json:"stage_id"`
|
||||
EndStatus string `json:"end_status"` // "WIN", "FAIL", "QUIT"
|
||||
TotalBattleTurns uint32 `json:"total_battle_turns"`
|
||||
TotalAutoTurns uint32 `json:"total_auto_turns"`
|
||||
RoundCnt uint32 `json:"round_cnt"`
|
||||
CostTime float64 `json:"cost_time"`
|
||||
ChallengeScore uint32 `json:"challenge_score"`
|
||||
TeamTotalDamage float64 `json:"team_total_damage"`
|
||||
TeamTotalHeal float64 `json:"team_total_heal"`
|
||||
TeamTotalShield float64 `json:"team_total_shield"`
|
||||
Avatars []*BattleAvatarReport `json:"avatars"`
|
||||
Monsters []*BattleMonsterReport `json:"monsters"`
|
||||
}
|
||||
|
||||
// BattleReportFromStatistics converts a protobuf BattleStatistics packet from the game client
|
||||
// into a standardized BattleReport.
|
||||
func BattleReportFromStatistics(stt *goproto.BattleStatistics, battleID uint32, stageID uint32, endStatus string) *BattleReport {
|
||||
if stt == nil {
|
||||
return nil
|
||||
}
|
||||
report := &BattleReport{
|
||||
Timestamp: time.Now().Unix(),
|
||||
BattleID: battleID,
|
||||
StageID: stageID,
|
||||
EndStatus: endStatus,
|
||||
TotalBattleTurns: stt.TotalBattleTurns,
|
||||
TotalAutoTurns: stt.TotalAutoTurns,
|
||||
RoundCnt: stt.RoundCnt,
|
||||
CostTime: stt.CostTime,
|
||||
ChallengeScore: stt.ChallengeScore,
|
||||
Avatars: make([]*BattleAvatarReport, 0, len(stt.BattleAvatarList)),
|
||||
Monsters: make([]*BattleMonsterReport, 0, len(stt.MonsterList)),
|
||||
}
|
||||
|
||||
var teamDmg, teamHeal, teamShield float64
|
||||
for _, a := range stt.BattleAvatarList {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
teamDmg += a.TotalDamage
|
||||
teamHeal += a.TotalHeal
|
||||
teamShield += a.TotalShield
|
||||
}
|
||||
report.TeamTotalDamage = teamDmg
|
||||
report.TeamTotalHeal = teamHeal
|
||||
report.TeamTotalShield = teamShield
|
||||
|
||||
for _, a := range stt.BattleAvatarList {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
var dmgPercent float64
|
||||
if teamDmg > 0 {
|
||||
dmgPercent = (a.TotalDamage / teamDmg) * 100.0
|
||||
}
|
||||
ar := &BattleAvatarReport{
|
||||
AvatarID: a.Id,
|
||||
AvatarLevel: a.AvatarLevel,
|
||||
AvatarRank: a.AvatarRank,
|
||||
TotalDamage: a.TotalDamage,
|
||||
DamagePercent: dmgPercent,
|
||||
TotalDamageTaken: a.TotalDamageTaken,
|
||||
TotalHeal: a.TotalHeal,
|
||||
TotalHpRecover: a.TotalHpRecover,
|
||||
TotalShield: a.TotalShield,
|
||||
TotalShieldTaken: a.TotalShieldTaken,
|
||||
TotalBreakDamage: a.TotalBreakDamage,
|
||||
TotalTurns: a.TotalTurns,
|
||||
ExtraTurns: a.ExtraTurns,
|
||||
DieTimes: a.DieTimes,
|
||||
ReviveTimes: a.ReviveTimes,
|
||||
BreakTimes: a.BreakTimes,
|
||||
AttackTypeDamage: make(map[string]float64),
|
||||
AttackTypeBreakDamage: make(map[string]float64),
|
||||
AttackTypeMaxDamage: make(map[string]float64),
|
||||
SkillTimes: make([]*BattleSkillReport, 0, len(a.SkillTimes)),
|
||||
}
|
||||
for _, prop := range a.AttackTypeDamage {
|
||||
if prop != nil && prop.AttackType != "" {
|
||||
ar.AttackTypeDamage[prop.AttackType] += prop.Damage
|
||||
}
|
||||
}
|
||||
for _, prop := range a.AttackTypeBreakDamage {
|
||||
if prop != nil && prop.AttackType != "" {
|
||||
ar.AttackTypeBreakDamage[prop.AttackType] += prop.Damage
|
||||
}
|
||||
}
|
||||
for _, prop := range a.AttackTypeMaxDamage {
|
||||
if prop != nil && prop.AttackType != "" {
|
||||
ar.AttackTypeMaxDamage[prop.AttackType] = prop.Damage
|
||||
}
|
||||
}
|
||||
for _, sk := range a.SkillTimes {
|
||||
if sk != nil {
|
||||
ar.SkillTimes = append(ar.SkillTimes, &BattleSkillReport{
|
||||
SkillID: sk.SkillId,
|
||||
SkillType: sk.SkillType,
|
||||
SkillLevel: sk.SkillLevel,
|
||||
SkillUseCount: sk.SkillUseCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
report.Avatars = append(report.Avatars, ar)
|
||||
}
|
||||
|
||||
for _, m := range stt.MonsterList {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
mr := &BattleMonsterReport{
|
||||
EntityID: m.EntityId,
|
||||
MonsterID: m.MonsterId,
|
||||
TotalTurns: m.TotalTurns,
|
||||
TotalDamage: m.TotalDamage,
|
||||
TotalDamageTaken: m.TotalDamageTaken,
|
||||
Wave: m.Wave,
|
||||
AttackTypeDamage: make(map[string]float64),
|
||||
}
|
||||
for _, prop := range m.AttackTypeDamage {
|
||||
if prop != nil && prop.AttackType != "" {
|
||||
mr.AttackTypeDamage[prop.AttackType] += prop.Damage
|
||||
}
|
||||
}
|
||||
report.Monsters = append(report.Monsters, mr)
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// SaveBattleReport writes the battle report to disk atomically.
|
||||
func SaveBattleReport(report *BattleReport) error {
|
||||
if report == nil {
|
||||
return nil
|
||||
}
|
||||
battleReportMu.Lock()
|
||||
defer battleReportMu.Unlock()
|
||||
return writeJSONAtomic(BattleReportFile, report)
|
||||
}
|
||||
|
||||
// LoadLatestBattleReport loads the most recent battle report from disk.
|
||||
func LoadLatestBattleReport() (*BattleReport, error) {
|
||||
battleReportMu.RLock()
|
||||
defer battleReportMu.RUnlock()
|
||||
raw, err := os.ReadFile(BattleReportFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var report BattleReport
|
||||
if err := json.Unmarshal(raw, &report); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
// ClearBattleReport deletes the current battle report file.
|
||||
func ClearBattleReport() error {
|
||||
battleReportMu.Lock()
|
||||
defer battleReportMu.Unlock()
|
||||
_ = os.Remove(BattleReportFile)
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user