package common import ( "encoding/json" "gac-hkrpg-go/internal/testutil" "os" "path/filepath" "runtime" "testing" ) func chdirRepoRoot(t *testing.T) { t.Helper() _, source, _, ok := runtime.Caller(0) if !ok { t.Fatal("cannot locate test helper source") } root := filepath.Clean(filepath.Join(filepath.Dir(source), "..", "..")) if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil { t.Fatalf("cannot locate repository root from %s: %v", source, err) } temporary := t.TempDir() if err := testutil.CopyRuntime(root, temporary); err != nil { t.Fatal(err) } t.Chdir(temporary) } func TestBannersConfigLoadAndVisibility(t *testing.T) { chdirRepoRoot(t) if err := LoadBannersConfig(); err != nil { t.Fatalf("failed to load banners config: %v", err) } mode0 := GetVisibleBanners(0) if len(mode0) < 13 { t.Errorf("expected at least 13 visible banners for mode 0, got %d", len(mode0)) } mode1 := GetVisibleBanners(1) if len(mode1) < len(mode0) { t.Errorf("expected mode 1 >= mode 0, got mode1=%d mode0=%d", len(mode1), len(mode0)) } // Verify no duplicate IDs in mode 1 seen := make(map[uint32]bool) for _, b := range mode1 { if seen[b.ID] { t.Fatalf("duplicate banner ID %d found in mode 1", b.ID) } seen[b.ID] = true } // Check mode 2 (character only) mode2 := GetVisibleBanners(2) if len(mode2) == 0 || len(mode2) >= len(mode1) { t.Errorf("unexpected banner count for mode 2: %d", len(mode2)) } // Check mode 3 (lightcone only) mode3 := GetVisibleBanners(3) if len(mode3) == 0 || len(mode3) >= len(mode1) { t.Errorf("unexpected banner count for mode 3: %d", len(mode3)) } } func TestBannerProtoSerialization(t *testing.T) { chdirRepoRoot(t) if err := LoadBannersConfig(); err != nil { t.Fatal(err) } // Check banner 1001 (Standard) b1001 := FindGachaBanner(1001, 1) if b1001 == nil { t.Fatal("banner 1001 not found") } proto1001 := b1001.ToProtoInfo() if proto1001.GachaId != 1001 { t.Fatalf("expected gacha_id 1001, got %d", proto1001.GachaId) } if proto1001.GachaCeiling == nil || !proto1001.GachaCeiling.IsClaimed || len(proto1001.GachaCeiling.AvatarList) != 7 { t.Fatalf("expected claimed gacha ceiling with 7 avatars, got %+v", proto1001.GachaCeiling) } // Check banner 2120 (AvatarUp) b2120 := FindGachaBanner(2120, 1) if b2120 == nil { t.Fatal("banner 2120 not found") } proto2120 := b2120.ToProtoInfo() if proto2120.GachaId != 2120 { t.Fatalf("expected gacha_id 2120, got %d", proto2120.GachaId) } if len(proto2120.ONIHOMCELJK) == 0 || proto2120.ONIHOMCELJK[0] != 1505 { t.Fatalf("expected featured 5-star 1505 in wire #5, got %+v", proto2120.ONIHOMCELJK) } if len(proto2120.ItemDetailList) != 1 || proto2120.ItemDetailList[0] != 11 { t.Fatalf("expected AvatarUp marker 11 in ItemDetailList, got %+v", proto2120.ItemDetailList) } if len(proto2120.PrizeItemList) == 0 { t.Fatal("expected PrizeItemList to contain rateUp items") } } func TestPityProgression(t *testing.T) { var state GachaPityState if FiveStarChance90(1) != 0.006 { t.Fatalf("expected base 5-star chance 0.006, got %f", FiveStarChance90(1)) } if FiveStarChance90(76) != 0.324 { t.Fatalf("expected soft pity 5-star chance 0.324, got %f", FiveStarChance90(76)) } if FiveStarChance90(90) != 1.0 { t.Fatalf("expected hard pity 5-star chance 1.0, got %f", FiveStarChance90(90)) } // Test pity state transitions state.RecordThreeStar() if state.PullsSinceFiveStar != 1 || state.PullsSinceFourStarOrHigher != 1 || state.TotalPulls != 1 { t.Fatalf("unexpected state after 3-star: %+v", state) } state.RecordFourStar() if state.PullsSinceFiveStar != 2 || state.PullsSinceFourStarOrHigher != 0 || state.TotalPulls != 2 { t.Fatalf("unexpected state after 4-star: %+v", state) } state.RecordFiveStar() if state.PullsSinceFiveStar != 0 || state.PullsSinceFourStarOrHigher != 0 || state.TotalPulls != 3 { t.Fatalf("unexpected state after 5-star: %+v", state) } } func TestGachaRollExecution(t *testing.T) { chdirRepoRoot(t) if err := LoadBannersConfig(); err != nil { t.Fatal(err) } player, err := LoadFreesrData() if err != nil { t.Fatal(err) } banner := FindGachaBanner(2120, 1) if banner == nil { t.Fatal("banner 2120 not found") } res, err := PerformGachaRolls(banner, 10, player) if err != nil { t.Fatalf("PerformGachaRolls failed: %v", err) } if len(res.Items) != 10 { t.Fatalf("expected 10 items, got %d", len(res.Items)) } // Verify persistence in freesr-data.json raw, err := os.ReadFile("freesr-data.json") if err != nil { t.Fatal(err) } var doc map[string]json.RawMessage if err := json.Unmarshal(raw, &doc); err != nil { t.Fatal(err) } if _, ok := doc["gacha_pity"]; !ok { t.Fatal("missing gacha_pity in freesr-data.json") } }