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
File diff suppressed because it is too large Load Diff
+930
View File
@@ -0,0 +1,930 @@
{
"use_cache": 1,
"is_unit_max": false,
"item_list": [
{
"item_id": 1,
"amount": 0
},
{
"item_id": 5,
"amount": 1
},
{
"item_id": 8,
"amount": 0
},
{
"item_id": 9,
"amount": 0
},
{
"item_id": 10,
"amount": 0
},
{
"item_id": 11,
"amount": 0
},
{
"item_id": 12,
"amount": 0
},
{
"item_id": 13,
"amount": 0
},
{
"item_id": 14,
"amount": 0
},
{
"item_id": 15,
"amount": 0
},
{
"item_id": 19,
"amount": 0
},
{
"item_id": 20,
"amount": 0
},
{
"item_id": 23,
"amount": 0
},
{
"item_id": 24,
"amount": 0
},
{
"item_id": 25,
"amount": 0
},
{
"item_id": 28,
"amount": 0
},
{
"item_id": 30,
"amount": 0
},
{
"item_id": 31,
"amount": 0
},
{
"item_id": 51,
"amount": 0
},
{
"item_id": 53,
"amount": 0
},
{
"item_id": 56,
"amount": 0
},
{
"item_id": 59,
"amount": 0
},
{
"item_id": 61,
"amount": 132
},
{
"item_id": 62,
"amount": 284
},
{
"item_id": 63,
"amount": 324
},
{
"item_id": 67,
"amount": 0
},
{
"item_id": 69,
"amount": 0
},
{
"item_id": 72,
"amount": 0
},
{
"item_id": 74,
"amount": 0
},
{
"item_id": 92,
"amount": 0
},
{
"item_id": 93,
"amount": 0
},
{
"item_id": 94,
"amount": 0
},
{
"item_id": 95,
"amount": 0
},
{
"item_id": 126,
"amount": 0
},
{
"item_id": 129,
"amount": 0
},
{
"item_id": 146,
"amount": 0
},
{
"item_id": 147,
"amount": 0
},
{
"item_id": 168,
"amount": 1043
},
{
"item_id": 173,
"amount": 0
},
{
"item_id": 174,
"amount": 0
},
{
"item_id": 179,
"amount": 0
},
{
"item_id": 180,
"amount": 0
},
{
"item_id": 181,
"amount": 0
},
{
"item_id": 182,
"amount": 0
},
{
"item_id": 186,
"amount": 0
},
{
"item_id": 187,
"amount": 0
},
{
"item_id": 188,
"amount": 40
},
{
"item_id": 189,
"amount": 54
},
{
"item_id": 190,
"amount": 96
},
{
"item_id": 191,
"amount": 246
},
{
"item_id": 192,
"amount": 0
},
{
"item_id": 193,
"amount": 0
},
{
"item_id": 194,
"amount": 0
},
{
"item_id": 195,
"amount": 0
},
{
"item_id": 196,
"amount": 0
},
{
"item_id": 197,
"amount": 0
},
{
"item_id": 214,
"amount": 0
},
{
"item_id": 216,
"amount": 0
},
{
"item_id": 217,
"amount": 0
},
{
"item_id": 247,
"amount": 0
},
{
"item_id": 248,
"amount": 0
},
{
"item_id": 250,
"amount": 0
},
{
"item_id": 256,
"amount": 0
},
{
"item_id": 265,
"amount": 0
},
{
"item_id": 267,
"amount": 0
},
{
"item_id": 268,
"amount": 0
},
{
"item_id": 269,
"amount": 0
},
{
"item_id": 277,
"amount": 0
},
{
"item_id": 280,
"amount": 0
},
{
"item_id": 281,
"amount": 0
},
{
"item_id": 282,
"amount": 0
},
{
"item_id": 283,
"amount": 0
},
{
"item_id": 284,
"amount": 0
},
{
"item_id": 285,
"amount": 0
},
{
"item_id": 313,
"amount": 0
},
{
"item_id": 314,
"amount": 0
},
{
"item_id": 315,
"amount": 0
},
{
"item_id": 316,
"amount": 0
},
{
"item_id": 317,
"amount": 0
},
{
"item_id": 344,
"amount": 0
},
{
"item_id": 345,
"amount": 0
},
{
"item_id": 346,
"amount": 0
},
{
"item_id": 347,
"amount": 0
},
{
"item_id": 348,
"amount": 0
},
{
"item_id": 353,
"amount": 0
},
{
"item_id": 395,
"amount": 0
},
{
"item_id": 412,
"amount": 0
},
{
"item_id": 422,
"amount": 22
},
{
"item_id": 423,
"amount": 11
},
{
"item_id": 424,
"amount": 33
},
{
"item_id": 1000,
"amount": 61
},
{
"item_id": 1200,
"amount": 702
},
{
"item_id": 10001,
"amount": 0
},
{
"item_id": 10003,
"amount": 0
},
{
"item_id": 10004,
"amount": 0
},
{
"item_id": 10005,
"amount": 0
},
{
"item_id": 10006,
"amount": 0
},
{
"item_id": 10007,
"amount": 0
},
{
"item_id": 10008,
"amount": 0
},
{
"item_id": 10010,
"amount": 0
},
{
"item_id": 10012,
"amount": 0
},
{
"item_id": 10015,
"amount": 0
},
{
"item_id": 10017,
"amount": 0
},
{
"item_id": 10019,
"amount": 0
},
{
"item_id": 10020,
"amount": 0
},
{
"item_id": 10022,
"amount": 0
},
{
"item_id": 10023,
"amount": 0
},
{
"item_id": 10024,
"amount": 0
},
{
"item_id": 10026,
"amount": 0
},
{
"item_id": 10031,
"amount": 0
},
{
"item_id": 10032,
"amount": 0
},
{
"item_id": 10047,
"amount": 0
},
{
"item_id": 10048,
"amount": 0
},
{
"item_id": 10053,
"amount": 1
},
{
"item_id": 10066,
"amount": 4
},
{
"item_id": 10067,
"amount": 0
},
{
"item_id": 10074,
"amount": 0
},
{
"item_id": 10090,
"amount": 0
},
{
"item_id": 10093,
"amount": 0
},
{
"item_id": 14001,
"amount": 0
}
],
"gauge_info": {
"max_gauge_point": 100,
"gauge_point": 60
},
"member_category_list": [
{
"member_category": 1,
"page_list": [
{
"menu_asset": "assets/image/secretbox/icon/s_ba_1718_1.png",
"page_order": 1457,
"animation_assets": {
"type": 1,
"background_asset": "assets/image/secretbox/background/bg_1718.png",
"additional_asset_1": "assets/image/secretbox/navi/navi_1718.png",
"additional_asset_2": "assets/image/secretbox/title/title_1718.png",
"additional_asset_3": "assets/image/secretbox/appeal/appeal_1718.png"
},
"button_list": [
{
"secret_box_button_type": 1,
"cost_list": [
{
"id": 330,
"payable": true,
"unit_count": 1,
"type": 3001,
"item_id": null,
"amount": 5
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜μ's〜 第1弹"
},
{
"secret_box_button_type": 2,
"cost_list": [
{
"id": 333,
"payable": false,
"unit_count": 11,
"type": 3001,
"item_id": null,
"amount": 50
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜μ's〜 第1弹",
"balloon_asset": "assets/image/secretbox/balloon/balloon_31.png"
},
{
"secret_box_button_type": 16,
"cost_list": [
{
"id": 336,
"payable": false,
"unit_count": 11,
"type": 1000,
"item_id": 8,
"amount": 1
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜μ's〜 第1弹",
"balloon_asset": "assets/image/secretbox/balloon/balloon_31.png"
}
],
"secret_box_info": {
"secret_box_id": 1718,
"secret_box_type": 0,
"name": "学园偶像祭大感谢祭BOX招募〜μ's〜 第1弹",
"description": null,
"start_date": "2023-03-05 15:00:00",
"end_date": "2037-03-31 14:59:59",
"show_end_date": "2037-03-31 14:59:59",
"add_gauge": 10,
"always_display_flag": 1,
"pon_count": 17,
"pon_upper_limit": 0
}
},
{
"menu_asset": "assets/image/secretbox/icon/s_ba_22_1.png",
"page_order": 6998,
"animation_assets": {
"type": 5,
"background_asset": "assets/image/secretbox/background/bg_22.png",
"additional_asset_1": "assets/image/secretbox/navi/navi_22.png",
"additional_asset_2": "assets/image/secretbox/title/title_22.png"
},
"button_list": [
{
"secret_box_button_type": 9,
"cost_list": [
{
"id": 17,
"payable": false,
"unit_count": 1,
"type": 1000,
"item_id": 5,
"amount": 5
}
],
"secret_box_name": "SR・UR招募 ~μ's",
"balloon_asset": "assets/image/secretbox/balloon/balloon_5.png"
},
{
"secret_box_button_type": 10,
"cost_list": [
{
"id": 18,
"payable": false,
"unit_count": 1,
"type": 1000,
"item_id": 5,
"amount": 10
}
],
"secret_box_name": "SSR・UR招募 ~μ's",
"balloon_asset": "assets/image/secretbox/balloon/balloon_6.png"
},
{
"secret_box_button_type": 11,
"cost_list": [
{
"id": 19,
"payable": false,
"unit_count": 1,
"type": 1000,
"item_id": 5,
"amount": 25
}
],
"secret_box_name": "UR招募 ~μ's",
"balloon_asset": "assets/image/secretbox/balloon/balloon_7.png"
}
],
"secret_box_info": {
"secret_box_id": 22,
"secret_box_type": 5,
"name": "SR・SSR・UR招募~μ's~",
"description": null,
"start_date": "2013-07-31 15:00:00",
"end_date": "2037-12-31 23:59:59",
"add_gauge": 0,
"always_display_flag": 1,
"pon_count": 98,
"pon_upper_limit": 0
}
}
]
},
{
"member_category": 2,
"page_list": [
{
"menu_asset": "assets/image/secretbox/icon/s_ba_1719_1.png",
"page_order": 11426,
"animation_assets": {
"type": 1,
"background_asset": "assets/image/secretbox/background/bg_1719.png",
"additional_asset_1": "assets/image/secretbox/navi/navi_1719.png",
"additional_asset_2": "assets/image/secretbox/title/title_1719.png",
"additional_asset_3": "assets/image/secretbox/appeal/appeal_1719.png"
},
"button_list": [
{
"secret_box_button_type": 1,
"cost_list": [
{
"id": 330,
"payable": true,
"unit_count": 1,
"type": 3001,
"item_id": null,
"amount": 5
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜Aqours〜 第1弹"
},
{
"secret_box_button_type": 2,
"cost_list": [
{
"id": 333,
"payable": false,
"unit_count": 11,
"type": 3001,
"item_id": null,
"amount": 50
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜Aqours〜 第1弹",
"balloon_asset": "assets/image/secretbox/balloon/balloon_31.png"
},
{
"secret_box_button_type": 16,
"cost_list": [
{
"id": 336,
"payable": false,
"unit_count": 11,
"type": 1000,
"item_id": 8,
"amount": 1
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜Aqours〜 第1弹",
"balloon_asset": "assets/image/secretbox/balloon/balloon_31.png"
}
],
"secret_box_info": {
"secret_box_id": 1719,
"secret_box_type": 0,
"name": "学园偶像祭大感谢祭BOX招募〜Aqours〜 第1弹",
"description": null,
"start_date": "2023-03-05 15:00:00",
"end_date": "2037-03-31 14:59:59",
"show_end_date": "2037-03-31 14:59:59",
"add_gauge": 10,
"always_display_flag": 1,
"pon_count": 0,
"pon_upper_limit": 0
}
},
{
"menu_asset": "assets/image/secretbox/icon/s_ba_73_1.png",
"page_order": 16998,
"animation_assets": {
"type": 5,
"background_asset": "assets/image/secretbox/background/bg_73.png",
"additional_asset_1": "assets/image/secretbox/navi/navi_73.png",
"additional_asset_2": "assets/image/secretbox/title/title_22.png"
},
"button_list": [
{
"secret_box_button_type": 9,
"cost_list": [
{
"id": 17,
"payable": false,
"unit_count": 1,
"type": 1000,
"item_id": 5,
"amount": 5
}
],
"secret_box_name": "SR・UR招募 Aqours",
"balloon_asset": "assets/image/secretbox/balloon/balloon_5.png"
},
{
"secret_box_button_type": 10,
"cost_list": [
{
"id": 18,
"payable": false,
"unit_count": 1,
"type": 1000,
"item_id": 5,
"amount": 10
}
],
"secret_box_name": "SSR・UR招募 Aqours",
"balloon_asset": "assets/image/secretbox/balloon/balloon_6.png"
},
{
"secret_box_button_type": 11,
"cost_list": [
{
"id": 19,
"payable": false,
"unit_count": 1,
"type": 1000,
"item_id": 5,
"amount": 25
}
],
"secret_box_name": "UR招募 Aqours",
"balloon_asset": "assets/image/secretbox/balloon/balloon_7.png"
}
],
"secret_box_info": {
"secret_box_id": 73,
"secret_box_type": 5,
"name": "SR・SSR・UR招募 ~Aqours~",
"description": null,
"start_date": "2017-03-31 15:00:00",
"end_date": "2037-12-31 23:59:59",
"add_gauge": 0,
"always_display_flag": 1,
"pon_count": 0,
"pon_upper_limit": 0
}
}
]
},
{
"member_category": 3,
"page_list": [
{
"menu_asset": "assets/image/secretbox/icon/s_ba_1720_1.png",
"page_order": 24986,
"animation_assets": {
"type": 1,
"background_asset": "assets/image/secretbox/background/bg_1720.png",
"additional_asset_1": "assets/image/secretbox/navi/navi_1720.png",
"additional_asset_2": "assets/image/secretbox/title/title_1720.png",
"additional_asset_3": "assets/image/secretbox/appeal/appeal_1720.png"
},
"button_list": [
{
"secret_box_button_type": 1,
"cost_list": [
{
"id": 330,
"payable": true,
"unit_count": 1,
"type": 3001,
"item_id": null,
"amount": 5
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜虹咲〜 第1弹"
},
{
"secret_box_button_type": 2,
"cost_list": [
{
"id": 333,
"payable": false,
"unit_count": 11,
"type": 3001,
"item_id": null,
"amount": 50
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜虹咲〜 第1弹",
"balloon_asset": "assets/image/secretbox/balloon/balloon_31.png"
},
{
"secret_box_button_type": 16,
"cost_list": [
{
"id": 336,
"payable": false,
"unit_count": 11,
"type": 1000,
"item_id": 8,
"amount": 1
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜虹咲〜 第1弹",
"balloon_asset": "assets/image/secretbox/balloon/balloon_31.png"
}
],
"secret_box_info": {
"secret_box_id": 1720,
"secret_box_type": 0,
"name": "学园偶像祭大感谢祭BOX招募〜虹咲〜 第1弹",
"description": null,
"start_date": "2023-03-05 15:00:00",
"end_date": "2037-03-31 14:59:59",
"show_end_date": "2037-03-31 14:59:59",
"add_gauge": 10,
"always_display_flag": 1,
"pon_count": 0,
"pon_upper_limit": 0
}
}
]
},
{
"member_category": 4,
"page_list": [
{
"menu_asset": "assets/image/secretbox/icon/s_ba_1721_1.png",
"page_order": 31971,
"animation_assets": {
"type": 1,
"background_asset": "assets/image/secretbox/background/bg_1721.png",
"additional_asset_1": "assets/image/secretbox/navi/navi_1721.png",
"additional_asset_2": "assets/image/secretbox/title/title_1721.png",
"additional_asset_3": "assets/image/secretbox/appeal/appeal_1721.png"
},
"button_list": [
{
"secret_box_button_type": 1,
"cost_list": [
{
"id": 330,
"payable": true,
"unit_count": 1,
"type": 3001,
"item_id": null,
"amount": 5
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜Liella!〜 第1弹"
},
{
"secret_box_button_type": 2,
"cost_list": [
{
"id": 333,
"payable": false,
"unit_count": 11,
"type": 3001,
"item_id": null,
"amount": 50
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜Liella!〜 第1弹",
"balloon_asset": "assets/image/secretbox/balloon/balloon_31.png"
},
{
"secret_box_button_type": 16,
"cost_list": [
{
"id": 336,
"payable": false,
"unit_count": 11,
"type": 1000,
"item_id": 8,
"amount": 1
}
],
"secret_box_name": "学园偶像祭大感谢祭BOX招募〜Liella!〜 第1弹",
"balloon_asset": "assets/image/secretbox/balloon/balloon_31.png"
}
],
"secret_box_info": {
"secret_box_id": 1721,
"secret_box_type": 0,
"name": "学园偶像祭大感谢祭BOX招募〜Liella!〜 第1弹",
"description": null,
"start_date": "2023-03-05 15:00:00",
"end_date": "2037-03-31 14:59:59",
"show_end_date": "2037-03-31 14:59:59",
"add_gauge": 10,
"always_display_flag": 1,
"pon_count": 0,
"pon_upper_limit": 0
}
}
]
}
]
}
+3
View File
@@ -24,6 +24,7 @@ import (
"honoka-chan/internal/handler/api/profile" "honoka-chan/internal/handler/api/profile"
"honoka-chan/internal/handler/api/reward" "honoka-chan/internal/handler/api/reward"
"honoka-chan/internal/handler/api/scenario" "honoka-chan/internal/handler/api/scenario"
"honoka-chan/internal/handler/api/secretbox"
"honoka-chan/internal/handler/api/stamp" "honoka-chan/internal/handler/api/stamp"
"honoka-chan/internal/handler/api/subscenario" "honoka-chan/internal/handler/api/subscenario"
"honoka-chan/internal/handler/api/unit" "honoka-chan/internal/handler/api/unit"
@@ -96,6 +97,8 @@ func api(ctx *gin.Context) {
result, err = reward.RewardApi(v.Action) result, err = reward.RewardApi(v.Action)
case "scenario": case "scenario":
result, err = scenario.ScenarioApi(ctx, v.Action) result, err = scenario.ScenarioApi(ctx, v.Action)
case "secretbox":
result, err = secretbox.SecretBoxApi(v.Action)
case "stamp": case "stamp":
result, err = stamp.StampApi(v.Action) result, err = stamp.StampApi(v.Action)
case "subscenario": case "subscenario":
+32 -36
View File
@@ -11,48 +11,44 @@ func bannerList() (res any, err error) {
TimeLimit: "2037-12-31 23:59:59", TimeLimit: "2037-12-31 23:59:59",
BannerList: []bannerapischema.BannerList{ BannerList: []bannerapischema.BannerList{
{ {
BannerType: 1, BannerType: 1,
TargetID: 1743, TargetID: 1718,
AssetPath: "assets/image/secretbox/icon/s_ba_1718_1.png", AssetPath: "assets/image/secretbox/icon/s_ba_1718_1.png",
FixedFlag: false, FixedFlag: false,
BackSide: false, BackSide: false,
BannerID: 101151, BannerID: 101151,
StartDate: "2013-04-15 00:00:00", StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59", EndDate: "2037-12-31 23:59:59",
AddUnitStartDate: "2022-01-01 00:00:00",
}, },
{ {
BannerType: 1, BannerType: 1,
TargetID: 1741, TargetID: 1719,
AssetPath: "assets/image/secretbox/icon/s_ba_1719_1.png", AssetPath: "assets/image/secretbox/icon/s_ba_1719_1.png",
FixedFlag: false, FixedFlag: false,
BackSide: false, BackSide: false,
BannerID: 101150, BannerID: 101150,
StartDate: "2013-04-15 00:00:00", StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59", EndDate: "2037-12-31 23:59:59",
AddUnitStartDate: "2022-01-01 00:00:00",
}, },
{ {
BannerType: 1, BannerType: 1,
TargetID: 1740, TargetID: 1720,
AssetPath: "assets/image/secretbox/icon/s_ba_1720_1.png", AssetPath: "assets/image/secretbox/icon/s_ba_1720_1.png",
FixedFlag: false, FixedFlag: false,
BackSide: false, BackSide: false,
BannerID: 101149, BannerID: 101149,
StartDate: "2013-04-15 00:00:00", StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59", EndDate: "2037-12-31 23:59:59",
AddUnitStartDate: "2022-01-01 00:00:00",
}, },
{ {
BannerType: 1, BannerType: 1,
TargetID: 1739, TargetID: 1721,
AssetPath: "assets/image/secretbox/icon/s_ba_1721_1.png", AssetPath: "assets/image/secretbox/icon/s_ba_1721_1.png",
FixedFlag: false, FixedFlag: false,
BackSide: false, BackSide: false,
BannerID: 101144, BannerID: 101144,
StartDate: "2013-04-15 00:00:00", StartDate: "2013-04-15 00:00:00",
EndDate: "2037-12-31 23:59:59", EndDate: "2037-12-31 23:59:59",
AddUnitStartDate: "2022-01-01 00:00:00",
}, },
{ {
BannerType: 2, 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/api"
_ "honoka-chan/internal/handler/award" _ "honoka-chan/internal/handler/award"
_ "honoka-chan/internal/handler/background" _ "honoka-chan/internal/handler/background"
_ "honoka-chan/internal/handler/banner"
_ "honoka-chan/internal/handler/download" _ "honoka-chan/internal/handler/download"
_ "honoka-chan/internal/handler/event" _ "honoka-chan/internal/handler/event"
_ "honoka-chan/internal/handler/eventscenario" _ "honoka-chan/internal/handler/eventscenario"
@@ -25,6 +26,7 @@ import (
_ "honoka-chan/internal/handler/profile" _ "honoka-chan/internal/handler/profile"
_ "honoka-chan/internal/handler/reward" _ "honoka-chan/internal/handler/reward"
_ "honoka-chan/internal/handler/scenario" _ "honoka-chan/internal/handler/scenario"
_ "honoka-chan/internal/handler/secretbox"
_ "honoka-chan/internal/handler/subscenario" _ "honoka-chan/internal/handler/subscenario"
_ "honoka-chan/internal/handler/tos" _ "honoka-chan/internal/handler/tos"
_ "honoka-chan/internal/handler/unit" _ "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 package apischema
type ApiReq struct { type ApiReq struct {
Module string `json:"module"` Module string `json:"module"`
UserID int `json:"user_id"` UserID int `json:"user_id"`
Action string `json:"action"` Action string `json:"action"`
Timestamp int64 `json:"timeStamp"` ID int `json:"id"`
SecretBoxID int `json:"secret_box_id"`
Timestamp int64 `json:"timeStamp"`
} }
type ApiResp struct { type ApiResp struct {
+9 -10
View File
@@ -1,16 +1,15 @@
package bannerapischema package bannerapischema
type BannerList struct { type BannerList struct {
BannerType int `json:"banner_type"` BannerType int `json:"banner_type"`
TargetID int `json:"target_id"` TargetID int `json:"target_id"`
AssetPath string `json:"asset_path"` AssetPath string `json:"asset_path"`
FixedFlag bool `json:"fixed_flag"` FixedFlag bool `json:"fixed_flag"`
BackSide bool `json:"back_side"` BackSide bool `json:"back_side"`
BannerID int `json:"banner_id"` BannerID int `json:"banner_id"`
StartDate string `json:"start_date"` StartDate string `json:"start_date"`
EndDate string `json:"end_date"` EndDate string `json:"end_date"`
AddUnitStartDate string `json:"add_unit_start_date,omitempty"` WebviewURL string `json:"webview_url,omitempty"`
WebviewURL string `json:"webview_url,omitempty"`
} }
type ListData struct { 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"`
}