- 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>
257 lines
6.4 KiB
Go
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]
|
|
}
|