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>
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user