2026-10-17 13:08:13 +07:00
|
|
|
package common
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
goproto "gac-hkrpg-go/protocol/proto"
|
2026-10-31 20:47:36 +07:00
|
|
|
"google.golang.org/protobuf/proto"
|
2026-10-17 13:08:13 +07:00
|
|
|
)
|
|
|
|
|
|
2026-10-21 20:40:13 +07:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-17 13:08:13 +07:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-18 22:40:03 +07:00
|
|
|
|
|
|
|
|
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])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 20:40:13 +07:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 22:40:03 +07:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-21 20:40:13 +07:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-31 20:47:36 +07:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|