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.
75 lines
2.2 KiB
Go
75 lines
2.2 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"errors"
|
|
"math/big"
|
|
)
|
|
|
|
// Local pity policy: no separate official equipment pity pool is available.
|
|
// gradePool conditions the actual nested weighted distribution on one grade.
|
|
// Exact rational path weights preserve branches with different child totals.
|
|
func (g EquipmentGacha) gradePool(grade uint64) ([]WeightedEquipment, error) {
|
|
weights := map[uint64]*big.Rat{}
|
|
var walk func([]WeightedEquipment, *big.Rat) error
|
|
walk = func(pool []WeightedEquipment, path *big.Rat) error {
|
|
total := new(big.Int)
|
|
for _, item := range pool {
|
|
if item.Weight == 0 {
|
|
return errors.New("gamedata: zero equipment weight")
|
|
}
|
|
total.Add(total, new(big.Int).SetUint64(item.Weight))
|
|
}
|
|
if total.Sign() == 0 {
|
|
return errors.New("gamedata: empty equipment pool")
|
|
}
|
|
for _, item := range pool {
|
|
prob := new(big.Rat).Mul(path, new(big.Rat).SetFrac(new(big.Int).SetUint64(item.Weight), total))
|
|
if item.ID == 0 {
|
|
if err := walk(item.Children, prob); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
actual, ok := g.Grades[item.ID]
|
|
if !ok || actual == 0 || actual > 4 {
|
|
return errors.New("gamedata: equipment grade unavailable")
|
|
}
|
|
if actual == grade {
|
|
if weights[item.ID] == nil {
|
|
weights[item.ID] = new(big.Rat)
|
|
}
|
|
weights[item.ID].Add(weights[item.ID], prob)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := walk(g.Pool, new(big.Rat).SetInt64(1)); err != nil {
|
|
return nil, err
|
|
}
|
|
denom := big.NewInt(1)
|
|
for _, weight := range weights {
|
|
gcd := new(big.Int).GCD(nil, nil, denom, weight.Denom())
|
|
denom.Mul(new(big.Int).Quo(denom, gcd), weight.Denom())
|
|
}
|
|
var ids []uint64
|
|
for id := range weights {
|
|
ids = append(ids, id)
|
|
}
|
|
// Stable table-independent ordering makes deterministic draw sources reproducible.
|
|
for i := 1; i < len(ids); i++ {
|
|
for j := i; j > 0 && ids[j] < ids[j-1]; j-- {
|
|
ids[j], ids[j-1] = ids[j-1], ids[j]
|
|
}
|
|
}
|
|
var out []WeightedEquipment
|
|
for _, id := range ids {
|
|
w := weights[id]
|
|
count := new(big.Int).Mul(w.Num(), new(big.Int).Quo(new(big.Int).Set(denom), w.Denom()))
|
|
if !count.IsUint64() {
|
|
return nil, errors.New("gamedata: conditioned equipment weight overflow")
|
|
}
|
|
out = append(out, WeightedEquipment{ID: id, Weight: count.Uint64()})
|
|
}
|
|
return out, nil
|
|
}
|