Files
bd2/go/internal/server/gamedata/gacha_active_test.go
T
Flechazo 5b5a219dc2 feat(server): add special loot and data-driven gameplay rules
Generate equipment loot atomically, grant random boxes and prevent repeat claims.
Read gacha pools, pity, products, tickets, missions, presets, chapter rewards
and inventory costs from GameData; extend both collaboration UR ticket pools.
Sync owned recipes, configure startup state and calendars in versioned seeds,
and validate unlock tickets and revival talents.
2026-10-04 19:15:59 +08:00

223 lines
9.6 KiB
Go

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)
}
}
}