package common import ( "os" "testing" goproto "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) func TestBattleReportUsesActualBattleSlots(t *testing.T) { r := &BattleReport{Avatars: []*BattleAvatarReport{{AvatarID: 11}, {AvatarID: 10}}} ApplyBattleReportContext(r, &goproto.SceneBattleInfo{BattleId: 7, StageId: 20, BattleAvatarList: []*goproto.BattleAvatar{{Id: 10, Index: 0}, {Id: 11, Index: 2}}}) if r.BattleID != 7 || r.StageID != 20 || r.Avatars[0].AvatarID != 10 || r.Avatars[0].Slot == nil || *r.Avatars[0].Slot != 0 || *r.Avatars[1].Slot != 2 { t.Fatalf("battle identity/order lost: %+v", r) } } func TestBattleReportSerialization(t *testing.T) { defer os.Remove(BattleReportFile) stt := &goproto.BattleStatistics{ TotalBattleTurns: 12, TotalAutoTurns: 0, RoundCnt: 2, CostTime: 45.5, ChallengeScore: 1000, BattleAvatarList: []*goproto.AvatarBattleInfo{ { Id: 1310, AvatarLevel: 80, AvatarRank: 2, TotalDamage: 500000, TotalDamageTaken: 2500, TotalHeal: 0, TotalBreakDamage: 400000, TotalTurns: 6, AttackTypeDamage: []*goproto.AttackDamageProperty{ {AttackType: "Normal", Damage: 20000}, {AttackType: "BPSkill", Damage: 80000}, {AttackType: "BreakDamage", Damage: 400000}, }, AttackTypeMaxDamage: []*goproto.AttackDamageProperty{ {AttackType: "BreakDamage", Damage: 250000}, }, SkillTimes: []*goproto.SkillUseProperty{ {SkillId: 131001, SkillType: "Normal", SkillUseCount: 1}, {SkillId: 131002, SkillType: "BPSkill", SkillUseCount: 4}, }, }, { Id: 1001, AvatarLevel: 80, AvatarRank: 6, TotalDamage: 50000, TotalDamageTaken: 1200, TotalShield: 4500, TotalTurns: 6, AttackTypeDamage: []*goproto.AttackDamageProperty{ {AttackType: "Normal", Damage: 50000}, }, }, }, MonsterList: []*goproto.MonsterBattleInfo{ { EntityId: 101, MonsterId: 3024010, TotalTurns: 3, TotalDamageTaken: 550000, TotalDamage: 3700, Wave: 1, }, }, } report := BattleReportFromStatistics(stt, 999, 1001, "WIN") if report == nil { t.Fatal("expected report, got nil") } if report.TeamTotalDamage != 550000 { t.Fatalf("expected team total damage 550000, got %f", report.TeamTotalDamage) } if len(report.Avatars) != 2 { t.Fatalf("expected 2 avatars, got %d", len(report.Avatars)) } if report.Avatars[0].DamagePercent < 90 || report.Avatars[0].DamagePercent > 91 { t.Fatalf("unexpected damage percent %f", report.Avatars[0].DamagePercent) } if err := SaveBattleReport(report); err != nil { t.Fatalf("failed to save report: %v", err) } loaded, err := LoadLatestBattleReport() if err != nil { t.Fatalf("failed to load report: %v", err) } if loaded.BattleID != 999 { t.Fatalf("expected battle id 999, got %d", loaded.BattleID) } if loaded.TeamTotalDamage != 550000 { t.Fatalf("expected team damage 550000, got %f", loaded.TeamTotalDamage) } if err := ClearBattleReport(); err != nil { t.Fatalf("failed to clear report: %v", err) } _, err = LoadLatestBattleReport() if err == nil { t.Fatal("expected error after clearing report, got nil") } } func TestBattleReportUsesAvatarDamageEventsWhenAvatarTotalsAreZero(t *testing.T) { stt := &goproto.BattleStatistics{ BattleAvatarList: []*goproto.AvatarBattleInfo{ {Id: 1503}, {Id: 1511}, }, OKFALMGFHMA: []*goproto.HDKEPPDIDIH{ {AvatarId: 1503, Damage: 720}, {AvatarId: 1503, Damage: 180}, {AvatarId: 1511, Damage: 100}, }, MonsterList: []*goproto.MonsterBattleInfo{{TotalDamageTaken: 1000}}, } report := BattleReportFromStatistics(stt, 1, 1052106, "WIN") if report.TeamTotalDamage != 1000 { t.Fatalf("expected event-derived team damage 1000, got %f", report.TeamTotalDamage) } if report.Avatars[0].TotalDamage != 900 || report.Avatars[0].DamagePercent != 90 { t.Fatalf("unexpected avatar 1503 report: %+v", report.Avatars[0]) } if report.Avatars[1].TotalDamage != 100 || report.Avatars[1].DamagePercent != 10 { t.Fatalf("unexpected avatar 1511 report: %+v", report.Avatars[1]) } } func TestBattleReportUsesDealtDamagePropertiesAndDoesNotCountDamageTaken(t *testing.T) { stt := &goproto.BattleStatistics{ BattleAvatarList: []*goproto.AvatarBattleInfo{ { Id: 1503, AttackTypeDamage: []*goproto.AttackDamageProperty{ {AttackType: "Normal", Damage: 600}, }, // Native NOGHNMCDDAI contains damage taken, not dealt. NOGHNMCDDAI: []*goproto.AttackDamageProperty{ {AttackType: "Normal", Damage: 900}, }, }, { Id: 1511, NOGHNMCDDAI: []*goproto.AttackDamageProperty{ {AttackType: "Skill", Damage: 400}, }, }, }, MonsterList: []*goproto.MonsterBattleInfo{{TotalDamageTaken: 1200}}, } report := BattleReportFromStatistics(stt, 1, 0, "WIN") if report.TeamTotalDamage != 600 { t.Fatalf("expected dealt damage 600, got %f", report.TeamTotalDamage) } if report.Avatars[0].TotalDamage != 600 || report.Avatars[0].DamagePercent != 100 { t.Fatalf("unexpected avatar 1503 report: %+v", report.Avatars[0]) } if report.Avatars[1].TotalDamage != 0 || report.Avatars[1].DamagePercent != 0 { t.Fatalf("unexpected avatar 1511 report: %+v", report.Avatars[1]) } if len(report.Avatars[1].AttackTypeDamage) != 0 { t.Fatalf("damage taken became dealt damage: %+v", report.Avatars[1].AttackTypeDamage) } } func TestBattleReportFallsBackToMonsterDamageTakenForTeamTotal(t *testing.T) { stt := &goproto.BattleStatistics{ BattleAvatarList: []*goproto.AvatarBattleInfo{{Id: 1503}}, MonsterList: []*goproto.MonsterBattleInfo{ {TotalDamageTaken: 250}, {TotalDamageTaken: 750}, }, } report := BattleReportFromStatistics(stt, 1, 1052106, "WIN") if report.TeamTotalDamage != 1000 { t.Fatalf("expected monster-derived team damage 1000, got %f", report.TeamTotalDamage) } if report.Avatars[0].DamagePercent != 0 { t.Fatalf("cannot infer avatar share from monster totals, got %f", report.Avatars[0].DamagePercent) } } func TestBattleReportUsesSupplementalAvatarDamageWhenStandardFieldsAreEmpty(t *testing.T) { stt := &goproto.BattleStatistics{ BattleAvatarList: []*goproto.AvatarBattleInfo{ {Id: 1409}, {Id: 1507}, }, MFOJKJGKDGD: &goproto.FAHGFCIJHFK{ TotalDamage: 1000, INBNCAKGDPJ: []*goproto.PMPCIBGILIE{ {AvatarId: 1409, Damage: 400}, {AvatarId: 1507, Damage: 600}, }, }, MonsterList: []*goproto.MonsterBattleInfo{{TotalDamageTaken: 1500}}, } report := BattleReportFromStatistics(stt, 1, 1052106, "WIN") if report.TeamTotalDamage != 1000 { t.Fatalf("expected supplemental team total damage 1000, got %f", report.TeamTotalDamage) } if report.Avatars[0].TotalDamage != 400 || report.Avatars[0].DamagePercent != 40 { t.Fatalf("unexpected avatar 1409 report: %+v", report.Avatars[0]) } if report.Avatars[1].TotalDamage != 600 || report.Avatars[1].DamagePercent != 60 { t.Fatalf("unexpected avatar 1507 report: %+v", report.Avatars[1]) } } func TestBattleReportDoesNotReplaceStandardDamageWithSupplementalAggregate(t *testing.T) { stt := &goproto.BattleStatistics{ BattleAvatarList: []*goproto.AvatarBattleInfo{{Id: 1409, TotalDamage: 400}}, MFOJKJGKDGD: &goproto.FAHGFCIJHFK{ TotalDamage: 900, INBNCAKGDPJ: []*goproto.PMPCIBGILIE{{AvatarId: 1409, Damage: 900}}, }, } report := BattleReportFromStatistics(stt, 1, 1052106, "WIN") if report.TeamTotalDamage != 400 || report.Avatars[0].TotalDamage != 400 { t.Fatalf("supplemental data replaced standard damage: %+v", report) } } func TestCurrencyWarsBattleReportWithMoreThanFourAvatars(t *testing.T) { // Simulated CW battle: 4 front row avatars + 4 back row avatars = 8 avatars on board. stt := &goproto.BattleStatistics{ TotalBattleTurns: 15, RoundCnt: 1, CostTime: 30.0, BattleAvatarList: []*goproto.AvatarBattleInfo{ {Id: 1213, TotalDamage: 0}, {Id: 1308, TotalDamage: 0}, {Id: 1511, TotalDamage: 0}, {Id: 1513, TotalDamage: 0}, }, APAGMJNECCN: &goproto.MGMJOPHJCNL{ DOGMMGLBJIJ: []*goproto.CPNCAPLOIHI{ {RoleId: 121302, Damage: 100000}, {RoleId: 130802, Damage: 50000}, {RoleId: 151102, Damage: 10000}, {RoleId: 151302, Damage: 40000}, {RoleId: 604202, Damage: 20000}, {RoleId: 150302, Damage: 30000}, {RoleId: 800902, Damage: 15000}, {RoleId: 150502, Damage: 35000}, }, }, } report := BattleReportFromStatistics(stt, 101, 80211, "WIN") if !report.IsCurrencyWars { t.Fatal("expected IsCurrencyWars to be true") } if report.TeamTotalDamage != 300000 { t.Fatalf("expected team total damage 300000, got %f", report.TeamTotalDamage) } battleContext := &goproto.SceneBattleInfo{ BattleId: 101, StageId: 80211, BattleAvatarList: []*goproto.BattleAvatar{ {Id: 1213, Level: 80, Rank: 2, Index: 0}, {Id: 1308, Level: 80, Rank: 1, Index: 1}, {Id: 1511, Level: 80, Rank: 0, Index: 2}, {Id: 1513, Level: 80, Rank: 1, Index: 3}, }, EIGMKOJOEEI: &goproto.OJCBOMCCPDB{ OPHONCKPNMG: []*goproto.BattleAvatar{ {Id: 6042, Level: 80, Rank: 1, Index: 0}, {Id: 1503, Level: 80, Rank: 0, Index: 1}, {Id: 8009, Level: 80, Rank: 6, Index: 2}, {Id: 1505, Level: 80, Rank: 0, Index: 3}, }, GridGameRoleList: []*goproto.BJGJBNJDLIK{ {RoleId: 121302, AvatarId: 1213, Pos: 1, RoleStar: 3}, {RoleId: 130802, AvatarId: 1308, Pos: 2, RoleStar: 2}, {RoleId: 151102, AvatarId: 1511, Pos: 3, RoleStar: 1}, {RoleId: 151302, AvatarId: 1513, Pos: 4, RoleStar: 2}, {RoleId: 604202, AvatarId: 6042, Pos: 5, RoleStar: 1}, {RoleId: 150302, AvatarId: 1503, Pos: 6, RoleStar: 2}, {RoleId: 800902, AvatarId: 8009, Pos: 7, RoleStar: 2}, {RoleId: 150502, AvatarId: 1505, Pos: 8, RoleStar: 3}, }, }, } ApplyBattleReportContext(report, battleContext) if len(report.Avatars) != 8 { t.Fatalf("expected 8 avatars in CW battle report, got %d", len(report.Avatars)) } expectedDamages := map[uint32]float64{ 1213: 100000, 1308: 50000, 1511: 10000, 1513: 40000, 6042: 20000, 1503: 30000, 8009: 15000, 1505: 35000, } var totalPercent float64 for i, a := range report.Avatars { if a.Slot == nil || *a.Slot != uint32(i+1) { t.Fatalf("avatar %d slot expected %d, got %v", a.AvatarID, i+1, a.Slot) } expectedDmg := expectedDamages[a.AvatarID] if a.TotalDamage != expectedDmg { t.Fatalf("avatar %d damage expected %f, got %f", a.AvatarID, expectedDmg, a.TotalDamage) } totalPercent += a.DamagePercent } if totalPercent < 99.9 || totalPercent > 100.1 { t.Fatalf("expected total percent ~100%%, got %f", totalPercent) } // Verify star rank if report.Avatars[0].AvatarRank != 3 { t.Fatalf("expected Dan Heng IL rank (star) to be 3, got %d", report.Avatars[0].AvatarRank) } } func TestRealCapturedBattleResult(t *testing.T) { path := "../../battle_diagnostics/20261031T050254.858914500Z_result_4462dff22ebf/battle_result.bin" wire, err := os.ReadFile(path) if err != nil { t.Skip("real captured battle result not found on disk") } var req goproto.PVEBattleResultCsReq if err := proto.Unmarshal(wire, &req); err != nil { t.Fatal(err) } report := BattleReportFromStatistics(req.Stt, req.BattleId, req.StageId, "WIN") if !report.IsCurrencyWars { t.Fatal("expected IsCurrencyWars = true") } if len(report.CurrencyWarsRoleDamages) != 9 { t.Fatalf("expected 9 role damages, got %d", len(report.CurrencyWarsRoleDamages)) } if report.TeamTotalDamage < 115000000 { t.Fatalf("expected team total damage > 115M, got %f", report.TeamTotalDamage) } battleWire, err := os.ReadFile("../../battle_diagnostics/20261031T050532.034020800Z_86babfb031c1/battle_last.bin") if err == nil { var battle goproto.SceneBattleInfo if err := proto.Unmarshal(battleWire, &battle); err == nil { ApplyBattleReportContext(report, &battle) if len(report.Avatars) != 9 { t.Fatalf("expected 9 avatars after applying CW battle context, got %d", len(report.Avatars)) } // Write to battle_report.json in root so current dashboard reflects this real battle _ = SaveBattleReport(report) } } }