feat(game): add warp pulls
This commit is contained in:
1 parent
0e2f2f71f7
commit
f27cde756c
19 files changed
+8166
-25
No files matched your search
+3
-1
@@ -44,7 +44,9 @@
|
|||||||
"unlock_all_challenges": true,
|
"unlock_all_challenges": true,
|
||||||
"main_character": 8008,
|
"main_character": 8008,
|
||||||
"march_type": 1224,
|
"march_type": 1224,
|
||||||
"skip_missions": []
|
"skip_missions": [],
|
||||||
|
"limited_warp_pools": [],
|
||||||
|
"standard_warp_featured": 1503
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"srtools_file": "data/freesr-data.json",
|
"srtools_file": "data/freesr-data.json",
|
||||||
|
|||||||
@@ -297,6 +297,12 @@ GetActivityScheduleConfigCsReq
|
|||||||
GetActivityScheduleConfigScRsp
|
GetActivityScheduleConfigScRsp
|
||||||
DoGachaCsReq
|
DoGachaCsReq
|
||||||
DoGachaScRsp
|
DoGachaScRsp
|
||||||
|
GetGachaCeilingCsReq
|
||||||
|
GetGachaCeilingScRsp
|
||||||
|
ExchangeGachaCeilingCsReq
|
||||||
|
ExchangeGachaCeilingScRsp
|
||||||
|
SetGachaDecideItemCsReq
|
||||||
|
SetGachaDecideItemScRsp
|
||||||
GetMailCsReq
|
GetMailCsReq
|
||||||
GetMailScRsp
|
GetMailScRsp
|
||||||
GetPhoneDataCsReq
|
GetPhoneDataCsReq
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"GachaID": 1001,
|
||||||
|
"GachaType": "Normal",
|
||||||
|
"SortID": 99,
|
||||||
|
"StartTime": "",
|
||||||
|
"EndTime": "",
|
||||||
|
"PrefabPath": "UI/Drawcard/GachaPanel/StandardGacha_1001.prefab",
|
||||||
|
"PoolName": {
|
||||||
|
"Hash": 12642828881931173103
|
||||||
|
},
|
||||||
|
"PoolDesc": {
|
||||||
|
"Hash": 1121704090967857799
|
||||||
|
},
|
||||||
|
"PoolDescFTC": {
|
||||||
|
"Hash": 6970163302865048869
|
||||||
|
},
|
||||||
|
"PoolLabelIcon": "SpriteOutput/DrawCardPic/GachaTabIcon/TabIcon_Standard.png",
|
||||||
|
"PoolLabelIconSelected": "SpriteOutput/DrawCardPic/GachaTabIcon/TabIcon_Standard_Selected.png",
|
||||||
|
"TypeTitle": {
|
||||||
|
"Hash": 10012362747660649297
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaID": 4001,
|
||||||
|
"GachaType": "Newbie",
|
||||||
|
"SortID": 1,
|
||||||
|
"StartTime": "",
|
||||||
|
"EndTime": "",
|
||||||
|
"PrefabPath": "UI/Drawcard/GachaPanel/NoviceGacha_4001.prefab",
|
||||||
|
"PoolName": {
|
||||||
|
"Hash": 16076008841484912183
|
||||||
|
},
|
||||||
|
"PoolDesc": {
|
||||||
|
"Hash": 13113094041792250269
|
||||||
|
},
|
||||||
|
"PoolDescFTC": {
|
||||||
|
"Hash": 6682982103413155220
|
||||||
|
},
|
||||||
|
"PoolLabelIcon": "SpriteOutput/DrawCardPic/GachaTabIcon/TabIcon_Novice.png",
|
||||||
|
"PoolLabelIconSelected": "SpriteOutput/DrawCardPic/GachaTabIcon/TabIcon_Novice_Selected.png",
|
||||||
|
"TypeTitle": {
|
||||||
|
"Hash": 12992035607736039415
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaID": 2001,
|
||||||
|
"GachaType": "AvatarUp",
|
||||||
|
"SortID": 2,
|
||||||
|
"StartTime": "",
|
||||||
|
"EndTime": "",
|
||||||
|
"PrefabPath": "UI/Drawcard/GachaPanelLimited/AvatarGacha_1102.prefab",
|
||||||
|
"PoolName": {
|
||||||
|
"Hash": 10466444443325460861
|
||||||
|
},
|
||||||
|
"PoolDesc": {
|
||||||
|
"Hash": 12575760539773716760
|
||||||
|
},
|
||||||
|
"PoolDescFTC": {
|
||||||
|
"Hash": 9461473220466468630
|
||||||
|
},
|
||||||
|
"PoolLabelIcon": "SpriteOutput/DrawCardPic/GachaTabIconLimited/TabIcon_1102.png",
|
||||||
|
"PoolLabelIconSelected": "SpriteOutput/DrawCardPic/GachaTabIconLimited/TabIcon_1102_Selected.png",
|
||||||
|
"TypeTitle": {
|
||||||
|
"Hash": 16335421618088345187
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaID": 3001,
|
||||||
|
"GachaType": "WeaponUp",
|
||||||
|
"SortID": 4,
|
||||||
|
"StartTime": "",
|
||||||
|
"EndTime": "",
|
||||||
|
"PrefabPath": "UI/Drawcard/GachaPanelLimited/LightConeGacha_23001.prefab",
|
||||||
|
"PoolName": {
|
||||||
|
"Hash": 10051276577110162368
|
||||||
|
},
|
||||||
|
"PoolDesc": {
|
||||||
|
"Hash": 13567216986870304529
|
||||||
|
},
|
||||||
|
"PoolDescFTC": {
|
||||||
|
"Hash": 12441093686588219336
|
||||||
|
},
|
||||||
|
"PoolLabelIcon": "SpriteOutput/DrawCardPic/GachaTabIconLimited/TabIcon_LightCone_23001.png",
|
||||||
|
"PoolLabelIconSelected": "SpriteOutput/DrawCardPic/GachaTabIconLimited/TabIcon_LightCone_23001_Selected.png",
|
||||||
|
"TypeTitle": {
|
||||||
|
"Hash": 689370920993170148
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaID": 2002,
|
||||||
|
"GachaType": "AvatarUp",
|
||||||
|
"SortID": 3,
|
||||||
|
"StartTime": "",
|
||||||
|
"EndTime": "",
|
||||||
|
"PrefabPath": "UI/Drawcard/GachaPanelLimited/AvatarGacha_1204.prefab",
|
||||||
|
"PoolName": {
|
||||||
|
"Hash": 9253952357381402356
|
||||||
|
},
|
||||||
|
"PoolDesc": {
|
||||||
|
"Hash": 12575760539773716760
|
||||||
|
},
|
||||||
|
"PoolDescFTC": {
|
||||||
|
"Hash": 9461473220466468630
|
||||||
|
},
|
||||||
|
"PoolLabelIcon": "SpriteOutput/DrawCardPic/GachaTabIconLimited/TabIcon_1204.png",
|
||||||
|
"PoolLabelIconSelected": "SpriteOutput/DrawCardPic/GachaTabIconLimited/TabIcon_1204_Selected.png",
|
||||||
|
"TypeTitle": {
|
||||||
|
"Hash": 16335421618088345187
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaID": 3002,
|
||||||
|
"GachaType": "WeaponUp",
|
||||||
|
"SortID": 5,
|
||||||
|
"StartTime": "",
|
||||||
|
"EndTime": "",
|
||||||
|
"PrefabPath": "UI/Drawcard/GachaPanelLimited/LightConeGacha_23010.prefab",
|
||||||
|
"PoolName": {
|
||||||
|
"Hash": 3268726668422334764
|
||||||
|
},
|
||||||
|
"PoolDesc": {
|
||||||
|
"Hash": 13567216986870304529
|
||||||
|
},
|
||||||
|
"PoolDescFTC": {
|
||||||
|
"Hash": 12441093686588219336
|
||||||
|
},
|
||||||
|
"PoolLabelIcon": "SpriteOutput/DrawCardPic/GachaTabIconLimited/TabIcon_LightCone_23010.png",
|
||||||
|
"PoolLabelIconSelected": "SpriteOutput/DrawCardPic/GachaTabIconLimited/TabIcon_LightCone_23010_Selected.png",
|
||||||
|
"TypeTitle": {
|
||||||
|
"Hash": 689370920993170148
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,535 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"ID": 1,
|
||||||
|
"RewardItem": 300011,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 2,
|
||||||
|
"RewardItem": 1,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 3,
|
||||||
|
"RewardItem": 102,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 4,
|
||||||
|
"RewardItem": 300003,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 5,
|
||||||
|
"RewardItem": 300002,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 6,
|
||||||
|
"RewardItem": 200103,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 7,
|
||||||
|
"RewardItem": 200104,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 8,
|
||||||
|
"RewardItem": 1,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 9,
|
||||||
|
"RewardItem": 201,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 10,
|
||||||
|
"RewardItem": 200101,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 11,
|
||||||
|
"RewardItem": 200102,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 12,
|
||||||
|
"RewardItem": 200107,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 13,
|
||||||
|
"RewardItem": 200108,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 14,
|
||||||
|
"RewardItem": 200109,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 15,
|
||||||
|
"RewardItem": 200110,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 16,
|
||||||
|
"RewardItem": 200111,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 17,
|
||||||
|
"RewardItem": 200112,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 18,
|
||||||
|
"RewardItem": 200113,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 19,
|
||||||
|
"RewardItem": 200114,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 20,
|
||||||
|
"RewardItem": 1,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 21,
|
||||||
|
"RewardItem": 236,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 22,
|
||||||
|
"RewardItem": 102,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 23,
|
||||||
|
"RewardItem": 300011,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 24,
|
||||||
|
"RewardItem": 300003,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 25,
|
||||||
|
"RewardItem": 201,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 26,
|
||||||
|
"RewardItem": 241,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 27,
|
||||||
|
"RewardItem": 213,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 28,
|
||||||
|
"RewardItem": 223,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 29,
|
||||||
|
"RewardItem": 235,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 30,
|
||||||
|
"RewardItem": 200115,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 31,
|
||||||
|
"RewardItem": 200116,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 32,
|
||||||
|
"RewardItem": 110101,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 33,
|
||||||
|
"RewardItem": 200117,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 34,
|
||||||
|
"RewardItem": 200118,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 35,
|
||||||
|
"RewardItem": 200119,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 36,
|
||||||
|
"RewardItem": 200120,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 37,
|
||||||
|
"RewardItem": 200121,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 38,
|
||||||
|
"RewardItem": 200122,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 39,
|
||||||
|
"RewardItem": 200123,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 40,
|
||||||
|
"RewardItem": 200124,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 41,
|
||||||
|
"RewardItem": 200125,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 42,
|
||||||
|
"RewardItem": 200126,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 43,
|
||||||
|
"RewardItem": 200127,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 44,
|
||||||
|
"RewardItem": 200128,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 45,
|
||||||
|
"RewardItem": 200129,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 46,
|
||||||
|
"RewardItem": 200130,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 47,
|
||||||
|
"RewardItem": 200131,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 48,
|
||||||
|
"RewardItem": 200132,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 49,
|
||||||
|
"RewardItem": 200133,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 50,
|
||||||
|
"RewardItem": 200134,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 51,
|
||||||
|
"RewardItem": 200135,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 52,
|
||||||
|
"RewardItem": 200136,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 53,
|
||||||
|
"RewardItem": 200137,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 54,
|
||||||
|
"RewardItem": 200138,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 55,
|
||||||
|
"RewardItem": 200139,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 56,
|
||||||
|
"RewardItem": 200140,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 57,
|
||||||
|
"RewardItem": 200141,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 58,
|
||||||
|
"RewardItem": 200142,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 59,
|
||||||
|
"RewardItem": 200143,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 60,
|
||||||
|
"RewardItem": 200144,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 61,
|
||||||
|
"RewardItem": 200145,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 62,
|
||||||
|
"RewardItem": 200146,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 63,
|
||||||
|
"RewardItem": 200147,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 64,
|
||||||
|
"RewardItem": 200148,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 65,
|
||||||
|
"RewardItem": 200149,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 66,
|
||||||
|
"RewardItem": 200150,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 67,
|
||||||
|
"RewardItem": 200151,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 68,
|
||||||
|
"RewardItem": 200152,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 69,
|
||||||
|
"RewardItem": 200153,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 70,
|
||||||
|
"RewardItem": 200154,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 71,
|
||||||
|
"RewardItem": 200155,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 72,
|
||||||
|
"RewardItem": 200156,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 73,
|
||||||
|
"RewardItem": 200157,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 74,
|
||||||
|
"RewardItem": 200158,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 75,
|
||||||
|
"RewardItem": 200159,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 76,
|
||||||
|
"RewardItem": 200160,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 77,
|
||||||
|
"RewardItem": 200161,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 78,
|
||||||
|
"RewardItem": 200162,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 501,
|
||||||
|
"RewardItem": 238,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 502,
|
||||||
|
"RewardItem": 237,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 503,
|
||||||
|
"RewardItem": 300012,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 504,
|
||||||
|
"RewardItem": 283,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 505,
|
||||||
|
"RewardItem": 300014,
|
||||||
|
"RewardIcon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1001,
|
||||||
|
"RewardItem": 21052,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1002,
|
||||||
|
"RewardItem": 21028,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1003,
|
||||||
|
"RewardItem": 21029,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1004,
|
||||||
|
"RewardItem": 21030,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1005,
|
||||||
|
"RewardItem": 21031,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1006,
|
||||||
|
"RewardItem": 21032,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1007,
|
||||||
|
"RewardItem": 21033,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1008,
|
||||||
|
"RewardItem": 21034,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1009,
|
||||||
|
"RewardItem": 21057,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1010,
|
||||||
|
"RewardItem": 21055,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1011,
|
||||||
|
"RewardItem": 21061,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1012,
|
||||||
|
"RewardItem": 21053,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1013,
|
||||||
|
"RewardItem": 21062,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1014,
|
||||||
|
"RewardItem": 21056,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1015,
|
||||||
|
"RewardItem": 21058,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1016,
|
||||||
|
"RewardItem": 21060,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": 1017,
|
||||||
|
"RewardItem": 21065,
|
||||||
|
"RewardIcon": "",
|
||||||
|
"NumShow": true
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,72 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"GachaTypeID": "Normal",
|
||||||
|
"ItemCosume": 101,
|
||||||
|
"ItemPrice": 160,
|
||||||
|
"DiamondID": 1,
|
||||||
|
"GachaBar": "StandardGachaPage",
|
||||||
|
"BuyPos": {
|
||||||
|
"ShopID": 1000,
|
||||||
|
"ShopGoodID": 1000001
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaTypeID": "Newbie",
|
||||||
|
"ItemCosume": 101,
|
||||||
|
"ItemPrice": 160,
|
||||||
|
"DiamondID": 1,
|
||||||
|
"GachaBar": "StandardGachaPage",
|
||||||
|
"BuyPos": {
|
||||||
|
"ShopID": 1000,
|
||||||
|
"ShopGoodID": 1000001
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaTypeID": "AvatarUp",
|
||||||
|
"UpPropability": 50,
|
||||||
|
"ItemCosume": 102,
|
||||||
|
"ItemPrice": 160,
|
||||||
|
"DiamondID": 1,
|
||||||
|
"GachaBar": "CharacterGachaPage",
|
||||||
|
"BuyPos": {
|
||||||
|
"ShopID": 1000,
|
||||||
|
"ShopGoodID": 1000002
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaTypeID": "WeaponUp",
|
||||||
|
"UpPropability": 75,
|
||||||
|
"ItemCosume": 102,
|
||||||
|
"ItemPrice": 160,
|
||||||
|
"DiamondID": 1,
|
||||||
|
"GachaBar": "LightconeGachaPage",
|
||||||
|
"BuyPos": {
|
||||||
|
"ShopID": 1000,
|
||||||
|
"ShopGoodID": 1000002
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaTypeID": "CollaborationAvatarUp",
|
||||||
|
"UpPropability": 50,
|
||||||
|
"ItemCosume": 102,
|
||||||
|
"ItemPrice": 160,
|
||||||
|
"DiamondID": 1,
|
||||||
|
"GachaBar": "CharacterGachaPage",
|
||||||
|
"BuyPos": {
|
||||||
|
"ShopID": 1000,
|
||||||
|
"ShopGoodID": 1000002
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GachaTypeID": "CollaborationWeaponUp",
|
||||||
|
"UpPropability": 75,
|
||||||
|
"ItemCosume": 102,
|
||||||
|
"ItemPrice": 160,
|
||||||
|
"DiamondID": 1,
|
||||||
|
"GachaBar": "LightconeGachaPage",
|
||||||
|
"BuyPos": {
|
||||||
|
"ShopID": 1000,
|
||||||
|
"ShopGoodID": 1000002
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -105,6 +105,25 @@ bool Config::load(const std::string& path) {
|
|||||||
pick(*it, "main_character", gameplay.mainCharacter);
|
pick(*it, "main_character", gameplay.mainCharacter);
|
||||||
pick(*it, "march_type", gameplay.marchType);
|
pick(*it, "march_type", gameplay.marchType);
|
||||||
pick(*it, "skip_missions", gameplay.skipMissions);
|
pick(*it, "skip_missions", gameplay.skipMissions);
|
||||||
|
// Each entry is a pool id, or {"id": pool, "featured": 5* id}.
|
||||||
|
if (auto pools = it->find("limited_warp_pools"); pools != it->end() && pools->is_array()) {
|
||||||
|
gameplay.limitedWarpPools.clear();
|
||||||
|
for (const json& entry : *pools) {
|
||||||
|
WarpBanner banner;
|
||||||
|
if (entry.is_number_unsigned()) {
|
||||||
|
banner.id = entry.get<uint32_t>();
|
||||||
|
} else if (entry.is_object()) {
|
||||||
|
pick(entry, "id", banner.id);
|
||||||
|
pick(entry, "featured", banner.featured);
|
||||||
|
}
|
||||||
|
if (banner.id == 0) {
|
||||||
|
logging::warn("config", "a limited_warp_pools entry has no pool id");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gameplay.limitedWarpPools.push_back(banner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pick(*it, "standard_warp_featured", gameplay.standardWarpFeatured);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ struct PathsConfig {
|
|||||||
std::string resources = "resources";
|
std::string resources = "resources";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct WarpBanner {
|
||||||
|
uint32_t id = 0;
|
||||||
|
uint32_t featured = 0; // 0 keeps the pool's own
|
||||||
|
};
|
||||||
|
|
||||||
struct GameplayConfig {
|
struct GameplayConfig {
|
||||||
// auto = a srtools build replaces calyx fights and nothing else; srtools = it
|
// auto = a srtools build replaces calyx fights and nothing else; srtools = it
|
||||||
// replaces every fight; stage = never, always use the stage the client asked for.
|
// replaces every fight; stage = never, always use the stage the client asked for.
|
||||||
@@ -74,6 +79,13 @@ struct GameplayConfig {
|
|||||||
// loading screen when entering at login, a fade that rolls straight back otherwise.
|
// loading screen when entering at login, a fade that rolls straight back otherwise.
|
||||||
// Main and sub mission ids both go here.
|
// Main and sub mission ids both go here.
|
||||||
std::vector<uint32_t> skipMissions;
|
std::vector<uint32_t> skipMissions;
|
||||||
|
// Limited warp pools offered next to the standard one, each optionally featuring another
|
||||||
|
// 5*. The beta clients list 2001, 2002, 3001 and 3002 but ship no banner art for any of
|
||||||
|
// them, and one pool without art turns the whole warp page black, so leave it empty there.
|
||||||
|
std::vector<WarpBanner> limitedWarpPools;
|
||||||
|
// A 5* character Stellar Warp features, 50/50 and guarantee included, standing in for
|
||||||
|
// the current banner the beta clients have no art for. 0 keeps it the standard pool.
|
||||||
|
uint32_t standardWarpFeatured = 0;
|
||||||
|
|
||||||
bool missionSkipped(uint32_t id) const {
|
bool missionSkipped(uint32_t id) const {
|
||||||
return std::find(skipMissions.begin(), skipMissions.end(), id) != skipMissions.end();
|
return std::find(skipMissions.begin(), skipMissions.end(), id) != skipMissions.end();
|
||||||
|
|||||||
+108
-4
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
@@ -77,6 +78,35 @@ std::vector<uint32_t> u32List(const json& j, const char* key) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "CombatPowerAvatarRarityType5", "CombatPowerLightconeRarity4": the stars are the last digit.
|
||||||
|
uint32_t rarityOf(const json& row) {
|
||||||
|
auto it = row.find("Rarity");
|
||||||
|
if (it == row.end()) return 0;
|
||||||
|
if (it->is_number()) return static_cast<uint32_t>(numberOf(*it));
|
||||||
|
if (!it->is_string()) return 0;
|
||||||
|
const std::string& name = it->get_ref<const std::string&>();
|
||||||
|
char last = name.empty() ? '\0' : name.back();
|
||||||
|
return (last >= '1' && last <= '9') ? static_cast<uint32_t>(last - '0') : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ".../AvatarGacha_1102.prefab" -> 1102. The Sub* prefabs belong to a grouped pool and
|
||||||
|
// give 0.
|
||||||
|
uint32_t featuredFromPrefab(const std::string& path, std::string_view stem) {
|
||||||
|
size_t slash = path.find_last_of('/');
|
||||||
|
std::string_view file(path);
|
||||||
|
if (slash != std::string::npos) file.remove_prefix(slash + 1);
|
||||||
|
constexpr std::string_view suffix = ".prefab";
|
||||||
|
if (file.size() <= stem.size() + suffix.size() || file.substr(0, stem.size()) != stem ||
|
||||||
|
file.substr(file.size() - suffix.size()) != suffix) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::string_view digits = file.substr(stem.size(), file.size() - stem.size() - suffix.size());
|
||||||
|
if (digits.size() > 9 || digits.find_first_not_of("0123456789") != std::string_view::npos) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return static_cast<uint32_t>(std::strtoul(std::string(digits).c_str(), nullptr, 10));
|
||||||
|
}
|
||||||
|
|
||||||
// AvatarSkillTreeConfig.AnchorType is "Point07"; the client wants the number.
|
// AvatarSkillTreeConfig.AnchorType is "Point07"; the client wants the number.
|
||||||
uint32_t anchorNumber(const std::string& anchor) {
|
uint32_t anchorNumber(const std::string& anchor) {
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
@@ -180,6 +210,23 @@ const AvatarInfo* Tables::avatar(uint32_t id) const {
|
|||||||
return it == avatars_.end() ? nullptr : &it->second;
|
return it == avatars_.end() ? nullptr : &it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LightconeInfo* Tables::lightcone(uint32_t id) const {
|
||||||
|
auto it = lightcones_.find(id);
|
||||||
|
return it == lightcones_.end() ? nullptr : &it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t Tables::gachaUpChance(GachaType type) const {
|
||||||
|
switch (type) {
|
||||||
|
case GachaType::AvatarUp:
|
||||||
|
return avatarUpChance_;
|
||||||
|
case GachaType::WeaponUp:
|
||||||
|
return weaponUpChance_;
|
||||||
|
case GachaType::Normal:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const StageInfo* Tables::stage(uint32_t id) const {
|
const StageInfo* Tables::stage(uint32_t id) const {
|
||||||
auto it = stages_.find(id);
|
auto it = stages_.find(id);
|
||||||
return it == stages_.end() ? nullptr : &it->second;
|
return it == stages_.end() ? nullptr : &it->second;
|
||||||
@@ -331,6 +378,7 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
|||||||
uint64_t started = util::nowMs();
|
uint64_t started = util::nowMs();
|
||||||
size_t used = 0;
|
size_t used = 0;
|
||||||
bool standardGachaFound = false;
|
bool standardGachaFound = false;
|
||||||
|
std::unordered_map<std::string, uint32_t> upChances;
|
||||||
|
|
||||||
for (const std::string& dir : sources) {
|
for (const std::string& dir : sources) {
|
||||||
json doc;
|
json doc;
|
||||||
@@ -356,6 +404,7 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
|||||||
info.baseType = str(*row, "AvatarBaseType");
|
info.baseType = str(*row, "AvatarBaseType");
|
||||||
info.maxPromotion = u32(*row, "MaxPromotion", 6);
|
info.maxPromotion = u32(*row, "MaxPromotion", 6);
|
||||||
info.maxRank = u32(*row, "MaxRank", 6);
|
info.maxRank = u32(*row, "MaxRank", 6);
|
||||||
|
info.rarity = rarityOf(*row);
|
||||||
// SPNeed is in whole points; SpBarInfo counts hundredths.
|
// SPNeed is in whole points; SpBarInfo counts hundredths.
|
||||||
uint32_t spNeed = u32(*row, "SPNeed");
|
uint32_t spNeed = u32(*row, "SPNeed");
|
||||||
info.spNeed = spNeed != 0 ? spNeed * 100 : 10000;
|
info.spNeed = spNeed != 0 ? spNeed * 100 : 10000;
|
||||||
@@ -503,12 +552,48 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
|||||||
farmStages_.insert(stageId);
|
farmStages_.insert(stageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const json* row : table("GachaBasicInfo.json")) {
|
for (const json* row : table("GachaTypeBasicInfo.json")) {
|
||||||
if (standardGachaFound || str(*row, "GachaType") != "Normal") continue;
|
std::string type = str(*row, "GachaTypeID");
|
||||||
|
if (!type.empty()) upChances.emplace(type, u32(*row, "UpPropability"));
|
||||||
|
}
|
||||||
|
// The client throws on a pool its own table lacks, and the beta table is a six-row
|
||||||
|
// stub, so here the last source with the table wins instead of the first.
|
||||||
|
const std::vector<const json*>& gachaRows = table("GachaBasicInfo.json");
|
||||||
|
if (!gachaRows.empty()) {
|
||||||
|
gachaPools_.clear();
|
||||||
|
standardGachaFound = false;
|
||||||
|
}
|
||||||
|
for (const json* row : gachaRows) {
|
||||||
uint32_t id = u32(*row, "GachaID");
|
uint32_t id = u32(*row, "GachaID");
|
||||||
if (id == 0) continue;
|
if (id == 0) continue;
|
||||||
standardGacha_.gachaId = id;
|
std::string type = str(*row, "GachaType");
|
||||||
standardGachaFound = true;
|
GachaPool pool;
|
||||||
|
pool.id = id;
|
||||||
|
if (type == "Normal") {
|
||||||
|
if (standardGachaFound && id != standardGacha_.gachaId) continue;
|
||||||
|
standardGacha_.gachaId = id;
|
||||||
|
standardGachaFound = true;
|
||||||
|
} else if (type == "AvatarUp") {
|
||||||
|
pool.type = GachaType::AvatarUp;
|
||||||
|
pool.featured = featuredFromPrefab(str(*row, "PrefabPath"), "AvatarGacha_");
|
||||||
|
} else if (type == "WeaponUp") {
|
||||||
|
pool.type = GachaType::WeaponUp;
|
||||||
|
pool.featured = featuredFromPrefab(str(*row, "PrefabPath"), "LightConeGacha_");
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pool.type != GachaType::Normal && pool.featured == 0) continue;
|
||||||
|
gachaPools_.push_back(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const json* row : table("EquipmentConfig.json")) {
|
||||||
|
uint32_t id = u32(*row, "EquipmentID");
|
||||||
|
if (id == 0 || lightcones_.count(id) != 0) continue;
|
||||||
|
lightcones_.emplace(id, LightconeInfo{id, rarityOf(*row)});
|
||||||
|
}
|
||||||
|
for (const json* row : table("BattlePassReward.json")) {
|
||||||
|
uint32_t item = u32(*row, "RewardItem");
|
||||||
|
if (item != 0) battlePassRewards_.insert(item);
|
||||||
}
|
}
|
||||||
for (const json* row : table("GachaCeiling.json")) {
|
for (const json* row : table("GachaCeiling.json")) {
|
||||||
if (!standardGacha_.ceilingAvatars.empty() || str(*row, "GachaType") != "Normal") {
|
if (!standardGacha_.ceilingAvatars.empty() || str(*row, "GachaType") != "Normal") {
|
||||||
@@ -744,6 +829,24 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
|||||||
sortById(challenges_);
|
sortById(challenges_);
|
||||||
sortById(challengeGroups_);
|
sortById(challengeGroups_);
|
||||||
sortById(peakGroups_);
|
sortById(peakGroups_);
|
||||||
|
sortById(gachaPools_);
|
||||||
|
|
||||||
|
// The defaults are GachaTypeBasicInfo's own numbers, for when the table is missing.
|
||||||
|
if (auto it = upChances.find("AvatarUp"); it != upChances.end()) avatarUpChance_ = it->second;
|
||||||
|
if (auto it = upChances.find("WeaponUp"); it != upChances.end()) weaponUpChance_ = it->second;
|
||||||
|
for (GachaPool& pool : gachaPools_) pool.upChance = gachaUpChance(pool.type);
|
||||||
|
// A featured 5* the tables do not have cannot be drawn.
|
||||||
|
std::erase_if(gachaPools_, [&](const GachaPool& pool) {
|
||||||
|
if (pool.type == GachaType::AvatarUp) {
|
||||||
|
const AvatarInfo* info = avatar(pool.featured);
|
||||||
|
return info == nullptr || info->rarity != 5;
|
||||||
|
}
|
||||||
|
if (pool.type == GachaType::WeaponUp) {
|
||||||
|
const LightconeInfo* info = lightcone(pool.featured);
|
||||||
|
return info == nullptr || info->rarity != 5;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
// Each fight learns its season, and plants its one monster over the arena's marker.
|
// Each fight learns its season, and plants its one monster over the arena's marker.
|
||||||
for (const PeakGroupInfo& group : peakGroups_) {
|
for (const PeakGroupInfo& group : peakGroups_) {
|
||||||
@@ -779,6 +882,7 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
|||||||
entrances_.size(), planeEvents_.size(), used, util::nowMs() - started);
|
entrances_.size(), planeEvents_.size(), used, util::nowMs() - started);
|
||||||
logging::info("data", "{} challenge floors in {} seasons, {} of them with a third node",
|
logging::info("data", "{} challenge floors in {} seasons, {} of them with a third node",
|
||||||
challenges_.size(), challengeGroups_.size(), challengeTierces_.size());
|
challenges_.size(), challengeGroups_.size(), challengeTierces_.size());
|
||||||
|
logging::info("data", "{} lightcones, {} warp pools", lightcones_.size(), gachaPools_.size());
|
||||||
logging::info("data", "{} main missions, {} tutorials, {} guides, {} quests",
|
logging::info("data", "{} main missions, {} tutorials, {} guides, {} quests",
|
||||||
mainMissions_.size(), tutorials_.size(), tutorialGuides_.size(),
|
mainMissions_.size(), tutorials_.size(), tutorialGuides_.size(),
|
||||||
quests_.size());
|
quests_.size());
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct AvatarInfo {
|
|||||||
uint32_t spNeed = 10000; // hundredths, as the SpBarInfo wants it
|
uint32_t spNeed = 10000; // hundredths, as the SpBarInfo wants it
|
||||||
uint32_t maxPromotion = 6;
|
uint32_t maxPromotion = 6;
|
||||||
uint32_t maxRank = 6;
|
uint32_t maxRank = 6;
|
||||||
|
uint32_t rarity = 0;
|
||||||
uint32_t enhancedId = 0;
|
uint32_t enhancedId = 0;
|
||||||
uint32_t attackBuffId = 0;
|
uint32_t attackBuffId = 0;
|
||||||
// AvatarDefaultMazeBuff; falls back to id * 100 + 1 when the row is missing.
|
// AvatarDefaultMazeBuff; falls back to id * 100 + 1 when the row is missing.
|
||||||
@@ -25,6 +26,21 @@ struct AvatarInfo {
|
|||||||
uint32_t techniqueSkillIndex = 2;
|
uint32_t techniqueSkillIndex = 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct LightconeInfo {
|
||||||
|
uint32_t id = 0;
|
||||||
|
uint32_t rarity = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class GachaType { Normal, AvatarUp, WeaponUp };
|
||||||
|
|
||||||
|
// A warp pool. A limited one is named after its featured 5* in its prefab.
|
||||||
|
struct GachaPool {
|
||||||
|
uint32_t id = 0;
|
||||||
|
GachaType type = GachaType::Normal;
|
||||||
|
uint32_t featured = 0;
|
||||||
|
uint32_t upChance = 0; // percent
|
||||||
|
};
|
||||||
|
|
||||||
struct SkillTreePoint {
|
struct SkillTreePoint {
|
||||||
uint32_t pointId = 0;
|
uint32_t pointId = 0;
|
||||||
uint32_t avatarId = 0;
|
uint32_t avatarId = 0;
|
||||||
@@ -247,6 +263,15 @@ public:
|
|||||||
uint32_t farmElementStage(uint32_t idOrStage, uint32_t worldLevel) const;
|
uint32_t farmElementStage(uint32_t idOrStage, uint32_t worldLevel) const;
|
||||||
|
|
||||||
const StandardGacha& standardGacha() const { return standardGacha_; }
|
const StandardGacha& standardGacha() const { return standardGacha_; }
|
||||||
|
// The standard pool, then the limited ones in id order.
|
||||||
|
const std::vector<GachaPool>& gachaPools() const { return gachaPools_; }
|
||||||
|
// GachaTypeBasicInfo.UpPropability, in percent; 0 for the standard pool.
|
||||||
|
uint32_t gachaUpChance(GachaType type) const;
|
||||||
|
|
||||||
|
const LightconeInfo* lightcone(uint32_t id) const;
|
||||||
|
const std::unordered_map<uint32_t, LightconeInfo>& lightcones() const { return lightcones_; }
|
||||||
|
// BattlePassReward: paid out by the pass, never by a warp.
|
||||||
|
bool battlePassReward(uint32_t itemId) const { return battlePassRewards_.count(itemId) != 0; }
|
||||||
|
|
||||||
// AvatarMazeBuff rows named ADV_GlobalSkill_Maze*: Castorice and Silver Wolf.
|
// AvatarMazeBuff rows named ADV_GlobalSkill_Maze*: Castorice and Silver Wolf.
|
||||||
const std::vector<uint32_t>& globalMazeBuffs() const { return globalMazeBuffs_; }
|
const std::vector<uint32_t>& globalMazeBuffs() const { return globalMazeBuffs_; }
|
||||||
@@ -300,6 +325,11 @@ private:
|
|||||||
std::unordered_map<uint64_t, uint32_t> farmElements_; // id * 100 + worldLevel -> stage
|
std::unordered_map<uint64_t, uint32_t> farmElements_; // id * 100 + worldLevel -> stage
|
||||||
std::unordered_set<uint32_t> farmStages_;
|
std::unordered_set<uint32_t> farmStages_;
|
||||||
StandardGacha standardGacha_;
|
StandardGacha standardGacha_;
|
||||||
|
std::vector<GachaPool> gachaPools_;
|
||||||
|
uint32_t avatarUpChance_ = 50;
|
||||||
|
uint32_t weaponUpChance_ = 75;
|
||||||
|
std::unordered_map<uint32_t, LightconeInfo> lightcones_;
|
||||||
|
std::unordered_set<uint32_t> battlePassRewards_;
|
||||||
std::vector<uint32_t> globalMazeBuffs_;
|
std::vector<uint32_t> globalMazeBuffs_;
|
||||||
std::vector<uint32_t> mainMissions_;
|
std::vector<uint32_t> mainMissions_;
|
||||||
std::vector<uint32_t> tutorials_;
|
std::vector<uint32_t> tutorials_;
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
#include "game/gacha.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "core/util.h"
|
||||||
|
|
||||||
|
namespace game {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Stellar Warp's 5* light cones; no table lists them.
|
||||||
|
constexpr uint32_t kStandardLightcones[] = {23000, 23002, 23003, 23004, 23005, 23012, 23013};
|
||||||
|
|
||||||
|
constexpr uint32_t kUndyingEmbers = 251;
|
||||||
|
constexpr uint32_t kUndyingStarlight = 252;
|
||||||
|
constexpr uint32_t kEidolonItemOffset = 10000;
|
||||||
|
constexpr uint32_t kFourStarPity = 10;
|
||||||
|
|
||||||
|
struct Rates {
|
||||||
|
uint32_t hardPity;
|
||||||
|
uint32_t softPity;
|
||||||
|
double base;
|
||||||
|
double step;
|
||||||
|
double fourStar;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 0.6%, +6% a pull from 74, certain at 90. Light cones: 0.8%, +7% from 66, certain at 80.
|
||||||
|
constexpr Rates kCharacterRates{90, 74, 0.006, 0.06, 0.051};
|
||||||
|
constexpr Rates kLightconeRates{80, 66, 0.008, 0.07, 0.066};
|
||||||
|
|
||||||
|
double fiveStarChance(const Rates& rates, uint32_t pullNumber) {
|
||||||
|
if (pullNumber >= rates.hardPity) return 1.0;
|
||||||
|
if (pullNumber < rates.softPity) return rates.base;
|
||||||
|
return std::min(1.0, rates.base + rates.step * (pullNumber - rates.softPity + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t pick(const std::vector<uint32_t>& ids, const GachaRoll& roll) {
|
||||||
|
size_t index = static_cast<size_t>(roll() * static_cast<double>(ids.size()));
|
||||||
|
return ids[std::min(index, ids.size() - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
GachaPull pullOne(const data::GachaPool& pool, const GachaItemPools& items, GachaPity& pity,
|
||||||
|
const GachaRoll& roll) {
|
||||||
|
const Rates& rates = pool.type == data::GachaType::WeaponUp ? kLightconeRates : kCharacterRates;
|
||||||
|
++pity.total;
|
||||||
|
GachaPull pull;
|
||||||
|
|
||||||
|
if (roll() < fiveStarChance(rates, pity.sinceFive + 1)) {
|
||||||
|
pull.rarity = 5;
|
||||||
|
pity.sinceFive = 0;
|
||||||
|
// A 5* does not use up the 4* guarantee; it lands on the next pull instead.
|
||||||
|
++pity.sinceFour;
|
||||||
|
if (pool.type == data::GachaType::Normal) {
|
||||||
|
pull.avatar = roll() < 0.5;
|
||||||
|
pull.itemId = pick(pull.avatar ? items.fiveStarAvatars : items.fiveStarLightcones, roll);
|
||||||
|
return pull;
|
||||||
|
}
|
||||||
|
pull.avatar = pool.type == data::GachaType::AvatarUp;
|
||||||
|
std::vector<uint32_t> others;
|
||||||
|
for (uint32_t id : pull.avatar ? items.fiveStarAvatars : items.fiveStarLightcones) {
|
||||||
|
if (id != pool.featured) others.push_back(id);
|
||||||
|
}
|
||||||
|
pull.guaranteed = pity.guaranteed;
|
||||||
|
pull.featured = pity.guaranteed || others.empty() || roll() * 100.0 < pool.upChance;
|
||||||
|
pity.guaranteed = !pull.featured;
|
||||||
|
pull.itemId = pull.featured ? pool.featured : pick(others, roll);
|
||||||
|
return pull;
|
||||||
|
}
|
||||||
|
|
||||||
|
++pity.sinceFive;
|
||||||
|
if (pity.sinceFour + 1 >= kFourStarPity || roll() < rates.fourStar) {
|
||||||
|
pull.rarity = 4;
|
||||||
|
pity.sinceFour = 0;
|
||||||
|
pull.avatar = roll() < 0.5;
|
||||||
|
pull.itemId = pick(pull.avatar ? items.fourStarAvatars : items.fourStarLightcones, roll);
|
||||||
|
return pull;
|
||||||
|
}
|
||||||
|
|
||||||
|
++pity.sinceFour;
|
||||||
|
pull.itemId = pick(items.threeStarLightcones, roll);
|
||||||
|
return pull;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
GachaPity& GachaProgress::forType(data::GachaType type) {
|
||||||
|
switch (type) {
|
||||||
|
case data::GachaType::AvatarUp:
|
||||||
|
return character;
|
||||||
|
case data::GachaType::WeaponUp:
|
||||||
|
return lightcone;
|
||||||
|
case data::GachaType::Normal:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return standard;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GachaItemPools::complete() const {
|
||||||
|
return !fiveStarAvatars.empty() && !fiveStarLightcones.empty() && !fourStarAvatars.empty() &&
|
||||||
|
!fourStarLightcones.empty() && !threeStarLightcones.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
GachaItemPools gachaItemPools(const data::Tables& tables) {
|
||||||
|
GachaItemPools items;
|
||||||
|
items.fiveStarAvatars = tables.standardGacha().ceilingAvatars;
|
||||||
|
for (uint32_t id : kStandardLightcones) {
|
||||||
|
const data::LightconeInfo* info = tables.lightcone(id);
|
||||||
|
if (info != nullptr && info->rarity == 5) items.fiveStarLightcones.push_back(id);
|
||||||
|
}
|
||||||
|
for (const auto& [id, avatar] : tables.avatars()) {
|
||||||
|
// Playable ids, with March's second form and the Trailblazer's paths folded away.
|
||||||
|
if (avatar.rarity != 4 || id < 1000 || id >= 2000 || tables.baseAvatarId(id) != id) continue;
|
||||||
|
items.fourStarAvatars.push_back(id);
|
||||||
|
}
|
||||||
|
for (const auto& [id, lightcone] : tables.lightcones()) {
|
||||||
|
// 22xxx are event rewards.
|
||||||
|
if (id >= 22000 || tables.battlePassReward(id)) continue;
|
||||||
|
if (lightcone.rarity == 4) items.fourStarLightcones.push_back(id);
|
||||||
|
if (lightcone.rarity == 3) items.threeStarLightcones.push_back(id);
|
||||||
|
}
|
||||||
|
std::sort(items.fourStarAvatars.begin(), items.fourStarAvatars.end());
|
||||||
|
std::sort(items.fourStarLightcones.begin(), items.fourStarLightcones.end());
|
||||||
|
std::sort(items.threeStarLightcones.begin(), items.threeStarLightcones.end());
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
double gachaRoll() { return static_cast<double>(util::randomU64() >> 11) * 0x1.0p-53; }
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool featurable(const data::Tables& tables, data::GachaType type, uint32_t id) {
|
||||||
|
if (type == data::GachaType::AvatarUp) {
|
||||||
|
const data::AvatarInfo* avatar = tables.avatar(id);
|
||||||
|
return avatar != nullptr && avatar->rarity == 5;
|
||||||
|
}
|
||||||
|
if (type == data::GachaType::WeaponUp) {
|
||||||
|
const data::LightconeInfo* lightcone = tables.lightcone(id);
|
||||||
|
return lightcone != nullptr && lightcone->rarity == 5;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<data::GachaPool> offeredGachaPools(const data::Tables& tables,
|
||||||
|
const std::vector<core::WarpBanner>& limited) {
|
||||||
|
const std::vector<data::GachaPool>& pools = tables.gachaPools();
|
||||||
|
auto find = [&pools](uint32_t id) -> const data::GachaPool* {
|
||||||
|
for (const data::GachaPool& pool : pools) {
|
||||||
|
if (pool.id == id) return &pool;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<data::GachaPool> offered;
|
||||||
|
const data::GachaPool* standard = find(tables.standardGacha().gachaId);
|
||||||
|
if (standard != nullptr && standard->type == data::GachaType::Normal) offered.push_back(*standard);
|
||||||
|
for (const core::WarpBanner& banner : limited) {
|
||||||
|
const data::GachaPool* pool = find(banner.id);
|
||||||
|
if (pool == nullptr || pool->type == data::GachaType::Normal) continue;
|
||||||
|
bool taken = std::any_of(offered.begin(), offered.end(),
|
||||||
|
[pool](const data::GachaPool& p) { return p.id == pool->id; });
|
||||||
|
if (taken) continue;
|
||||||
|
data::GachaPool entry = *pool;
|
||||||
|
if (banner.featured != 0 && featurable(tables, entry.type, banner.featured)) {
|
||||||
|
entry.featured = banner.featured;
|
||||||
|
}
|
||||||
|
offered.push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
return offered;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<data::GachaPool> offeredGachaPool(const data::Tables& tables,
|
||||||
|
const std::vector<core::WarpBanner>& limited,
|
||||||
|
uint32_t gachaId) {
|
||||||
|
for (data::GachaPool& pool : offeredGachaPools(tables, limited)) {
|
||||||
|
if (pool.id == gachaId) return std::move(pool);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
data::GachaPool effectiveGachaPool(const data::GachaPool& pool, const data::Tables& tables,
|
||||||
|
uint32_t standardFeatured) {
|
||||||
|
if (pool.type != data::GachaType::Normal || standardFeatured == 0) return pool;
|
||||||
|
if (!featurable(tables, data::GachaType::AvatarUp, standardFeatured)) return pool;
|
||||||
|
data::GachaPool banner = pool;
|
||||||
|
banner.type = data::GachaType::AvatarUp;
|
||||||
|
banner.featured = standardFeatured;
|
||||||
|
banner.upChance = tables.gachaUpChance(data::GachaType::AvatarUp);
|
||||||
|
return banner;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GachaPull> pullGacha(const data::GachaPool& pool, const GachaItemPools& items,
|
||||||
|
GachaPity& pity, uint32_t count, const GachaRoll& roll) {
|
||||||
|
std::vector<GachaPull> pulls;
|
||||||
|
pulls.reserve(count);
|
||||||
|
for (uint32_t i = 0; i < count; ++i) pulls.push_back(pullOne(pool, items, pity, roll));
|
||||||
|
return pulls;
|
||||||
|
}
|
||||||
|
|
||||||
|
proto::GachaItem gachaResult(const GachaPull& pull, const SrToolsData& roster) {
|
||||||
|
proto::GachaItem result;
|
||||||
|
proto::Item& item = result.gacha_item.emplace();
|
||||||
|
item.item_id = pull.itemId;
|
||||||
|
item.num = 1;
|
||||||
|
item.level = 1;
|
||||||
|
item.rank = 1;
|
||||||
|
// Both lists go out even when empty.
|
||||||
|
proto::ItemList& transfer = result.transfer_item_list.emplace();
|
||||||
|
proto::ItemList& tokens = result.token_item.emplace();
|
||||||
|
auto token = [&tokens](uint32_t id, uint32_t num) {
|
||||||
|
proto::Item entry;
|
||||||
|
entry.item_id = id;
|
||||||
|
entry.num = num;
|
||||||
|
tokens.item_list.push_back(entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pull.avatar) {
|
||||||
|
if (pull.rarity == 3) {
|
||||||
|
token(kUndyingEmbers, 20);
|
||||||
|
} else {
|
||||||
|
token(kUndyingStarlight, pull.rarity == 5 ? 40 : 8);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto owned = roster.avatars.find(pull.itemId);
|
||||||
|
if (owned == roster.avatars.end()) {
|
||||||
|
result.is_new = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
bool fiveStar = pull.rarity == 5;
|
||||||
|
if (owned->second.rank >= 6) {
|
||||||
|
token(kUndyingStarlight, fiveStar ? 100 : 20);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
token(kUndyingStarlight, fiveStar ? 40 : 8);
|
||||||
|
proto::Item eidolon;
|
||||||
|
eidolon.item_id = pull.itemId + kEidolonItemOffset;
|
||||||
|
eidolon.num = 1;
|
||||||
|
transfer.item_list.push_back(eidolon);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace game
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/config.h"
|
||||||
|
#include "data/excel.h"
|
||||||
|
#include "game/srtools.h"
|
||||||
|
#include "proto/gen/protos.h"
|
||||||
|
|
||||||
|
namespace game {
|
||||||
|
|
||||||
|
struct GachaPity {
|
||||||
|
uint32_t sinceFive = 0;
|
||||||
|
uint32_t sinceFour = 0;
|
||||||
|
bool guaranteed = false; // the next 5* is the featured one
|
||||||
|
uint32_t total = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Every limited pool of one kind shares its counter.
|
||||||
|
struct GachaProgress {
|
||||||
|
GachaPity standard;
|
||||||
|
GachaPity character;
|
||||||
|
GachaPity lightcone;
|
||||||
|
|
||||||
|
GachaPity& forType(data::GachaType type);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GachaItemPools {
|
||||||
|
std::vector<uint32_t> fiveStarAvatars;
|
||||||
|
std::vector<uint32_t> fiveStarLightcones;
|
||||||
|
std::vector<uint32_t> fourStarAvatars;
|
||||||
|
std::vector<uint32_t> fourStarLightcones;
|
||||||
|
std::vector<uint32_t> threeStarLightcones;
|
||||||
|
|
||||||
|
bool complete() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
GachaItemPools gachaItemPools(const data::Tables& tables);
|
||||||
|
|
||||||
|
struct GachaPull {
|
||||||
|
uint32_t itemId = 0;
|
||||||
|
uint32_t rarity = 3;
|
||||||
|
bool avatar = false;
|
||||||
|
bool featured = false; // a limited 5* that was the featured one
|
||||||
|
bool guaranteed = false; // and was owed it by an earlier lost 50/50
|
||||||
|
};
|
||||||
|
|
||||||
|
// Uniform in [0, 1); injectable so the rates can be tested.
|
||||||
|
using GachaRoll = std::function<double()>;
|
||||||
|
double gachaRoll();
|
||||||
|
|
||||||
|
// The standard pool, then each configured limited pool the tables have, featuring what the
|
||||||
|
// config names. An override that is not a 5* of the pool's kind is ignored.
|
||||||
|
std::vector<data::GachaPool> offeredGachaPools(const data::Tables& tables,
|
||||||
|
const std::vector<core::WarpBanner>& limited);
|
||||||
|
std::optional<data::GachaPool> offeredGachaPool(const data::Tables& tables,
|
||||||
|
const std::vector<core::WarpBanner>& limited,
|
||||||
|
uint32_t gachaId);
|
||||||
|
// The standard pool as a character banner featuring `standardFeatured`. Anything but a 5*
|
||||||
|
// character leaves the pool as it is, and so does any limited pool.
|
||||||
|
data::GachaPool effectiveGachaPool(const data::GachaPool& pool, const data::Tables& tables,
|
||||||
|
uint32_t standardFeatured);
|
||||||
|
|
||||||
|
// `items` must be complete().
|
||||||
|
std::vector<GachaPull> pullGacha(const data::GachaPool& pool, const GachaItemPools& items,
|
||||||
|
GachaPity& pity, uint32_t count, const GachaRoll& roll);
|
||||||
|
|
||||||
|
// The result card. Nothing is granted: the srtools build stays the roster.
|
||||||
|
proto::GachaItem gachaResult(const GachaPull& pull, const SrToolsData& roster);
|
||||||
|
|
||||||
|
} // namespace game
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/config.h"
|
||||||
|
#include "core/logger.h"
|
||||||
|
#include "core/util.h"
|
||||||
|
#include "data/excel.h"
|
||||||
|
#include "game/gacha.h"
|
||||||
|
#include "game/handlers.h"
|
||||||
|
#include "game/player.h"
|
||||||
|
#include "net/cmd_ids.h"
|
||||||
|
#include "net/handler.h"
|
||||||
|
#include "net/session.h"
|
||||||
|
#include "proto/gen/protos.h"
|
||||||
|
|
||||||
|
namespace game {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int64_t kLimitedEndTime = 4102416000; // 2100-01-01
|
||||||
|
// Character pools carry this in item_detail_list.
|
||||||
|
constexpr uint32_t kAvatarPoolDetail = 11;
|
||||||
|
|
||||||
|
// Always claimed: nothing is granted, so the 300-pull pick has nothing to pay out.
|
||||||
|
proto::GachaCeiling standardCeiling(const Player* player) {
|
||||||
|
const data::StandardGacha& standard = data::Tables::get().standardGacha();
|
||||||
|
proto::GachaCeiling ceiling;
|
||||||
|
ceiling.is_claimed = true;
|
||||||
|
uint32_t pulls = player != nullptr ? player->gacha().standard.total : 0;
|
||||||
|
ceiling.ceiling_num = std::min(pulls, standard.ceilingNum);
|
||||||
|
for (uint32_t avatarId : standard.ceilingAvatars) {
|
||||||
|
proto::GachaCeilingAvatar avatar;
|
||||||
|
avatar.avatar_id = avatarId;
|
||||||
|
ceiling.avatar_list.push_back(avatar);
|
||||||
|
}
|
||||||
|
return ceiling;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onGetGachaInfo(net::Session& session, const proto::GetGachaInfoCsReq&) {
|
||||||
|
proto::GetGachaInfoScRsp rsp;
|
||||||
|
rsp.retcode = 0;
|
||||||
|
rsp.gacha_random = util::randomRange(1000, 1999);
|
||||||
|
const std::vector<core::WarpBanner>& limited = core::Config::get().gameplay.limitedWarpPools;
|
||||||
|
for (const data::GachaPool& pool : offeredGachaPools(data::Tables::get(), limited)) {
|
||||||
|
proto::GachaInfo& info = rsp.gacha_info_list.emplace_back();
|
||||||
|
info.gacha_id = pool.id;
|
||||||
|
if (pool.type == data::GachaType::Normal) {
|
||||||
|
info.gacha_ceiling.emplace() = standardCeiling(session.player());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
info.end_time = kLimitedEndTime;
|
||||||
|
info.prize_item_list.push_back(pool.featured);
|
||||||
|
if (pool.type == data::GachaType::AvatarUp) info.item_detail_list.push_back(kAvatarPoolDetail);
|
||||||
|
}
|
||||||
|
session.send(cmd::GetGachaInfoScRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDoGacha(net::Session& session, const proto::DoGachaCsReq& req) {
|
||||||
|
proto::DoGachaScRsp rsp;
|
||||||
|
rsp.gacha_id = req.gacha_id;
|
||||||
|
rsp.gacha_num = req.gacha_num;
|
||||||
|
|
||||||
|
Player* player = session.player();
|
||||||
|
const data::Tables& tables = data::Tables::get();
|
||||||
|
std::optional<data::GachaPool> pool =
|
||||||
|
offeredGachaPool(tables, core::Config::get().gameplay.limitedWarpPools, req.gacha_id);
|
||||||
|
if (player == nullptr) {
|
||||||
|
rsp.retcode = fail(proto::Retcode::RET_FAIL);
|
||||||
|
} else if (!pool) {
|
||||||
|
rsp.retcode = fail(proto::Retcode::RET_GACHA_ID_NOT_EXIST);
|
||||||
|
} else if (req.gacha_num != 1 && req.gacha_num != 10) {
|
||||||
|
rsp.retcode = fail(proto::Retcode::RET_GACHA_NUM_INVALID);
|
||||||
|
} else {
|
||||||
|
GachaItemPools items = gachaItemPools(tables);
|
||||||
|
if (!items.complete()) {
|
||||||
|
logging::warn("game", "warp {} has an empty item pool, check the tables", pool->id);
|
||||||
|
rsp.retcode = fail(proto::Retcode::RET_GACHA_NOT_SUPPORT);
|
||||||
|
} else {
|
||||||
|
uint32_t featured = core::Config::get().gameplay.standardWarpFeatured;
|
||||||
|
data::GachaPool banner = effectiveGachaPool(*pool, tables, featured);
|
||||||
|
if (pool->type == data::GachaType::Normal && featured != 0 && banner.featured == 0) {
|
||||||
|
logging::warn("game", "standard_warp_featured {} is not a 5* character", featured);
|
||||||
|
}
|
||||||
|
Roster roster = player->roster();
|
||||||
|
GachaPity& pity = player->gacha().forType(banner.type);
|
||||||
|
uint32_t fiveStars = 0;
|
||||||
|
for (const GachaPull& pull : pullGacha(banner, items, pity, req.gacha_num, gachaRoll)) {
|
||||||
|
if (pull.rarity == 5) {
|
||||||
|
++fiveStars;
|
||||||
|
const char* how = banner.type == data::GachaType::Normal ? "standard"
|
||||||
|
: pull.guaranteed ? "guaranteed"
|
||||||
|
: pull.featured ? "won the 50/50"
|
||||||
|
: "lost the 50/50";
|
||||||
|
logging::info("game", "warp {} 5*: {} ({})", banner.id, pull.itemId, how);
|
||||||
|
}
|
||||||
|
rsp.gacha_item_list.push_back(gachaResult(pull, roster.data()));
|
||||||
|
}
|
||||||
|
player->saveNow();
|
||||||
|
logging::info("game", "warp {} x{}: {} 5*, {} pulls since the last", banner.id,
|
||||||
|
req.gacha_num, fiveStars, pity.sinceFive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.send(cmd::DoGachaScRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onGetGachaCeiling(net::Session& session, const proto::GetGachaCeilingCsReq& req) {
|
||||||
|
proto::GetGachaCeilingScRsp rsp;
|
||||||
|
rsp.retcode = 0;
|
||||||
|
rsp.gacha_type = req.DDMCNOJFGON;
|
||||||
|
rsp.gacha_ceiling.emplace() = standardCeiling(session.player());
|
||||||
|
session.send(cmd::GetGachaCeilingScRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onExchangeGachaCeiling(net::Session& session, const proto::ExchangeGachaCeilingCsReq& req) {
|
||||||
|
proto::ExchangeGachaCeilingScRsp rsp;
|
||||||
|
rsp.retcode = fail(proto::Retcode::RET_GACHA_CEILING_CLOSE);
|
||||||
|
rsp.gacha_type = req.gacha_type;
|
||||||
|
rsp.avatar_id = req.avatar_id;
|
||||||
|
rsp.gacha_ceiling.emplace() = standardCeiling(session.player());
|
||||||
|
rsp.transfer_item_list.emplace();
|
||||||
|
session.send(cmd::ExchangeGachaCeilingScRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A lost 50/50 always draws from the standard seven, so a custom pick is refused.
|
||||||
|
void onSetGachaDecideItem(net::Session& session, const proto::SetGachaDecideItemCsReq&) {
|
||||||
|
proto::SetGachaDecideItemScRsp rsp;
|
||||||
|
rsp.retcode = fail(proto::Retcode::RET_GACHA_NOT_SUPPORT);
|
||||||
|
rsp.NBLOJLDLBEB.emplace();
|
||||||
|
session.send(cmd::SetGachaDecideItemScRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void registerGachaHandlers() {
|
||||||
|
net::on<proto::GetGachaInfoCsReq>(cmd::GetGachaInfoCsReq, onGetGachaInfo);
|
||||||
|
net::on<proto::DoGachaCsReq>(cmd::DoGachaCsReq, onDoGacha);
|
||||||
|
net::on<proto::GetGachaCeilingCsReq>(cmd::GetGachaCeilingCsReq, onGetGachaCeiling);
|
||||||
|
net::on<proto::ExchangeGachaCeilingCsReq>(cmd::ExchangeGachaCeilingCsReq, onExchangeGachaCeiling);
|
||||||
|
net::on<proto::SetGachaDecideItemCsReq>(cmd::SetGachaDecideItemCsReq, onSetGachaDecideItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace game
|
||||||
@@ -23,6 +23,7 @@ void registerTalkHandlers();
|
|||||||
void registerChallengeHandlers();
|
void registerChallengeHandlers();
|
||||||
void registerTierceHandlers();
|
void registerTierceHandlers();
|
||||||
void registerPeakHandlers();
|
void registerPeakHandlers();
|
||||||
|
void registerGachaHandlers();
|
||||||
void registerModuleHandlers();
|
void registerModuleHandlers();
|
||||||
void registerMiscHandlers();
|
void registerMiscHandlers();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include "core/logger.h"
|
#include "core/logger.h"
|
||||||
#include "core/util.h"
|
#include "core/util.h"
|
||||||
#include "data/excel.h"
|
|
||||||
#include "game/handlers.h"
|
#include "game/handlers.h"
|
||||||
#include "net/cmd_ids.h"
|
#include "net/cmd_ids.h"
|
||||||
#include "net/handler.h"
|
#include "net/handler.h"
|
||||||
@@ -77,24 +76,6 @@ void onSyncClientResVersion(net::Session& session, const proto::SyncClientResVer
|
|||||||
session.send(cmd::SyncClientResVersionScRsp, rsp);
|
session.send(cmd::SyncClientResVersionScRsp, rsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The warp pools. Only the standard pool is offered, and its ceiling as already
|
|
||||||
// claimed: nothing can pay a warp out yet.
|
|
||||||
void onGetGachaInfo(net::Session& session, const proto::GetGachaInfoCsReq&) {
|
|
||||||
const data::StandardGacha& standard = data::Tables::get().standardGacha();
|
|
||||||
proto::GetGachaInfoScRsp rsp;
|
|
||||||
rsp.retcode = 0;
|
|
||||||
auto& pool = rsp.gacha_info_list.emplace_back();
|
|
||||||
pool.gacha_id = standard.gachaId;
|
|
||||||
auto& ceiling = pool.gacha_ceiling.emplace();
|
|
||||||
ceiling.is_claimed = true;
|
|
||||||
for (uint32_t avatarId : standard.ceilingAvatars) {
|
|
||||||
proto::GachaCeilingAvatar avatar;
|
|
||||||
avatar.avatar_id = avatarId;
|
|
||||||
ceiling.avatar_list.push_back(avatar);
|
|
||||||
}
|
|
||||||
session.send(cmd::GetGachaInfoScRsp, rsp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activities reported as scheduled: module id, panel and window.
|
// Activities reported as scheduled: module id, panel and window.
|
||||||
struct ScheduledActivity {
|
struct ScheduledActivity {
|
||||||
uint32_t activityId;
|
uint32_t activityId;
|
||||||
@@ -207,7 +188,6 @@ void registerMiscHandlers() {
|
|||||||
for (uint16_t cmdId : kEmptyReplies) net::Handlers::get().addEmpty(cmdId);
|
for (uint16_t cmdId : kEmptyReplies) net::Handlers::get().addEmpty(cmdId);
|
||||||
for (const auto& [req, rsp] : kResponseAliases) net::Handlers::get().addAlias(req, rsp);
|
for (const auto& [req, rsp] : kResponseAliases) net::Handlers::get().addAlias(req, rsp);
|
||||||
net::on<proto::SetClientPausedCsReq>(cmd::SetClientPausedCsReq, onSetClientPaused);
|
net::on<proto::SetClientPausedCsReq>(cmd::SetClientPausedCsReq, onSetClientPaused);
|
||||||
net::on<proto::GetGachaInfoCsReq>(cmd::GetGachaInfoCsReq, onGetGachaInfo);
|
|
||||||
net::on<proto::GetActivityScheduleConfigCsReq>(cmd::GetActivityScheduleConfigCsReq,
|
net::on<proto::GetActivityScheduleConfigCsReq>(cmd::GetActivityScheduleConfigCsReq,
|
||||||
onGetActivityScheduleConfig);
|
onGetActivityScheduleConfig);
|
||||||
net::on<proto::SyncClientResVersionCsReq>(cmd::SyncClientResVersionCsReq,
|
net::on<proto::SyncClientResVersionCsReq>(cmd::SyncClientResVersionCsReq,
|
||||||
@@ -226,6 +206,7 @@ void registerAllHandlers() {
|
|||||||
registerChallengeHandlers();
|
registerChallengeHandlers();
|
||||||
registerTierceHandlers();
|
registerTierceHandlers();
|
||||||
registerPeakHandlers();
|
registerPeakHandlers();
|
||||||
|
registerGachaHandlers();
|
||||||
registerModuleHandlers();
|
registerModuleHandlers();
|
||||||
registerMiscHandlers();
|
registerMiscHandlers();
|
||||||
logging::info("game", "{} packet handlers registered, {} answered empty",
|
logging::info("game", "{} packet handlers registered, {} answered empty",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/gacha.h"
|
||||||
#include "game/lineup.h"
|
#include "game/lineup.h"
|
||||||
#include "game/roster.h"
|
#include "game/roster.h"
|
||||||
#include "game/scene.h"
|
#include "game/scene.h"
|
||||||
@@ -168,6 +169,8 @@ public:
|
|||||||
const std::map<uint32_t, TierceProgress>& tierceHistory() const { return tierceHistory_; }
|
const std::map<uint32_t, TierceProgress>& tierceHistory() const { return tierceHistory_; }
|
||||||
PeakProgress& peakProgress() { return peakProgress_; }
|
PeakProgress& peakProgress() { return peakProgress_; }
|
||||||
const PeakProgress& peakProgress() const { return peakProgress_; }
|
const PeakProgress& peakProgress() const { return peakProgress_; }
|
||||||
|
GachaProgress& gacha() { return gacha_; }
|
||||||
|
const GachaProgress& gacha() const { return gacha_; }
|
||||||
|
|
||||||
uint32_t mainCharacter() const { return mainCharacter_; }
|
uint32_t mainCharacter() const { return mainCharacter_; }
|
||||||
void setMainCharacter(uint32_t value) { mainCharacter_ = value; }
|
void setMainCharacter(uint32_t value) { mainCharacter_ = value; }
|
||||||
@@ -209,6 +212,7 @@ private:
|
|||||||
PeakProgress peakProgress_;
|
PeakProgress peakProgress_;
|
||||||
TierceRun tierce_;
|
TierceRun tierce_;
|
||||||
std::map<uint32_t, TierceProgress> tierceHistory_;
|
std::map<uint32_t, TierceProgress> tierceHistory_;
|
||||||
|
GachaProgress gacha_;
|
||||||
|
|
||||||
uint32_t mainCharacter_ = 8008;
|
uint32_t mainCharacter_ = 8008;
|
||||||
uint32_t marchType_ = 1224;
|
uint32_t marchType_ = 1224;
|
||||||
|
|||||||
@@ -171,6 +171,20 @@ bool loadPlayerState(Player& player) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (auto gacha = j.find("gacha"); gacha != j.end() && gacha->is_object()) {
|
||||||
|
auto readPity = [&gacha](const char* key, GachaPity& pity) {
|
||||||
|
auto it = gacha->find(key);
|
||||||
|
if (it == gacha->end() || !it->is_object()) return;
|
||||||
|
pity.sinceFive = u32(*it, "since_five", 0);
|
||||||
|
pity.sinceFour = u32(*it, "since_four", 0);
|
||||||
|
pity.guaranteed = boolOr(*it, "guaranteed", false);
|
||||||
|
pity.total = u32(*it, "total", 0);
|
||||||
|
};
|
||||||
|
readPity("standard", player.gacha().standard);
|
||||||
|
readPity("character", player.gacha().character);
|
||||||
|
readPity("lightcone", player.gacha().lightcone);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +258,17 @@ bool savePlayerState(const Player& player) {
|
|||||||
}
|
}
|
||||||
j["tierce"] = tierce;
|
j["tierce"] = tierce;
|
||||||
|
|
||||||
|
auto pity = [](const GachaPity& p) {
|
||||||
|
return json{{"since_five", p.sinceFive},
|
||||||
|
{"since_four", p.sinceFour},
|
||||||
|
{"guaranteed", p.guaranteed},
|
||||||
|
{"total", p.total}};
|
||||||
|
};
|
||||||
|
const GachaProgress& gacha = player.gacha();
|
||||||
|
j["gacha"] = {{"standard", pity(gacha.standard)},
|
||||||
|
{"character", pity(gacha.character)},
|
||||||
|
{"lightcone", pity(gacha.lightcone)}};
|
||||||
|
|
||||||
const std::string& path = core::Config::get().paths.playerFile;
|
const std::string& path = core::Config::get().paths.playerFile;
|
||||||
if (!util::writeFile(path, j.dump(2))) {
|
if (!util::writeFile(path, j.dump(2))) {
|
||||||
logging::warn("player", "could not write {}", path);
|
logging::warn("player", "could not write {}", path);
|
||||||
|
|||||||
@@ -982,6 +982,85 @@ void runFlowTests() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warp: results only, and every refusal still answers.
|
||||||
|
std::vector<core::WarpBanner> configuredPools = core::Config::get().gameplay.limitedWarpPools;
|
||||||
|
core::Config::get().gameplay.limitedWarpPools.clear();
|
||||||
|
client.sendEmpty(cmd::GetGachaInfoCsReq);
|
||||||
|
check(client.await(cmd::GetGachaInfoScRsp, packet), "warp pools answered");
|
||||||
|
proto::GetGachaInfoScRsp standardInfo;
|
||||||
|
check(parseBody(packet, standardInfo) && standardInfo.gacha_info_list.size() == 1 &&
|
||||||
|
standardInfo.gacha_info_list[0].gacha_id == 1001,
|
||||||
|
"only the standard pool by default");
|
||||||
|
|
||||||
|
core::Config::get().gameplay.limitedWarpPools = {{2002, 0}, {3002, 0}};
|
||||||
|
client.sendEmpty(cmd::GetGachaInfoCsReq);
|
||||||
|
check(client.await(cmd::GetGachaInfoScRsp, packet), "and again with limited pools configured");
|
||||||
|
proto::GetGachaInfoScRsp gachaInfo;
|
||||||
|
check(parseBody(packet, gachaInfo), "the pools parse");
|
||||||
|
check(gachaInfo.gacha_info_list.size() == 3, "the standard pool and the two configured ones");
|
||||||
|
check(!gachaInfo.gacha_info_list.empty() && gachaInfo.gacha_info_list[0].gacha_ceiling.has() &&
|
||||||
|
gachaInfo.gacha_info_list[0].gacha_ceiling->avatar_list.size() == 7,
|
||||||
|
"the standard pool carries its ceiling");
|
||||||
|
uint32_t limitedId = 0;
|
||||||
|
if (gachaInfo.gacha_info_list.size() > 1) {
|
||||||
|
limitedId = gachaInfo.gacha_info_list[1].gacha_id;
|
||||||
|
check(gachaInfo.gacha_info_list[1].prize_item_list.size() == 1,
|
||||||
|
"a limited pool names its featured 5*");
|
||||||
|
}
|
||||||
|
|
||||||
|
proto::DoGachaCsReq tenPull;
|
||||||
|
tenPull.gacha_id = limitedId;
|
||||||
|
tenPull.gacha_num = 10;
|
||||||
|
tenPull.gacha_random = gachaInfo.gacha_random;
|
||||||
|
client.send(cmd::DoGachaCsReq, tenPull);
|
||||||
|
check(client.await(cmd::DoGachaScRsp, packet), "a ten pull answers");
|
||||||
|
proto::DoGachaScRsp tenRsp;
|
||||||
|
check(parseBody(packet, tenRsp) && tenRsp.retcode == 0, "and succeeds");
|
||||||
|
check(tenRsp.gacha_id == limitedId && tenRsp.gacha_num == 10 &&
|
||||||
|
tenRsp.gacha_item_list.size() == 10,
|
||||||
|
"with ten results");
|
||||||
|
bool cardsWhole = !tenRsp.gacha_item_list.empty();
|
||||||
|
for (const proto::GachaItem& card : tenRsp.gacha_item_list) {
|
||||||
|
cardsWhole &= card.gacha_item.has() && card.gacha_item->item_id != 0 &&
|
||||||
|
card.transfer_item_list.has() && card.token_item.has();
|
||||||
|
}
|
||||||
|
check(cardsWhole, "every card is filled in");
|
||||||
|
|
||||||
|
proto::DoGachaCsReq oddPull = tenPull;
|
||||||
|
oddPull.gacha_num = 5;
|
||||||
|
client.send(cmd::DoGachaCsReq, oddPull);
|
||||||
|
proto::DoGachaScRsp oddRsp;
|
||||||
|
check(client.await(cmd::DoGachaScRsp, packet) && parseBody(packet, oddRsp) &&
|
||||||
|
oddRsp.retcode == static_cast<uint32_t>(proto::Retcode::RET_GACHA_NUM_INVALID) &&
|
||||||
|
oddRsp.gacha_item_list.empty(),
|
||||||
|
"a five pull is refused");
|
||||||
|
|
||||||
|
proto::DoGachaCsReq oldPool;
|
||||||
|
oldPool.gacha_id = 2138;
|
||||||
|
oldPool.gacha_num = 1;
|
||||||
|
client.send(cmd::DoGachaCsReq, oldPool);
|
||||||
|
proto::DoGachaScRsp oldRsp;
|
||||||
|
check(client.await(cmd::DoGachaScRsp, packet) && parseBody(packet, oldRsp) &&
|
||||||
|
oldRsp.retcode == static_cast<uint32_t>(proto::Retcode::RET_GACHA_ID_NOT_EXIST),
|
||||||
|
"a pool that is not offered is refused");
|
||||||
|
|
||||||
|
proto::GetGachaCeilingCsReq ceilingReq;
|
||||||
|
ceilingReq.DDMCNOJFGON = 1;
|
||||||
|
client.send(cmd::GetGachaCeilingCsReq, ceilingReq);
|
||||||
|
proto::GetGachaCeilingScRsp ceilingRsp;
|
||||||
|
check(client.await(cmd::GetGachaCeilingScRsp, packet) && parseBody(packet, ceilingRsp) &&
|
||||||
|
ceilingRsp.gacha_ceiling.has() && ceilingRsp.gacha_ceiling->is_claimed,
|
||||||
|
"the ceiling answers, claimed");
|
||||||
|
|
||||||
|
proto::SetGachaDecideItemCsReq decideReq;
|
||||||
|
decideReq.gacha_id = limitedId;
|
||||||
|
client.send(cmd::SetGachaDecideItemCsReq, decideReq);
|
||||||
|
proto::SetGachaDecideItemScRsp decideRsp;
|
||||||
|
check(client.await(cmd::SetGachaDecideItemScRsp, packet) && parseBody(packet, decideRsp) &&
|
||||||
|
decideRsp.retcode != 0 && decideRsp.NBLOJLDLBEB.has(),
|
||||||
|
"a custom 50/50 pool is refused with a body");
|
||||||
|
core::Config::get().gameplay.limitedWarpPools = configuredPools;
|
||||||
|
|
||||||
// An unimplemented request still has to complete, or the client hangs on it. This
|
// An unimplemented request still has to complete, or the client hangs on it. This
|
||||||
// one has no handler at all, so it exercises the name-derived fallback.
|
// one has no handler at all, so it exercises the name-derived fallback.
|
||||||
client.sendEmpty(cmd::GetStarFightDataCsReq);
|
client.sendEmpty(cmd::GetStarFightDataCsReq);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// present; the data-driven checks are skipped (and reported) when they are not.
|
// present; the data-driven checks are skipped (and reported) when they are not.
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -13,8 +15,10 @@
|
|||||||
#include "data/scene_res.h"
|
#include "data/scene_res.h"
|
||||||
#include "game/battle.h"
|
#include "game/battle.h"
|
||||||
#include "game/challenge.h"
|
#include "game/challenge.h"
|
||||||
|
#include "game/gacha.h"
|
||||||
#include "game/lineup.h"
|
#include "game/lineup.h"
|
||||||
#include "game/player.h"
|
#include "game/player.h"
|
||||||
|
#include "game/player_store.h"
|
||||||
#include "game/roster.h"
|
#include "game/roster.h"
|
||||||
#include "game/scene.h"
|
#include "game/scene.h"
|
||||||
#include "game/srtools.h"
|
#include "game/srtools.h"
|
||||||
@@ -186,6 +190,220 @@ void testTables() {
|
|||||||
check(tables.plane(10000) != nullptr, "plane 10000 is loaded");
|
check(tables.plane(10000) != nullptr, "plane 10000 is loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool contains(const std::vector<uint32_t>& ids, uint32_t id) {
|
||||||
|
return std::find(ids.begin(), ids.end(), id) != ids.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t tokenCount(const proto::GachaItem& result, uint32_t itemId) {
|
||||||
|
uint32_t total = 0;
|
||||||
|
if (!result.token_item.has()) return 0;
|
||||||
|
for (const proto::Item& item : result.token_item->item_list) {
|
||||||
|
if (item.item_id == itemId) total += item.num;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testGacha() {
|
||||||
|
const data::Tables& tables = data::Tables::get();
|
||||||
|
if (!tables.loaded()) {
|
||||||
|
std::printf("SKIP gacha (no data source configured)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
check(tables.avatar(1001) != nullptr && tables.avatar(1001)->rarity == 4, "March is a 4*");
|
||||||
|
check(tables.avatar(1003) != nullptr && tables.avatar(1003)->rarity == 5, "Himeko is a 5*");
|
||||||
|
check(tables.lightcone(23000) != nullptr && tables.lightcone(23000)->rarity == 5,
|
||||||
|
"light cone rarity is read");
|
||||||
|
check(tables.lightcone(20000) != nullptr && tables.lightcone(20000)->rarity == 3,
|
||||||
|
"down to the 3* ones");
|
||||||
|
check(tables.battlePassReward(21028) && !tables.battlePassReward(21000),
|
||||||
|
"battle pass light cones are known");
|
||||||
|
|
||||||
|
// The beta client's own table has six pools, and it throws on any other.
|
||||||
|
check(tables.gachaPools().size() == 5, "the client's pools, less the beginner one");
|
||||||
|
std::vector<data::GachaPool> standardOnly = game::offeredGachaPools(tables, {});
|
||||||
|
check(standardOnly.size() == 1 && standardOnly[0].id == 1001,
|
||||||
|
"only the standard pool unless limited ones are configured");
|
||||||
|
const std::vector<core::WarpBanner> limited{{2002, 0}, {3002, 0}, {2002, 0},
|
||||||
|
{2138, 0}, {1001, 0}, {4001, 0}};
|
||||||
|
std::vector<data::GachaPool> offered = game::offeredGachaPools(tables, limited);
|
||||||
|
check(offered.size() == 3, "each configured pool once, if the table has it");
|
||||||
|
check(!offered.empty() && offered[0].id == 1001, "standard first");
|
||||||
|
std::optional<data::GachaPool> character = game::offeredGachaPool(tables, limited, 2002);
|
||||||
|
check(character && character->type == data::GachaType::AvatarUp &&
|
||||||
|
character->featured == 1204 && character->upChance == 50,
|
||||||
|
"2002 features 1204 at 50%");
|
||||||
|
std::optional<data::GachaPool> lightcone = game::offeredGachaPool(tables, limited, 3002);
|
||||||
|
check(lightcone && lightcone->type == data::GachaType::WeaponUp &&
|
||||||
|
lightcone->featured == 23010 && lightcone->upChance == 75,
|
||||||
|
"3002 features 23010 at 75%");
|
||||||
|
check(!game::offeredGachaPool(tables, limited, 2138), "a pool the client lacks is not offered");
|
||||||
|
check(!game::offeredGachaPool(tables, limited, 4001), "nor the beginner pool");
|
||||||
|
check(!game::offeredGachaPool(tables, {}, 2002), "nor an unconfigured one");
|
||||||
|
|
||||||
|
game::GachaItemPools items = game::gachaItemPools(tables);
|
||||||
|
check(items.complete(), "every rarity has something to draw");
|
||||||
|
check(items.fiveStarAvatars.size() == 7 && items.fiveStarLightcones.size() == 7,
|
||||||
|
"seven standard 5* of each");
|
||||||
|
check(contains(items.fourStarAvatars, 1001) && !contains(items.fourStarAvatars, 1224),
|
||||||
|
"4* characters without March's second form");
|
||||||
|
check(!contains(items.fourStarAvatars, 1003), "and without 5*s");
|
||||||
|
check(contains(items.fourStarLightcones, 21000) && !contains(items.fourStarLightcones, 21028) &&
|
||||||
|
!contains(items.fourStarLightcones, 22000),
|
||||||
|
"4* light cones without pass or event ones");
|
||||||
|
check(items.threeStarLightcones.size() == 25, "all 25 3* light cones");
|
||||||
|
if (!items.complete() || !character || !lightcone || offered.empty()) return;
|
||||||
|
|
||||||
|
auto unlucky = [] { return 0.999999; };
|
||||||
|
auto lucky = [] { return 0.0; };
|
||||||
|
|
||||||
|
game::GachaPity pity;
|
||||||
|
std::vector<game::GachaPull> pulls = game::pullGacha(offered[0], items, pity, 91, unlucky);
|
||||||
|
bool early = false;
|
||||||
|
for (size_t i = 0; i < 89; ++i) early |= pulls[i].rarity == 5;
|
||||||
|
check(!early && pulls[89].rarity == 5, "the 90th standard pull is a 5*");
|
||||||
|
check(pulls[9].rarity == 4 && pulls[19].rarity == 4 && pulls[8].rarity == 3,
|
||||||
|
"every tenth pull is a 4*");
|
||||||
|
check(pulls[90].rarity == 4, "a 5* pushes the due 4* to the next pull");
|
||||||
|
check(pity.sinceFive == 1 && pity.sinceFour == 0 && pity.total == 91, "pity is counted");
|
||||||
|
|
||||||
|
game::GachaPity lcPity;
|
||||||
|
pulls = game::pullGacha(*lightcone, items, lcPity, 80, unlucky);
|
||||||
|
early = false;
|
||||||
|
for (size_t i = 0; i < 79; ++i) early |= pulls[i].rarity == 5;
|
||||||
|
check(!early && pulls[79].rarity == 5, "the 80th light cone pull is a 5*");
|
||||||
|
|
||||||
|
game::GachaPity charPity;
|
||||||
|
pulls = game::pullGacha(*character, items, charPity, 90, unlucky);
|
||||||
|
check(pulls[89].rarity == 5 && pulls[89].avatar && pulls[89].itemId != 1204 &&
|
||||||
|
contains(items.fiveStarAvatars, pulls[89].itemId),
|
||||||
|
"losing the 50/50 gives a standard 5*");
|
||||||
|
check(charPity.guaranteed, "and guarantees the next one");
|
||||||
|
pulls = game::pullGacha(*character, items, charPity, 90, unlucky);
|
||||||
|
check(pulls[89].itemId == 1204 && !charPity.guaranteed, "which is the featured 5*");
|
||||||
|
|
||||||
|
game::GachaPity luckyPity;
|
||||||
|
pulls = game::pullGacha(*character, items, luckyPity, 10, lucky);
|
||||||
|
bool allFeatured = pulls.size() == 10;
|
||||||
|
for (const game::GachaPull& pull : pulls) allFeatured &= pull.rarity == 5 && pull.itemId == 1204;
|
||||||
|
check(allFeatured, "winning every roll gives the featured 5* every time");
|
||||||
|
pulls = game::pullGacha(offered[0], items, luckyPity, 1, lucky);
|
||||||
|
check(pulls[0].rarity == 5 && pulls[0].itemId == items.fiveStarAvatars[0], "standard draws too");
|
||||||
|
|
||||||
|
game::SrToolsData roster;
|
||||||
|
game::Avatar maxed;
|
||||||
|
maxed.avatarId = 1003;
|
||||||
|
maxed.rank = 6;
|
||||||
|
roster.avatars[1003] = maxed;
|
||||||
|
game::Avatar fresh;
|
||||||
|
fresh.avatarId = 1004;
|
||||||
|
roster.avatars[1004] = fresh;
|
||||||
|
|
||||||
|
proto::GachaItem result = game::gachaResult({20000, 3, false}, roster);
|
||||||
|
check(result.gacha_item.has() && result.gacha_item->item_id == 20000 && result.gacha_item->num == 1,
|
||||||
|
"the card names the item");
|
||||||
|
check(result.transfer_item_list.has() && result.token_item.has(), "both lists are present");
|
||||||
|
check(tokenCount(result, 251) == 20 && !result.is_new, "a 3* light cone is 20 embers");
|
||||||
|
check(tokenCount(game::gachaResult({23000, 5, false}, roster), 252) == 40, "a 5* one 40 starlight");
|
||||||
|
check(tokenCount(game::gachaResult({21000, 4, false}, roster), 252) == 8, "a 4* one 8");
|
||||||
|
result = game::gachaResult({1003, 5, true}, roster);
|
||||||
|
check(tokenCount(result, 252) == 100 && result.transfer_item_list->item_list.empty(),
|
||||||
|
"a 5* past E6 is 100 starlight");
|
||||||
|
result = game::gachaResult({1004, 5, true}, roster);
|
||||||
|
check(tokenCount(result, 252) == 40 && result.transfer_item_list->item_list.size() == 1 &&
|
||||||
|
result.transfer_item_list->item_list[0].item_id == 11004,
|
||||||
|
"below E6 it is 40 and an eidolon");
|
||||||
|
result = game::gachaResult({1101, 5, true}, roster);
|
||||||
|
check(result.is_new && tokenCount(result, 252) == 0, "a character not in the build is new");
|
||||||
|
|
||||||
|
// A configured featured 5* replaces the pool's own, if it is of the pool's kind.
|
||||||
|
std::optional<data::GachaPool> pearl = game::offeredGachaPool(tables, {{2001, 1503}}, 2001);
|
||||||
|
check(pearl && pearl->type == data::GachaType::AvatarUp && pearl->featured == 1503 &&
|
||||||
|
pearl->upChance == 50,
|
||||||
|
"2001 can feature Pearl at 50%");
|
||||||
|
std::optional<data::GachaPool> fourStar = game::offeredGachaPool(tables, {{2001, 1001}}, 2001);
|
||||||
|
check(fourStar && fourStar->featured == 1102, "a 4* cannot be featured");
|
||||||
|
std::optional<data::GachaPool> wrongKind = game::offeredGachaPool(tables, {{3001, 1503}}, 3001);
|
||||||
|
check(wrongKind && wrongKind->featured == 23001, "nor a character on a light cone pool");
|
||||||
|
if (pearl) {
|
||||||
|
game::GachaPity pearlPity;
|
||||||
|
pulls = game::pullGacha(*pearl, items, pearlPity, 10, lucky);
|
||||||
|
bool allPearl = pulls.size() == 10;
|
||||||
|
for (const game::GachaPull& pull : pulls) {
|
||||||
|
allPearl &= pull.rarity == 5 && pull.avatar && pull.itemId == 1503;
|
||||||
|
}
|
||||||
|
check(allPearl, "winning the 50/50 draws her");
|
||||||
|
pulls = game::pullGacha(*pearl, items, pearlPity, 90, unlucky);
|
||||||
|
check(pulls[89].rarity == 5 && pulls[89].avatar && pulls[89].itemId != 1503 &&
|
||||||
|
contains(items.fiveStarAvatars, pulls[89].itemId) && pearlPity.guaranteed,
|
||||||
|
"losing it gives a standard 5* character and the guarantee");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stellar Warp standing in for Pearl's banner.
|
||||||
|
data::GachaPool standardPearl = game::effectiveGachaPool(offered[0], tables, 1503);
|
||||||
|
check(standardPearl.id == 1001 && standardPearl.type == data::GachaType::AvatarUp &&
|
||||||
|
standardPearl.featured == 1503 && standardPearl.upChance == 50,
|
||||||
|
"Stellar Warp can feature Pearl at 50%");
|
||||||
|
check(game::effectiveGachaPool(offered[0], tables, 0).type == data::GachaType::Normal,
|
||||||
|
"0 keeps it the standard pool");
|
||||||
|
check(game::effectiveGachaPool(offered[0], tables, 1001).type == data::GachaType::Normal,
|
||||||
|
"a 4* cannot be featured on it");
|
||||||
|
check(game::effectiveGachaPool(*character, tables, 1503).featured == 1204,
|
||||||
|
"a limited pool is left alone");
|
||||||
|
game::GachaPity standardPearlPity;
|
||||||
|
pulls = game::pullGacha(standardPearl, items, standardPearlPity, 10, lucky);
|
||||||
|
bool allStandardPearl = pulls.size() == 10;
|
||||||
|
for (const game::GachaPull& pull : pulls) {
|
||||||
|
allStandardPearl &= pull.rarity == 5 && pull.avatar && pull.itemId == 1503;
|
||||||
|
}
|
||||||
|
check(allStandardPearl, "and it draws her on a won 50/50");
|
||||||
|
|
||||||
|
// The real dice, 200000 pulls: no two lost 50/50s in a row, and the losses spread evenly.
|
||||||
|
game::GachaPity simPity;
|
||||||
|
std::map<uint32_t, uint32_t> lostTo;
|
||||||
|
uint32_t fiveStars = 0, owed = 0, lost = 0;
|
||||||
|
bool lostTwice = false, lastLost = false;
|
||||||
|
for (int batch = 0; batch < 20000; ++batch) {
|
||||||
|
for (const game::GachaPull& pull :
|
||||||
|
game::pullGacha(standardPearl, items, simPity, 10, game::gachaRoll)) {
|
||||||
|
if (pull.rarity != 5) continue;
|
||||||
|
++fiveStars;
|
||||||
|
if (pull.featured) {
|
||||||
|
if (pull.guaranteed) ++owed;
|
||||||
|
lastLost = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++lost;
|
||||||
|
++lostTo[pull.itemId];
|
||||||
|
lostTwice |= lastLost;
|
||||||
|
lastLost = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(!lostTwice, "a lost 50/50 is never followed by another");
|
||||||
|
check(owed == lost || owed + 1 == lost, "every loss is paid back by the next 5*");
|
||||||
|
double rate = static_cast<double>(fiveStars) / 200000.0;
|
||||||
|
check(rate > 0.013 && rate < 0.019, "about 1.6% of pulls are 5*");
|
||||||
|
bool even = lostTo.size() == items.fiveStarAvatars.size();
|
||||||
|
for (const auto& [id, count] : lostTo) even &= count > lost * 8 / 100 && count < lost * 20 / 100;
|
||||||
|
check(even, "a lost 50/50 lands evenly on the seven standard 5*s");
|
||||||
|
std::printf(" warp simulation: %u 5* (%.2f%%), %u lost, spread:", fiveStars, rate * 100, lost);
|
||||||
|
for (const auto& [id, count] : lostTo) std::printf(" %u=%u", id, count);
|
||||||
|
std::printf("\n");
|
||||||
|
|
||||||
|
// Pity outlives a restart.
|
||||||
|
std::string realPath = core::Config::get().paths.playerFile;
|
||||||
|
core::Config::get().paths.playerFile = "build/test-gacha-player.json";
|
||||||
|
game::Player saved(1);
|
||||||
|
saved.gacha().character = {12, 3, true, 345};
|
||||||
|
check(game::savePlayerState(saved), "pity saves");
|
||||||
|
game::Player loaded(1);
|
||||||
|
check(game::loadPlayerState(loaded), "and loads");
|
||||||
|
const game::GachaPity& back = loaded.gacha().character;
|
||||||
|
check(back.sinceFive == 12 && back.sinceFour == 3 && back.guaranteed && back.total == 345,
|
||||||
|
"unchanged");
|
||||||
|
core::Config::get().paths.playerFile = realPath;
|
||||||
|
}
|
||||||
|
|
||||||
void testChallengeHistory() {
|
void testChallengeHistory() {
|
||||||
const data::Tables& tables = data::Tables::get();
|
const data::Tables& tables = data::Tables::get();
|
||||||
if (!tables.loaded()) return;
|
if (!tables.loaded()) return;
|
||||||
@@ -816,6 +1034,7 @@ void runGameTests() {
|
|||||||
testItemUniqueIds();
|
testItemUniqueIds();
|
||||||
testElementBuffs();
|
testElementBuffs();
|
||||||
testTables();
|
testTables();
|
||||||
|
testGacha();
|
||||||
testChallengeHistory();
|
testChallengeHistory();
|
||||||
testSceneRes();
|
testSceneRes();
|
||||||
testRosterProtos();
|
testRosterProtos();
|
||||||
|
|||||||
Reference in new issue
Block a user