Files
YumeMichi 750c2cf3d0 Implement secretbox handlers and align gacha behavior
- Add secretbox API schemas, handlers, and routes for all/showDetail/pon/multi
- Load secretbox server data through the shared serverdata loader
- Fix rarity mapping and pool rules for SR/SSR/UR special boxes
- Implement thanks festival pool behavior, including UR guarantee and SSR/UR-only rates
- Correct animation-related fields such as is_hit and lowest_rarity, and add rare UR promotion animation

Signed-off-by: Sean Du <do4suki@gmail.com>
2026-06-05 21:57:04 +08:00

257 lines
6.4 KiB
Go

package secretbox
import (
"fmt"
secretboxapischema "honoka-chan/internal/schema/api/secretbox"
secretboxschema "honoka-chan/internal/schema/secretbox"
"honoka-chan/internal/session"
"math/rand"
"slices"
)
const (
rarityN = 1
rarityR = 2
raritySR = 3
rarityUR = 4
raritySSR = 5
urPromotionAnimationRate = 10
)
type mainQuery interface {
findUnitIDs(memberCategory int, supportOnly bool, rarity int) ([]int, error)
}
type sessionMainQuery struct {
ss *session.Session
}
type sessionlessMainQuery struct{}
func (q sessionMainQuery) findUnitIDs(memberCategory int, supportOnly bool, rarity int) ([]int, error) {
return queryUnitIDs(q.ss, memberCategory, supportOnly, rarity)
}
func (sessionlessMainQuery) findUnitIDs(memberCategory int, supportOnly bool, rarity int) ([]int, error) {
return queryUnitIDs(nil, memberCategory, supportOnly, rarity)
}
type poolRule struct {
memberCategory int
supportOnly bool
allowedRarities []int
}
func buildUnitLineUp(memberCategory int, page secretboxapischema.SecretBoxPage, button secretboxapischema.SecretBoxButton) []secretboxschema.UnitLineUp {
rarities := []int{rarityUR, raritySSR, raritySR, rarityR, rarityN}
result := make([]secretboxschema.UnitLineUp, 0, len(rarities))
for _, rarity := range rarities {
unitIDs, err := findUnitIDsByRule(sessionlessMainQuery{}, memberCategory, page, button, rarity)
if err != nil || len(unitIDs) == 0 {
continue
}
result = append(result, secretboxschema.UnitLineUp{
Rarity: rarity,
UnitIDs: unitIDs,
})
}
return result
}
func findUnitIDsByRule(q mainQuery, memberCategory int, page secretboxapischema.SecretBoxPage, button secretboxapischema.SecretBoxButton, rarity int) ([]int, error) {
rule := buildPoolRule(memberCategory, page, button)
if len(rule.allowedRarities) > 0 && !slices.Contains(rule.allowedRarities, rarity) {
return []int{}, nil
}
return q.findUnitIDs(rule.memberCategory, rule.supportOnly, rarity)
}
func randomUnitID(ss *session.Session, rng *rand.Rand, memberCategory int, page secretboxapischema.SecretBoxPage, button secretboxapischema.SecretBoxButton, rarity int) (int, error) {
unitIDs, err := findUnitIDsByRule(sessionMainQuery{ss: ss}, memberCategory, page, button, rarity)
if err != nil {
return 0, err
}
if len(unitIDs) == 0 {
return 0, fmt.Errorf("no units available for secretbox=%d rarity=%d", page.SecretBoxInfo.SecretBoxID, rarity)
}
return unitIDs[rng.Intn(len(unitIDs))], nil
}
func buildPoolRule(memberCategory int, page secretboxapischema.SecretBoxPage, button secretboxapischema.SecretBoxButton) poolRule {
rule := poolRule{
memberCategory: memberCategory,
allowedRarities: []int{rarityR, raritySR, raritySSR, rarityUR},
}
if isThanksFestivalBox(page) {
rule.allowedRarities = []int{raritySSR, rarityUR}
}
switch page.SecretBoxInfo.SecretBoxID {
case 1, 61:
rule.memberCategory = 0
rule.allowedRarities = []int{rarityN}
return rule
case 23:
rule.memberCategory = 1
rule.supportOnly = true
rule.allowedRarities = []int{rarityN, rarityR, raritySR, rarityUR, raritySSR}
return rule
}
switch button.SecretBoxButtonType {
case 6:
rule.memberCategory = 0
rule.allowedRarities = []int{rarityN}
case 9:
rule.allowedRarities = []int{raritySR, rarityUR}
case 10:
rule.allowedRarities = []int{raritySSR, rarityUR}
case 11:
rule.allowedRarities = []int{rarityUR}
case 14, 15:
rule.allowedRarities = []int{raritySR, raritySSR, rarityUR}
}
return rule
}
func isThanksFestivalBox(page secretboxapischema.SecretBoxPage) bool {
switch page.SecretBoxInfo.SecretBoxID {
case 1718, 1719, 1720, 1721:
return true
default:
return false
}
}
func pickRarity(rng *rand.Rand, page secretboxapischema.SecretBoxPage, button secretboxapischema.SecretBoxButton, guaranteed bool) int {
rule := buildPoolRule(0, page, button)
if len(rule.allowedRarities) == 1 {
return rule.allowedRarities[0]
}
weights := map[int]int{
rarityR: 70,
raritySR: 20,
raritySSR: 7,
rarityUR: 3,
}
if isThanksFestivalBox(page) {
weights = map[int]int{
raritySSR: 70,
rarityUR: 30,
}
}
switch button.SecretBoxButtonType {
case 9:
weights = map[int]int{raritySR: 80, rarityUR: 20}
case 10:
weights = map[int]int{raritySSR: 80, rarityUR: 20}
case 11:
return rarityUR
case 14, 15:
weights = map[int]int{raritySR: 80, raritySSR: 15, rarityUR: 5}
}
if guaranteed {
return pickGuaranteedRarity(rng, page, button)
}
return weightedPick(rng, rule.allowedRarities, weights)
}
func pickGuaranteedRarity(rng *rand.Rand, page secretboxapischema.SecretBoxPage, button secretboxapischema.SecretBoxButton) int {
rule := buildPoolRule(0, page, button)
weights := map[int]int{raritySR: 20, raritySSR: 7, rarityUR: 3}
return weightedPick(rng, rule.allowedRarities, weights)
}
func hasGuaranteedUR(button secretboxapischema.SecretBoxButton, cost secretboxapischema.SecretBoxCost) bool {
if cost.UnitCount <= 1 {
return false
}
switch button.SecretBoxButtonType {
case 2, 16:
return true
default:
return false
}
}
func satisfiesGuaranteedRarity(rarity int, guaranteedRarity int) bool {
switch guaranteedRarity {
case rarityUR:
return rarity == rarityUR
case raritySR:
return rarity == raritySR || rarity == raritySSR || rarity == rarityUR
default:
return rarity == guaranteedRarity
}
}
func rarityQualityRank(rarity int) int {
switch rarity {
case rarityN:
return 1
case rarityR:
return 2
case raritySR:
return 3
case raritySSR:
return 4
case rarityUR:
return 5
default:
return 0
}
}
func isLowerQualityRarity(left int, right int) bool {
return rarityQualityRank(left) < rarityQualityRank(right)
}
func shouldShowDirectURHit(rng *rand.Rand, page secretboxapischema.SecretBoxPage, button secretboxapischema.SecretBoxButton, rarity int) bool {
if rarity != rarityUR {
return false
}
rule := buildPoolRule(0, page, button)
for _, allowedRarity := range rule.allowedRarities {
if allowedRarity != rarityUR {
return rng.Intn(100) >= urPromotionAnimationRate
}
}
return true
}
func weightedPick(rng *rand.Rand, allowed []int, weights map[int]int) int {
filtered := make([]int, 0, len(allowed))
total := 0
for _, rarity := range allowed {
weight := weights[rarity]
if weight <= 0 {
continue
}
filtered = append(filtered, rarity)
total += weight
}
if len(filtered) == 0 {
return allowed[len(allowed)-1]
}
n := rng.Intn(total) + 1
acc := 0
for _, rarity := range filtered {
acc += weights[rarity]
if n <= acc {
return rarity
}
}
return filtered[len(filtered)-1]
}