package gamedata import ( "os" "testing" "bd2server/internal/server/wire" ) func TestPermanentEquipmentGroupRequiresPityAndPoints(t *testing.T) { // Content-ticket groups have both products too; those alone do not make // a group an ordinary permanent equipment draw. content := wire.AppendVarint(nil, 17, 2) content = wire.AppendVarint(content, 24, 70100001) content = wire.AppendVarint(content, 33, 71000001) if isPermanentEquipmentGachaGroup(content) { t.Fatal("content-ticket group classified as ordinary equipment") } ordinary := wire.AppendVarint(content, 10, 1) ordinary = wire.AppendVarint(ordinary, 27, 1) if !isPermanentEquipmentGachaGroup(ordinary) { t.Fatal("permanent equipment group with shared pity and points omitted") } if isPermanentEquipmentGachaGroup(wire.AppendVarint(ordinary, 16, 1)) { t.Fatal("equipment selection group classified as ordinary equipment") } } func TestSingleSidedCostumeFixedThresholdsDoNotTriggerZeroSide(t *testing.T) { pool := []WeightedCostume{ {Weight: 150, ID: 5001}, {Weight: 150, ID: 5002}, {Weight: 1400, ID: 4001}, {Weight: 8300, ID: 3001}, } gacha := RegularGacha{Grades: map[uint64]uint64{5001: 5, 5002: 5, 4001: 4, 3001: 3, 21201: 5}, ID: 1, Count: 1, PriceType: 3, Price: 200, Pool: pool} chooseThree := func(limit uint64) (uint64, error) { if limit == officialRateScale { return limit - 1, nil } return 0, nil } roll, state, err := gacha.rollWithCostumeFixed(0, 0, GachaFixedDesign{ID: 3, CostumeGrade5Count: 10}, nil, nil, chooseThree) if err != nil || roll[0] != 3001 || state.CostumeGrade4Sort != -1 || state.CostumeGrade5Sort != -1 { t.Fatalf("five-only fixed roll=%v state=%+v err=%v", roll, state, err) } roll, state, err = gacha.rollWithCostumeFixed(9, 500, GachaFixedDesign{ID: 5, CostumeGrade4Count: 10, ResetOnMatchingGrade: true}, nil, nil, chooseThree) if err != nil || roll[0] != 4001 || state.CostumeGrade4Sort != 0 || state.CostumeGrade5Sort != -1 { t.Fatalf("four-only fixed roll=%v state=%+v err=%v", roll, state, err) } } func TestCompositeCostumeRewardGroupExecutesAllChildren(t *testing.T) { program := &CostumeRewardGroup{ID: 1, DropCount: 1, DropType: 1, Entries: []CostumeRewardEntry{ {ItemType: 11, ItemID: 5001, Count: 1, Weight: 1}, {ItemType: 9, ItemID: 2, Count: 1, Weight: 1, Group: &CostumeRewardGroup{ID: 2, DropCount: 9, Entries: []CostumeRewardEntry{ {ItemType: 11, ItemID: 4001, Count: 1, Weight: 1400}, {ItemType: 11, ItemID: 3001, Count: 1, Weight: 8300}, }}}, }} if count, err := costumeRewardCount(program); err != nil || count != 10 { t.Fatalf("count=%d err=%v", count, err) } gacha := RegularGacha{Grades: map[uint64]uint64{5001: 5, 5002: 5, 4001: 4, 3001: 3, 21201: 5}, ID: 1, Count: 10, PriceType: 2, Price: 500, RewardGroup: program} roll, err := gacha.rollRewardGroup(func(limit uint64) (uint64, error) { return 0, nil }) if err != nil || len(roll) != 10 || roll[0] != 5001 { t.Fatalf("roll=%v err=%v", roll, err) } for i := 1; i < len(roll); i++ { if roll[i] != 4001 { t.Fatalf("roll[%d]=%d", i, roll[i]) } } } func TestMoonriseSpecialSelectionUsesThreeChoicesAndGameDataRemainder(t *testing.T) { choices := make([]CostumeRewardEntry, 12) selected := make([]uint64, 12) for i := range choices { selected[i] = uint64(5001 + i) choices[i] = CostumeRewardEntry{ItemType: 11, ItemID: selected[i], Count: 1, Weight: 1} } gacha := RegularGacha{Grades: map[uint64]uint64{5001: 5, 5002: 5, 4001: 4, 3001: 3, 21201: 5}, ID: 9100037, Count: 10, PriceType: 19, PriceID: 450030, Price: 1, RewardGroup: &CostumeRewardGroup{ ID: 9100037, DropCount: 1, DropType: 1, Entries: []CostumeRewardEntry{ {ItemType: 9, ItemID: 9100038, Count: 1, Weight: 1, Group: &CostumeRewardGroup{ID: 9100038, DropCount: 3, Entries: choices}}, {ItemType: 9, ItemID: 9100039, Count: 1, Weight: 1, Group: &CostumeRewardGroup{ID: 9100039, DropCount: 7, Entries: []CostumeRewardEntry{ {ItemType: 11, ItemID: 4001, Count: 1, Weight: 1443}, {ItemType: 11, ItemID: 3001, Count: 1, Weight: 8557}, }}}, }, }} draws := []uint64{0, 1, 2, 0, 9999, 0, 9999, 0, 9999, 0} roll, err := gacha.rollSpecialSelection(selected, func(limit uint64) (uint64, error) { value := draws[0] draws = draws[1:] return value % limit, nil }) if err != nil { t.Fatal(err) } if len(roll) != 10 || roll[0] != selected[0] || roll[1] != selected[1] || roll[2] != selected[2] { t.Fatalf("selection results=%v", roll) } for _, id := range roll[3:] { if id != 4001 && id != 3001 { t.Fatalf("remainder contains non-GameData costume %d: %v", id, roll) } } } func TestAddAndQueryStepUpDesign(t *testing.T) { characters := map[uint64]CharacterDesign{5001: {ID: 500, HP: 100}} catalog, err := fixtureRegularCatalog(map[uint64]RegularGacha{ 10: {ID: 10, Count: 1, PriceType: 2, Price: 1, Pool: []WeightedCostume{{ID: 5001, Weight: 1}}}, }, characters) if err != nil { t.Fatal(err) } design := GachaStepUpDesign{ID: 30, Steps: []GachaStepDesign{{Step: 1, GroupID: 20122, GachaID: 10, FixedID: 5, IsDisplayFixedItem: 1}}} if err := catalog.AddStepUpDesign(design); err != nil { t.Fatal(err) } step, ok := catalog.StepForGacha(10) if !ok || step.GroupID != 20122 || step.Step != 1 { t.Fatalf("step=%+v ok=%v", step, ok) } group, ok := catalog.StepUp(30) if !ok || len(group.Steps) != 1 || len(catalog.StepUps()) != 1 { t.Fatalf("group=%+v ok=%v", group, ok) } } func TestActiveGachaAgainstInstalledVersion23510(t *testing.T) { root := os.Getenv("BD2_TEST_GAMEDATA_ROOT") if root == "" { t.Skip("set BD2_TEST_GAMEDATA_ROOT for installed GameData integration test") } regular, equipment, err := LoadActiveGachaForSchedules(root, "20260923193640", []uint64{166, 30010, 30011, 206, 205, 208, 153, 72, 71, 207, 1009}, []uint64{29, 30}) if err != nil { t.Fatal(err) } for _, id := range []uint64{101, 10100113, 11000113, 10100145, 11000145, 10100040, 11000040, 10100146, 11000146, 31, 9100037, 8100118, 8100119, 8100120, 8100121, 8100122, 8100123, 8100124, 8100125} { if _, ok := regular.Gacha(id); !ok { t.Errorf("costume gacha %d missing", id) } } paid, ok := regular.Gacha(9100037) if !ok || paid.PriceType != 19 || paid.PriceID != 450030 || paid.Price != 1 || paid.RewardGroup == nil { t.Fatalf("paid selection gacha=%+v ok=%v", paid, ok) } if got := regular.SpecialSelectionCount(9100037); got != 3 || len(regular.SpecialSelectionIDs(9100037)) != 129 { t.Fatalf("paid selection guaranteed=%d eligible=%d", got, len(regular.SpecialSelectionIDs(9100037))) } one, ok := regular.Gacha(10100113) if !ok || one.Count != 1 || one.FreeCountDay != 1 || one.DailyPayGachaCount != 1 || one.DailyPayGachaPriceCount != 90 || one.Price != 200 { t.Fatalf("daily single gacha=%+v ok=%v", one, ok) } moonriseGroup, ok := regular.Group(30011) if !ok || moonriseGroup.BuyLimitCount != 10 || moonriseGroup.CashProductGroupID != 1500001 || moonriseGroup.CashProductID != 9100037 || moonriseGroup.SelectCount != 12 || moonriseGroup.SelectionChoiceRate != 100 { t.Fatalf("moonrise group=%+v ok=%v", moonriseGroup, ok) } permanentSelection, ok := regular.Group(10001) if !ok || permanentSelection.TenTimeGachaID != 101 || permanentSelection.SelectCount != 12 || permanentSelection.GachaSubType != 1 { t.Fatalf("permanent selection group=%+v ok=%v", permanentSelection, ok) } newbie, ok := regular.Group(1009) if !ok || newbie.TenTimeGachaID != 31 || newbie.BuyLimitCount != 30 || newbie.SelectCount != 3 || newbie.PointCount != 0 || newbie.FixedID != 3 || !newbie.UseSelectionOnlyFixedApply { t.Fatalf("newbie group=%+v ok=%v", newbie, ok) } for _, id := range []uint64{8100121, 8100125} { gacha, ok := regular.Gacha(id) if !ok || gacha.FixedCostumeID == 0 || gacha.RewardGroup == nil || len(gacha.Pool) != 3 { t.Fatalf("fixed step gacha %d=%+v ok=%v", id, gacha, ok) } } for _, fixedID := range []uint64{1, 3, 5} { if _, ok := regular.Fixed(fixedID); !ok { t.Errorf("fixed %d missing", fixedID) } } for _, groupID := range []uint64{29, 30} { stepUp, ok := regular.StepUp(groupID) if !ok || len(stepUp.Steps) != 4 { t.Errorf("step-up %d=%+v ok=%v", groupID, stepUp, ok) } } for _, id := range []uint64{200, 201, 20100082, 21000082, 20100034, 21000034, 20100070, 21000070, 20100083, 21000083} { if _, ok := equipment.Gacha(id); !ok { t.Errorf("equipment gacha %d missing", id) } } permanentEquipment, ok := equipment.Group(10002) if !ok || permanentEquipment.OneTimeGachaID != 200 || permanentEquipment.TenTimeGachaID != 201 || permanentEquipment.FixedID != 1 || permanentEquipment.PointCount != 1 { t.Fatalf("permanent equipment group=%+v ok=%v", permanentEquipment, ok) } for _, id := range []uint64{200, 201} { product, _ := equipment.Gacha(id) if product.TicketOnly || product.PriceType != 3 || product.Price != uint64(product.Count)*200 || len(product.TicketIDs) != 1 || product.TicketIDs[0] != 1000 || len(product.Pool) != 8 { t.Fatalf("permanent equipment product %d=%+v", id, product) } } for _, groupID := range []uint64{1003, 1004, 1005, 1006} { if _, ok := equipment.Group(groupID); ok { t.Errorf("content-ticket equipment group %d became ordinary permanent draw", groupID) } } } func TestLegacyRegularGachaLoaderAgainstInstalledVersion23510(t *testing.T) { root := os.Getenv("BD2_TEST_GAMEDATA_ROOT") if root == "" { t.Skip("set BD2_TEST_GAMEDATA_ROOT for installed GameData integration test") } catalog, err := LoadRegularCostumeGachaGroups(root, "20260923193640", []uint64{10001, 135, 205, 121}, []uint64{29}) if err != nil { t.Fatal(err) } for _, id := range []uint64{100, 101, 10100084, 11000084, 10100145, 11000145, 10100072, 11000072, 8100118, 8100119, 8100120, 8100121} { if _, ok := catalog.Gacha(id); !ok { t.Errorf("legacy gacha %d missing", id) } } }