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:
2026-06-05 21:57:04 +08:00
parent acd9be627e
commit 750c2cf3d0
20 changed files with 2558 additions and 576 deletions
+3
View File
@@ -24,6 +24,7 @@ import (
"honoka-chan/internal/handler/api/profile"
"honoka-chan/internal/handler/api/reward"
"honoka-chan/internal/handler/api/scenario"
"honoka-chan/internal/handler/api/secretbox"
"honoka-chan/internal/handler/api/stamp"
"honoka-chan/internal/handler/api/subscenario"
"honoka-chan/internal/handler/api/unit"
@@ -96,6 +97,8 @@ func api(ctx *gin.Context) {
result, err = reward.RewardApi(v.Action)
case "scenario":
result, err = scenario.ScenarioApi(ctx, v.Action)
case "secretbox":
result, err = secretbox.SecretBoxApi(v.Action)
case "stamp":
result, err = stamp.StampApi(v.Action)
case "subscenario":
+32 -36
View File
@@ -11,48 +11,44 @@ func bannerList() (res any, err error) {
TimeLimit: "2037-12-31 23:59:59",
BannerList: []bannerapischema.BannerList{
{
BannerType: 1,
TargetID: 1743,
AssetPath: "assets/image/secretbox/icon/s_ba_1718_1.png",
FixedFlag: false,
BackSide: false,
BannerID: 101151,
StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59",
AddUnitStartDate: "2022-01-01 00:00:00",
BannerType: 1,
TargetID: 1718,
AssetPath: "assets/image/secretbox/icon/s_ba_1718_1.png",
FixedFlag: false,
BackSide: false,
BannerID: 101151,
StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59",
},
{
BannerType: 1,
TargetID: 1741,
AssetPath: "assets/image/secretbox/icon/s_ba_1719_1.png",
FixedFlag: false,
BackSide: false,
BannerID: 101150,
StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59",
AddUnitStartDate: "2022-01-01 00:00:00",
BannerType: 1,
TargetID: 1719,
AssetPath: "assets/image/secretbox/icon/s_ba_1719_1.png",
FixedFlag: false,
BackSide: false,
BannerID: 101150,
StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59",
},
{
BannerType: 1,
TargetID: 1740,
AssetPath: "assets/image/secretbox/icon/s_ba_1720_1.png",
FixedFlag: false,
BackSide: false,
BannerID: 101149,
StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59",
AddUnitStartDate: "2022-01-01 00:00:00",
BannerType: 1,
TargetID: 1720,
AssetPath: "assets/image/secretbox/icon/s_ba_1720_1.png",
FixedFlag: false,
BackSide: false,
BannerID: 101149,
StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59",
},
{
BannerType: 1,
TargetID: 1739,
AssetPath: "assets/image/secretbox/icon/s_ba_1721_1.png",
FixedFlag: false,
BackSide: false,
BannerID: 101144,
StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59",
AddUnitStartDate: "2022-01-01 00:00:00",
BannerType: 1,
TargetID: 1721,
AssetPath: "assets/image/secretbox/icon/s_ba_1721_1.png",
FixedFlag: false,
BackSide: false,
BannerID: 101144,
StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59",
},
{
BannerType: 2,
+44
View File
@@ -0,0 +1,44 @@
package secretbox
import (
secretboxapischema "honoka-chan/internal/schema/api/secretbox"
honokautils "honoka-chan/internal/utils"
"time"
)
func all() (res any, err error) {
secretBoxData, err := honokautils.LoadServerData[secretboxapischema.AllData]("secretbox_data.json")
if err != nil {
return nil, err
}
secretBoxData.UseCache = 0
secretBoxData.IsUnitMax = false
secretBoxData.GaugeInfo.MaxGaugePoint = 100
secretBoxData.GaugeInfo.GaugePoint = 0
for i := range secretBoxData.ItemList {
if secretBoxData.ItemList[i].Amount < 999 {
secretBoxData.ItemList[i].Amount = 999
}
}
for i := range secretBoxData.MemberCategoryList {
for j := range secretBoxData.MemberCategoryList[i].PageList {
for k := range secretBoxData.MemberCategoryList[i].PageList[j].ButtonList {
for m := range secretBoxData.MemberCategoryList[i].PageList[j].ButtonList[k].CostList {
secretBoxData.MemberCategoryList[i].PageList[j].ButtonList[k].CostList[m].Payable = true
}
}
}
}
res = secretboxapischema.AllResp{
Result: secretBoxData,
Status: 200,
CommandNum: false,
TimeStamp: time.Now().Unix(),
}
return res, nil
}
@@ -0,0 +1,15 @@
package secretbox
import (
"fmt"
)
func SecretBoxApi(action string) (res any, err error) {
switch action {
case "all":
res, err = all()
default:
err = fmt.Errorf("unimplemented action: secretbox: %s", action)
}
return res, err
}
+26
View File
@@ -0,0 +1,26 @@
package banner
import "reflect"
func extractResultField(data any) any {
if data == nil {
return nil
}
value := reflect.ValueOf(data)
if value.Kind() == reflect.Pointer {
if value.IsNil() {
return nil
}
value = value.Elem()
}
if value.Kind() != reflect.Struct {
return data
}
field := value.FieldByName("Result")
if !field.IsValid() || !field.CanInterface() {
return data
}
return field.Interface()
}
+51
View File
@@ -0,0 +1,51 @@
package banner
import (
apibanner "honoka-chan/internal/handler/api/banner"
"honoka-chan/internal/middleware"
"honoka-chan/internal/router"
"honoka-chan/internal/session"
"github.com/gin-gonic/gin"
)
type bannerResp struct {
ResponseData any `json:"response_data"`
ReleaseInfo []any `json:"release_info"`
StatusCode int `json:"status_code"`
}
func list(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
data, err := apibanner.BannerApi("bannerList")
if ss.CheckErr(err) {
return
}
resp, ok := data.(interface {
GetResult() any
})
_ = resp
_ = ok
ss.Respond(bannerResp{
ResponseData: extractBannerResult(data),
ReleaseInfo: []any{},
StatusCode: 200,
})
}
func extractBannerResult(data any) any {
if v, ok := data.(map[string]any); ok {
if result, ok := v["result"]; ok {
return result
}
}
return extractResultField(data)
}
func init() {
router.AddHandler("main.php", "POST", "/banner/bannerList", middleware.Common, list)
}
+2
View File
@@ -6,6 +6,7 @@ import (
_ "honoka-chan/internal/handler/api"
_ "honoka-chan/internal/handler/award"
_ "honoka-chan/internal/handler/background"
_ "honoka-chan/internal/handler/banner"
_ "honoka-chan/internal/handler/download"
_ "honoka-chan/internal/handler/event"
_ "honoka-chan/internal/handler/eventscenario"
@@ -25,6 +26,7 @@ import (
_ "honoka-chan/internal/handler/profile"
_ "honoka-chan/internal/handler/reward"
_ "honoka-chan/internal/handler/scenario"
_ "honoka-chan/internal/handler/secretbox"
_ "honoka-chan/internal/handler/subscenario"
_ "honoka-chan/internal/handler/tos"
_ "honoka-chan/internal/handler/unit"
+37
View File
@@ -0,0 +1,37 @@
package secretbox
import (
"honoka-chan/internal/middleware"
"honoka-chan/internal/router"
secretboxschema "honoka-chan/internal/schema/secretbox"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin"
)
func multi(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
multiReq := secretboxschema.MultiReq{}
err := honokautils.ParseRequestData(ctx, &multiReq)
if ss.CheckErr(err) {
return
}
data, err := drawData(ctx, multiReq.SecretBoxID, multiReq.ID)
if ss.CheckErr(err) {
return
}
ss.Respond(secretboxschema.MultiResp{
ResponseData: data,
ReleaseInfo: []any{},
StatusCode: 200,
})
}
func init() {
router.AddHandler("main.php", "POST", "/secretbox/multi", middleware.Common, multi)
}
+211
View File
@@ -0,0 +1,211 @@
package secretbox
import (
"fmt"
"honoka-chan/internal/middleware"
unitmodel "honoka-chan/internal/model/unit"
"honoka-chan/internal/router"
secretboxapischema "honoka-chan/internal/schema/api/secretbox"
secretboxschema "honoka-chan/internal/schema/secretbox"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"math/rand"
"time"
"github.com/gin-gonic/gin"
)
func pon(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
ponReq := secretboxschema.PonReq{}
err := honokautils.ParseRequestData(ctx, &ponReq)
if ss.CheckErr(err) {
return
}
data, err := drawData(ctx, ponReq.SecretBoxID, ponReq.ID)
if ss.CheckErr(err) {
return
}
ss.Respond(secretboxschema.PonResp{
ResponseData: data,
ReleaseInfo: []any{},
StatusCode: 200,
})
}
func init() {
router.AddHandler("main.php", "POST", "/secretbox/pon", middleware.Common, pon)
}
func drawData(ctx *gin.Context, secretBoxID, costID int) (secretboxschema.PonData, error) {
if secretBoxID <= 0 || costID <= 0 {
return secretboxschema.PonData{}, fmt.Errorf("invalid secretbox draw params: secret_box_id=%d id=%d", secretBoxID, costID)
}
ss := session.Get(ctx)
config, err := loadSecretBoxAllData()
if err != nil {
return secretboxschema.PonData{}, err
}
memberCategory, page, err := findSecretBoxPage(config, secretBoxID)
if err != nil {
return secretboxschema.PonData{}, err
}
button, cost, err := findSecretBoxCost(page, costID)
if err != nil {
return secretboxschema.PonData{}, err
}
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
drawnUnits, lowestRarity, err := buildDrawUnits(ss, rng, memberCategory, page, button, cost)
if err != nil {
return secretboxschema.PonData{}, err
}
for i := range page.ButtonList {
for j := range page.ButtonList[i].CostList {
page.ButtonList[i].CostList[j].Payable = true
}
}
page.SecretBoxInfo.PonCount += cost.UnitCount
itemList := config.Result.ItemList
for i := range itemList {
if itemList[i].Amount < 999 {
itemList[i].Amount = 999
}
}
return secretboxschema.PonData{
IsUnitMax: false,
ItemList: itemList,
GaugeInfo: secretboxapischema.GaugeInfo{
MaxGaugePoint: 100,
GaugePoint: 0,
},
ButtonList: page.ButtonList,
SecretBoxInfo: page.SecretBoxInfo,
SecretBoxItems: secretboxschema.SecretBoxItems{Unit: drawnUnits, Item: []secretboxschema.SecretBoxRewardItem{}},
BeforeUserInfo: ss.GetUserInfo(),
AfterUserInfo: ss.GetUserInfo(),
FreeMuseGachaFlag: false,
FreeAqoursGachaFlag: false,
LowestRarity: lowestRarity,
PromotionPerformanceRate: 25,
SecretBoxParcelType: 2,
LimitBonusInfo: []any{},
LimitBonusRewards: []any{},
UnitSupportList: []any{},
}, nil
}
func buildDrawUnits(ss *session.Session, rng *rand.Rand, memberCategory int, page secretboxapischema.SecretBoxPage, button secretboxapischema.SecretBoxButton, cost secretboxapischema.SecretBoxCost) ([]secretboxschema.SecretBoxUnitItem, int, error) {
drawCount := cost.UnitCount
if drawCount <= 0 {
drawCount = 1
}
rarities := make([]int, 0, drawCount)
for i := 0; i < drawCount; i++ {
rarities = append(rarities, pickRarity(rng, page, button, i == drawCount-1 && drawCount > 1))
}
if drawCount > 1 {
guaranteedRarity := raritySR
if hasGuaranteedUR(button, cost) {
guaranteedRarity = rarityUR
}
guaranteed := false
for _, rarity := range rarities {
if satisfiesGuaranteedRarity(rarity, guaranteedRarity) {
guaranteed = true
break
}
}
if !guaranteed {
if guaranteedRarity == rarityUR {
rarities[drawCount-1] = rarityUR
} else {
rarities[drawCount-1] = pickGuaranteedRarity(rng, page, button)
}
}
}
result := make([]secretboxschema.SecretBoxUnitItem, 0, drawCount)
lowestRarity := 0
for _, rarity := range rarities {
unitID, err := randomUnitID(ss, rng, memberCategory, page, button, rarity)
if err != nil {
return nil, 0, err
}
unitData, err := getUserUnitByUnitID(ss, unitID)
if err != nil {
return nil, 0, err
}
item := buildUnitItem(unitData)
item.IsHit = shouldShowDirectURHit(rng, page, button, rarity)
result = append(result, item)
if lowestRarity == 0 || isLowerQualityRarity(rarity, lowestRarity) {
lowestRarity = rarity
}
}
return result, lowestRarity, nil
}
func buildUnitItem(unitData unitmodel.UnitDataMap) secretboxschema.SecretBoxUnitItem {
isSupportMember := unitData.MaxLove == 0 && unitData.UnitSkillLevel == 0 && unitData.UnitRemovableSkillCapacity == 0
insertDate := time.Unix(unitData.InsertDate, 0).Format("2006-01-02 15:04:05")
return secretboxschema.SecretBoxUnitItem{
UnitOwningUserID: unitData.UnitOwningUserID,
UnitOwningIDs: []int{unitData.UnitOwningUserID},
UnitID: unitData.UnitID,
Exp: unitData.Exp,
NextExp: 0,
Level: unitData.Level,
MaxLevel: unitData.MaxLevel,
LevelLimitID: unitData.LevelLimitID,
Rank: unitData.Rank,
MaxRank: unitData.MaxRank,
Love: unitData.Love,
MaxLove: unitData.MaxLove,
UnitSkillLevel: unitData.UnitSkillLevel,
SkillLevel: unitData.UnitSkillLevel,
UnitSkillExp: unitData.UnitSkillExp,
MaxHp: unitData.MaxHp,
FavoriteFlag: unitData.FavoriteFlag,
DisplayRank: unitData.DisplayRank,
UnitRemovableSkillCapacity: unitData.UnitRemovableSkillCapacity,
MaxRemovableSkillCapacity: unitData.UnitRemovableSkillCapacity,
Attribute: unitData.Attribute,
Smile: unitData.Smile,
Cute: unitData.Cute,
Cool: unitData.Cool,
IsRankMax: unitData.IsRankMax,
IsLoveMax: unitData.IsLoveMax,
IsLevelMax: unitData.IsLevelMax,
IsSigned: unitData.IsSigned,
IsSkillLevelMax: unitData.IsSkillLevelMax,
IsRemovableSkillCapacityMax: unitData.IsRemovableSkillCapacityMax,
IsSupportMember: isSupportMember,
NewUnitFlag: false,
UnitRarityID: unitData.Rarity,
IsHit: unitData.Rarity == rarityUR,
RemovableSkillIDs: []int{},
InsertDate: insertDate,
TotalSmile: unitData.Smile,
TotalCute: unitData.Cute,
TotalCool: unitData.Cool,
TotalHp: unitData.MaxHp,
}
}
+256
View File
@@ -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]
}
+104
View File
@@ -0,0 +1,104 @@
package secretbox
import (
"fmt"
unitmodel "honoka-chan/internal/model/unit"
secretboxapischema "honoka-chan/internal/schema/api/secretbox"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"honoka-chan/pkg/db"
"sort"
"time"
)
const secretBoxDataFile = "secretbox_data.json"
func loadSecretBoxAllData() (secretboxapischema.AllResp, error) {
data, err := honokautils.LoadServerData[secretboxapischema.AllData](secretBoxDataFile)
if err != nil {
return secretboxapischema.AllResp{}, err
}
return secretboxapischema.AllResp{
Result: data,
Status: 200,
CommandNum: false,
TimeStamp: time.Now().Unix(),
}, nil
}
func findSecretBoxPage(config secretboxapischema.AllResp, secretBoxID int) (int, secretboxapischema.SecretBoxPage, error) {
for _, category := range config.Result.MemberCategoryList {
for _, page := range category.PageList {
if page.SecretBoxInfo.SecretBoxID == secretBoxID {
return category.MemberCategory, page, nil
}
}
}
return 0, secretboxapischema.SecretBoxPage{}, fmt.Errorf("secretbox not found: %d", secretBoxID)
}
func findSecretBoxCost(page secretboxapischema.SecretBoxPage, costID int) (secretboxapischema.SecretBoxButton, secretboxapischema.SecretBoxCost, error) {
for _, button := range page.ButtonList {
for _, cost := range button.CostList {
if cost.ID == costID {
return button, cost, nil
}
}
}
return secretboxapischema.SecretBoxButton{}, secretboxapischema.SecretBoxCost{}, fmt.Errorf("secretbox cost not found: %d", costID)
}
func queryUnitIDs(ss *session.Session, memberCategory int, supportOnly bool, rarity int) ([]int, error) {
var (
rows []struct {
UnitID int `xorm:"unit_id"`
}
err error
)
sql := `
SELECT u.unit_id
FROM unit_m u
LEFT JOIN unit_type_m ut ON ut.unit_type_id = u.unit_type_id
WHERE u.rarity = ?`
args := []any{rarity}
if supportOnly {
sql += ` AND u.disable_rank_up != 0`
} else if memberCategory >= 0 {
sql += ` AND ut.member_category = ?`
args = append(args, memberCategory)
}
if ss != nil {
err = ss.MainEng.SQL(sql, args...).Find(&rows)
} else {
err = db.MainEng.SQL(sql, args...).Find(&rows)
}
if err != nil {
return nil, err
}
result := make([]int, 0, len(rows))
for _, row := range rows {
result = append(result, row.UnitID)
}
sort.Ints(result)
return result, nil
}
func getUserUnitByUnitID(ss *session.Session, unitID int) (unitmodel.UnitDataMap, error) {
unitData := unitmodel.UnitDataMap{}
has, err := ss.GetBasicUnitInfo().
Where("a.user_id = ?", ss.UserID).
Where("a.unit_id = ?", unitID).
Get(&unitData)
if err != nil {
return unitmodel.UnitDataMap{}, err
}
if !has {
return unitmodel.UnitDataMap{}, fmt.Errorf("user %d does not own unit %d", ss.UserID, unitID)
}
return unitData, nil
}
+78
View File
@@ -0,0 +1,78 @@
package secretbox
import (
"fmt"
"honoka-chan/internal/middleware"
"honoka-chan/internal/router"
secretboxschema "honoka-chan/internal/schema/secretbox"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin"
)
func showDetail(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
showDetailReq := secretboxschema.ShowDetailReq{}
err := honokautils.ParseRequestData(ctx, &showDetailReq)
if ss.CheckErr(err) {
return
}
data, err := showDetailData(showDetailReq.SecretBoxID)
if ss.CheckErr(err) {
return
}
ss.Respond(secretboxschema.ShowDetailResp{
ResponseData: data,
ReleaseInfo: []any{},
StatusCode: 200,
})
}
func init() {
router.AddHandler("main.php", "POST", "/secretbox/showDetail", middleware.Common, showDetail)
}
func showDetailData(secretBoxID int) (secretboxschema.ShowDetailData, error) {
if secretBoxID <= 0 {
return secretboxschema.ShowDetailData{}, fmt.Errorf("invalid secret_box_id: %d", secretBoxID)
}
config, err := loadSecretBoxAllData()
if err != nil {
return secretboxschema.ShowDetailData{}, err
}
memberCategory, page, err := findSecretBoxPage(config, secretBoxID)
if err != nil {
return secretboxschema.ShowDetailData{}, err
}
result := secretboxschema.ShowDetailData{
URL: fmt.Sprintf("/webview.php/secretbox/detail?id=%d&no_title=1", secretBoxID),
}
switch page.SecretBoxInfo.SecretBoxType {
case 5:
buttons := make([]secretboxschema.ButtonTypeUnitLineUp, 0, len(page.ButtonList))
for _, button := range page.ButtonList {
buttons = append(buttons, secretboxschema.ButtonTypeUnitLineUp{
SecretBoxButtonType: button.SecretBoxButtonType,
SecretBoxName: button.SecretBoxName,
UnitLineUp: buildUnitLineUp(memberCategory, page, button),
})
}
result.ButtonTypeUnitLineUp = buttons
default:
if len(page.ButtonList) == 0 {
return secretboxschema.ShowDetailData{}, fmt.Errorf("secretbox %d has no buttons", secretBoxID)
}
result.UnitLineUp = buildUnitLineUp(memberCategory, page, page.ButtonList[0])
}
return result, nil
}
+6 -4
View File
@@ -1,10 +1,12 @@
package apischema
type ApiReq struct {
Module string `json:"module"`
UserID int `json:"user_id"`
Action string `json:"action"`
Timestamp int64 `json:"timeStamp"`
Module string `json:"module"`
UserID int `json:"user_id"`
Action string `json:"action"`
ID int `json:"id"`
SecretBoxID int `json:"secret_box_id"`
Timestamp int64 `json:"timeStamp"`
}
type ApiResp struct {
+9 -10
View File
@@ -1,16 +1,15 @@
package bannerapischema
type BannerList struct {
BannerType int `json:"banner_type"`
TargetID int `json:"target_id"`
AssetPath string `json:"asset_path"`
FixedFlag bool `json:"fixed_flag"`
BackSide bool `json:"back_side"`
BannerID int `json:"banner_id"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
AddUnitStartDate string `json:"add_unit_start_date,omitempty"`
WebviewURL string `json:"webview_url,omitempty"`
BannerType int `json:"banner_type"`
TargetID int `json:"target_id"`
AssetPath string `json:"asset_path"`
FixedFlag bool `json:"fixed_flag"`
BackSide bool `json:"back_side"`
BannerID int `json:"banner_id"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
WebviewURL string `json:"webview_url,omitempty"`
}
type ListData struct {
+80
View File
@@ -0,0 +1,80 @@
package secretboxapischema
type ItemAmount struct {
ItemID int `json:"item_id"`
Amount int `json:"amount"`
}
type GaugeInfo struct {
MaxGaugePoint int `json:"max_gauge_point"`
GaugePoint int `json:"gauge_point"`
}
type SecretBoxAnimationAssets struct {
Type int `json:"type"`
BackgroundAsset string `json:"background_asset"`
AdditionalAsset1 string `json:"additional_asset_1"`
AdditionalAsset2 string `json:"additional_asset_2"`
AdditionalAsset3 string `json:"additional_asset_3"`
}
type SecretBoxCost struct {
ID int `json:"id"`
Type int `json:"type"`
Amount int `json:"amount"`
ItemID *int `json:"item_id"`
UnitCount int `json:"unit_count"`
Payable bool `json:"payable"`
}
type SecretBoxButton struct {
SecretBoxButtonType int `json:"secret_box_button_type"`
CostList []SecretBoxCost `json:"cost_list"`
SecretBoxName string `json:"secret_box_name"`
LimitCount int `json:"limit_count,omitempty"`
LimitUnderMessage string `json:"limit_under_message,omitempty"`
LimitUnderMessageSuffix string `json:"limit_under_message_suffix,omitempty"`
BalloonAsset string `json:"balloon_asset,omitempty"`
}
type SecretBoxInfo struct {
SecretBoxID int `json:"secret_box_id"`
SecretBoxType int `json:"secret_box_type"`
Name string `json:"name"`
Description string `json:"description"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
ShowEndDate string `json:"show_end_date"`
AddGauge int `json:"add_gauge"`
AlwaysDisplayFlag int `json:"always_display_flag"`
PonCount int `json:"pon_count"`
PonUpperLimit int `json:"pon_upper_limit"`
}
type SecretBoxPage struct {
MenuAsset string `json:"menu_asset"`
PageOrder int `json:"page_order"`
AnimationAssets SecretBoxAnimationAssets `json:"animation_assets"`
ButtonList []SecretBoxButton `json:"button_list"`
SecretBoxInfo SecretBoxInfo `json:"secret_box_info"`
}
type MemberCategoryPageList struct {
MemberCategory int `json:"member_category"`
PageList []SecretBoxPage `json:"page_list"`
}
type AllData struct {
UseCache int `json:"use_cache"`
IsUnitMax bool `json:"is_unit_max"`
ItemList []ItemAmount `json:"item_list"`
GaugeInfo GaugeInfo `json:"gauge_info"`
MemberCategoryList []MemberCategoryPageList `json:"member_category_list"`
}
type AllResp struct {
Result AllData `json:"result"`
Status int `json:"status"`
CommandNum bool `json:"commandNum"`
TimeStamp int64 `json:"timeStamp"`
}
+19
View File
@@ -0,0 +1,19 @@
package secretboxschema
type MultiReq struct {
Module string `json:"module"`
Action string `json:"action"`
Mgd int `json:"mgd"`
ID int `json:"id"`
SecretBoxID int `json:"secret_box_id"`
Count int `json:"count"`
TimeStamp int64 `json:"timeStamp"`
CommandNum string `json:"commandNum"`
UnitTypeIds []any `json:"unit_type_ids"`
}
type MultiResp struct {
ResponseData PonData `json:"response_data"`
ReleaseInfo []any `json:"release_info"`
StatusCode int `json:"status_code"`
}
+96
View File
@@ -0,0 +1,96 @@
package secretboxschema
import secretboxapischema "honoka-chan/internal/schema/api/secretbox"
type PonReq struct {
Module string `json:"module"`
Mgd int `json:"mgd"`
Action string `json:"action"`
TimeStamp int64 `json:"timeStamp"`
ID int `json:"id"`
CommandNum string `json:"commandNum"`
SecretBoxID int `json:"secret_box_id"`
UnitTypeIds []any `json:"unit_type_ids"`
}
type SecretBoxUnitItem struct {
UnitOwningUserID int `json:"unit_owning_user_id"`
UnitOwningIDs []int `json:"unit_owning_ids"`
UnitID int `json:"unit_id"`
Exp int `json:"exp"`
NextExp int `json:"next_exp"`
Level int `json:"level"`
MaxLevel int `json:"max_level"`
LevelLimitID int `json:"level_limit_id"`
Rank int `json:"rank"`
MaxRank int `json:"max_rank"`
Love int `json:"love"`
MaxLove int `json:"max_love"`
UnitSkillLevel int `json:"unit_skill_level"`
SkillLevel int `json:"skill_level"`
UnitSkillExp int `json:"unit_skill_exp"`
MaxHp int `json:"max_hp"`
FavoriteFlag bool `json:"favorite_flag"`
DisplayRank int `json:"display_rank"`
UnitRemovableSkillCapacity int `json:"unit_removable_skill_capacity"`
MaxRemovableSkillCapacity int `json:"max_removable_skill_capacity"`
Attribute int `json:"attribute"`
Smile int `json:"smile"`
Cute int `json:"cute"`
Cool int `json:"cool"`
IsRankMax bool `json:"is_rank_max"`
IsLoveMax bool `json:"is_love_max"`
IsLevelMax bool `json:"is_level_max"`
IsSigned bool `json:"is_signed"`
IsSkillLevelMax bool `json:"is_skill_level_max"`
IsRemovableSkillCapacityMax bool `json:"is_removable_skill_capacity_max"`
IsSupportMember bool `json:"is_support_member"`
NewUnitFlag bool `json:"new_unit_flag"`
UnitRarityID int `json:"unit_rarity_id"`
IsHit bool `json:"is_hit"`
RemovableSkillIDs []int `json:"removable_skill_ids"`
InsertDate string `json:"insert_date"`
TotalSmile int `json:"total_smile"`
TotalCute int `json:"total_cute"`
TotalCool int `json:"total_cool"`
TotalHp int `json:"total_hp"`
}
type SecretBoxRewardItem struct {
ItemID int `json:"item_id"`
AddType int `json:"add_type"`
Amount int `json:"amount"`
ItemCategoryID int `json:"item_category_id"`
RewardBoxFlag bool `json:"reward_box_flag"`
InsertDate string `json:"insert_date"`
}
type SecretBoxItems struct {
Unit []SecretBoxUnitItem `json:"unit"`
Item []SecretBoxRewardItem `json:"item"`
}
type PonData struct {
IsUnitMax bool `json:"is_unit_max"`
ItemList []secretboxapischema.ItemAmount `json:"item_list"`
GaugeInfo secretboxapischema.GaugeInfo `json:"gauge_info"`
ButtonList []secretboxapischema.SecretBoxButton `json:"button_list"`
SecretBoxInfo secretboxapischema.SecretBoxInfo `json:"secret_box_info"`
SecretBoxItems SecretBoxItems `json:"secret_box_items"`
BeforeUserInfo any `json:"before_user_info"`
AfterUserInfo any `json:"after_user_info"`
FreeMuseGachaFlag bool `json:"free_muse_gacha_flag"`
FreeAqoursGachaFlag bool `json:"free_aqours_gacha_flag"`
LowestRarity int `json:"lowest_rarity"`
PromotionPerformanceRate int `json:"promotion_performance_rate"`
SecretBoxParcelType int `json:"secret_box_parcel_type"`
LimitBonusInfo []any `json:"limit_bonus_info"`
LimitBonusRewards []any `json:"limit_bonus_rewards"`
UnitSupportList []any `json:"unit_support_list"`
}
type PonResp struct {
ResponseData PonData `json:"response_data"`
ReleaseInfo []any `json:"release_info"`
StatusCode int `json:"status_code"`
}
+33
View File
@@ -0,0 +1,33 @@
package secretboxschema
type ShowDetailReq struct {
Module string `json:"module"`
Action string `json:"action"`
TimeStamp int64 `json:"timeStamp"`
Mgd int `json:"mgd"`
SecretBoxID int `json:"secret_box_id"`
CommandNum string `json:"commandNum"`
}
type UnitLineUp struct {
Rarity int `json:"rarity"`
UnitIDs []int `json:"unit_ids"`
}
type ButtonTypeUnitLineUp struct {
SecretBoxButtonType int `json:"secret_box_button_type"`
SecretBoxName string `json:"secret_box_name"`
UnitLineUp []UnitLineUp `json:"unit_line_up"`
}
type ShowDetailData struct {
ButtonTypeUnitLineUp []ButtonTypeUnitLineUp `json:"button_type_unit_line_up,omitempty"`
UnitLineUp []UnitLineUp `json:"unit_line_up,omitempty"`
URL string `json:"url"`
}
type ShowDetailResp struct {
ResponseData ShowDetailData `json:"response_data"`
ReleaseInfo []any `json:"release_info"`
StatusCode int `json:"status_code"`
}