Compare commits

...

10 commits

Author SHA1 Message Date
YYHEggEgg
436e691245 QuickMenu bugfix 2024-08-06 23:30:16 +08:00
YYHEggEgg
bb058b451d Separate Gacha logic & DTO, Fix items issue 2024-08-06 22:58:24 +08:00
YYHEggEgg
b2401d566b Support teleport map, mod button, post girl, nickname init 2024-08-06 22:43:45 +08:00
3c2397cdbd README: update feature list 2024-08-05 15:08:00 +00:00
356fc5278e server version: milestone 2 reached (0.2.0) 2024-08-05 17:59:18 +03:00
50f694add0 H.D.D. and Combat commissions implementation
Implement Combat commissions (PureHollowBattle and LongFight) (including Rally commissions)
Refactor some battle structures
Unlock hollow quests (QuestInfo and YorozuyaInfo)
2024-08-05 17:45:13 +03:00
f99165e452 chore: move new dependencies to workspace's Cargo.toml 2024-08-04 16:20:31 +03:00
8d1211750b chore: fix warnings 2024-08-04 15:46:43 +03:00
285fc0b6fa NewsStand interaction 2024-08-04 15:40:06 +03:00
55b7ed3beb Gacha System implementation
## Abstract

This PR implements

- The Gacha System for client. Player can pull in defined pools with a similar experience to zenless & mihoyo gacha, and these status can be saved through player save and recovered.
- `player kick` command in extra. Command `player kick [player_uid] (optional: [reason_id])` can be used to kick a player to offline.

## Support list

- Similar probability to mihoyo gacha
- QingYi & Jane Doe's Agent & W-Engine banner
- Standard Banner:
  - Get a S Agent in the first 50 pulls
  - Get a 20% discount for the first 5 10-pulls
  - Choose a S Agent once you pulled for 300
- ~~Bangboo Banner~~ (not working due to other reasons)
  - Choosing your demanded S bangboo for 100% UP

## Principle

- A complex configuration file `gacha.jsonc` is introduced to define all behaviours.
- Gacha status is saved via `GachaModelBin`.

For more informations about the config and save, an article is available [here](https://yyheggegg.github.io/mihomo-gio-blogs/zzz-gacha-impl-en/).

## Known issues

- You can not see any bangboos in the collection when choosing Bangboo.
- **Specifically for 1.1 Beta**, performing gacha may lead to random client issues, including:
  - The TVs showing rarity ending up in blank after its flash.
  - Game become black screen.
  - If clicking 'Skip' but not fast enough, game'll stuck and not able to do anything. You may try to click 'Skip' scarcely when 'REC' shows, or after all animations has stopped.

Co-authored-by: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com>
Reviewed-on: NewEriduPubSec/JaneDoe-ZS#1
Co-authored-by: YYHEggEgg <yyheggegg@xeondev.com>
Co-committed-by: YYHEggEgg <yyheggegg@xeondev.com>
2024-08-04 11:41:23 +00:00
69 changed files with 199417 additions and 358 deletions

View file

@ -13,6 +13,8 @@ tokio-util = { version = "0.7.10", features = ["io"] }
# Serialization
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
jsonc-parser = { version = "0.23.0", features = ["serde"] }
chrono = { version = "0.4.38", features = ["serde"] }
toml = "0.8.4"
rbase64 = "2.0.3"
prost = "0.12.3"

View file

@ -6,9 +6,13 @@
### Current features
- Logging in
- Fully playable prologue
- HollowDeepDive with Combat and Rally commissions
- Archive (playable cutscenes and battles)
- Open world
- World exploration with any character (can be changed with a command)
- Player progress saving with PostgreSQL
- Unlock all characters
- Training battle
- Player management command system
- Training
### Requirements
- [Rust](https://www.rust-lang.org/tools/install)

View file

@ -200,5 +200,16 @@
"store_template_id": 1191
}
]
},
{
"event_id": 15000301,
"actions": [
{
"$type": "OpenUi",
"ui": "UINewsStandPageController",
"args": 0,
"store_template_id": 1061
}
]
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,212 @@
[
{
"ID": 100001,
"FOPDNLNNHDJ": 1000,
"DIIDBBGLDOL": "HollowGroupNameText_Arcade",
"EFPBDDJIJBO": "1040140114",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_RallyQuest.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_L.prefab",
"LDKNDDHOHND": "FF0019FF",
"EEBAIANJMEC": "006EFFFF|FF001900",
"EKMLECIKMID": false,
"FNIGJDFMHBA": "",
"BECFHCOGPKG": ""
},
{
"ID": 100101,
"FOPDNLNNHDJ": 1001,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": false,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
},
{
"ID": 100102,
"FOPDNLNNHDJ": 1001,
"DIIDBBGLDOL": "HollowGroupNameText_exploration",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Explore.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M02.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips03.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_03"
},
{
"ID": 100103,
"FOPDNLNNHDJ": 1001,
"DIIDBBGLDOL": "HollowGroupNameText_combat",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Combat.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_S.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips02.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_02"
},
{
"ID": 100201,
"FOPDNLNNHDJ": 1002,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
},
{
"ID": 100202,
"FOPDNLNNHDJ": 1002,
"DIIDBBGLDOL": "HollowGroupNameText_exploration",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Explore.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M02.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips03.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_03"
},
{
"ID": 100203,
"FOPDNLNNHDJ": 1002,
"DIIDBBGLDOL": "HollowGroupNameText_combat",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Combat.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_S.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips02.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_02"
},
{
"ID": 100301,
"FOPDNLNNHDJ": 1003,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
},
{
"ID": 100302,
"FOPDNLNNHDJ": 1003,
"DIIDBBGLDOL": "HollowGroupNameText_exploration",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Explore.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M02.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips03.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_03"
},
{
"ID": 100303,
"FOPDNLNNHDJ": 1003,
"DIIDBBGLDOL": "HollowGroupNameText_combat",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Combat.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_S.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips02.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_02"
},
{
"ID": 100401,
"FOPDNLNNHDJ": 1004,
"DIIDBBGLDOL": "HollowGroupNameText_BigWorld",
"EFPBDDJIJBO": "1100140008",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_HighRisk.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_L.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": false,
"FNIGJDFMHBA": "",
"BECFHCOGPKG": ""
},
{
"ID": 100501,
"FOPDNLNNHDJ": 1005,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
},
{
"ID": 100502,
"FOPDNLNNHDJ": 1005,
"DIIDBBGLDOL": "HollowGroupNameText_exploration",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Explore.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M02.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips03.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_03"
},
{
"ID": 100503,
"FOPDNLNNHDJ": 1005,
"DIIDBBGLDOL": "HollowGroupNameText_combat",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Combat.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_S.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips02.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_02"
},
{
"ID": 109901,
"FOPDNLNNHDJ": 1099,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": false,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
[
{
"QuickAccessIndex": 1,
"QuickFuncID": 1005,
"RequiredUnlockIDs": [
1005
],
"EFPBDDJIJBO": 1040140154,
"BOKMLGNMJPB": null
},
{
"QuickAccessIndex": 2,
"QuickFuncID": 1022,
"RequiredUnlockIDs": [
1022
],
"EFPBDDJIJBO": 1040140049,
"BOKMLGNMJPB": null
},
{
"QuickAccessIndex": 3,
"QuickFuncID": 1009,
"RequiredUnlockIDs": [
1009
],
"EFPBDDJIJBO": 1050060115,
"BOKMLGNMJPB": null
}
]

View file

@ -0,0 +1,112 @@
[
{
"BtnID": 1001,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1002,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1003,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1004,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1005,
"BOMNEKHOGIH": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnInterKnot.png",
"PECPFBBOGKO": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnInterKnot.png"
},
{
"BtnID": 1006,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1007,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1008,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1009,
"BOMNEKHOGIH": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnGuide.png",
"PECPFBBOGKO": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnGuide.png"
},
{
"BtnID": 1010,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1011,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1013,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1014,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1015,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1016,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1017,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1018,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1019,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1020,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1021,
"BOMNEKHOGIH": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnCamera.png",
"PECPFBBOGKO": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnCamera.png"
},
{
"BtnID": 1022,
"BOMNEKHOGIH": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnMessage.png",
"PECPFBBOGKO": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnMessage.png"
},
{
"BtnID": 1023,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
}
]

View file

@ -0,0 +1,882 @@
[
{
"AreaID": 500000101,
"BattleEventID": 5000001,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000001_1.asset",
"MDMOGIDKCLG": "metro4_2",
"LLPNKBBJHKL": "PlayerPos_E2",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000102,
"BattleEventID": 5000001,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000001_2.asset",
"MDMOGIDKCLG": "metro4_2",
"LLPNKBBJHKL": "PlayerPos_E1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000103,
"BattleEventID": 5000001,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000001_3.asset",
"MDMOGIDKCLG": "metro4_2",
"LLPNKBBJHKL": "PlayerPos_E1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000201,
"BattleEventID": 5000002,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000002_1.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_B5",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000202,
"BattleEventID": 5000002,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000002_2.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000203,
"BattleEventID": 5000002,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000002_3.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000301,
"BattleEventID": 5000003,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000003_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_3_1",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000302,
"BattleEventID": 5000003,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000003_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_3_1",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000303,
"BattleEventID": 5000003,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000003_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_3_1",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000401,
"BattleEventID": 5000004,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000004_1.asset",
"MDMOGIDKCLG": "Stage_Zero_SpecialRoom_5",
"LLPNKBBJHKL": "PlayerPos_E1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000402,
"BattleEventID": 5000004,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000004_2.asset",
"MDMOGIDKCLG": "Stage_Zero_SpecialRoom_5",
"LLPNKBBJHKL": "PlayerPos_SP",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000403,
"BattleEventID": 5000004,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000004_3.asset",
"MDMOGIDKCLG": "Stage_Zero_SpecialRoom_5",
"LLPNKBBJHKL": "PlayerPos_SP",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010101,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_1.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010102,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_2.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010103,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_3.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010104,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_4.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_22",
"LLPNKBBJHKL": "SpecialPlayerPos_1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010105,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_5.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint5",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011101,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_1.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Rain",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011102,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_2.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "RallyPlayerPos_C1",
"TimePeriod": "Night",
"Weather": "Rain",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011103,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_3.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "RallyPlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Rain",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011104,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_4.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_22",
"LLPNKBBJHKL": "SpecialPlayerPos_1",
"TimePeriod": "Morning",
"Weather": "Rain",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011105,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_5.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "PlayerPos_F2",
"TimePeriod": "Night",
"Weather": "Rain",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501012101,
"BattleEventID": 5010121,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010121_1.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501012102,
"BattleEventID": 5010121,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010121_2.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint7",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501012103,
"BattleEventID": 5010121,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010121_3.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501012104,
"BattleEventID": 5010121,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010121_4.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "PlayerPos_E2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020200,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_0.asset",
"MDMOGIDKCLG": "Stage_VR_TrainingRoom_30R",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020201,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_1.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020202,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_2.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020203,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_3.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020204,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_4.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint4",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020205,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_5.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint5",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020206,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_6.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_25_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020207,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_7.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint7",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020208,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_6.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_25_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020209,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_9.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021101,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_1.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021102,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_2.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021103,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_3.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021104,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_4.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_12_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint4",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021105,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_5.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint5",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021106,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_6.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021107,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_7.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021301,
"BattleEventID": 5010213,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010213_1.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_B5",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021302,
"BattleEventID": 5010213,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010213_2.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021303,
"BattleEventID": 5010213,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010213_3.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021304,
"BattleEventID": 5010213,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010213_4.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501022101,
"BattleEventID": 5010221,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010221_1.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint9",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501022102,
"BattleEventID": 5010221,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010221_2.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_C2",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501022103,
"BattleEventID": 5010221,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010221_3.asset",
"MDMOGIDKCLG": "Construction2_3_Zero",
"LLPNKBBJHKL": "PlayerPos_E4_Rally",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501022104,
"BattleEventID": 5010221,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010221_4.asset",
"MDMOGIDKCLG": "Construction2_3_Zero",
"LLPNKBBJHKL": "PlayerPos_D4_Rally",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030101,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_3_Successive",
"LLPNKBBJHKL": "SpecialPlayerPos_B1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030102,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030103,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030104,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_4.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030105,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_5.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "PlayerPos_CheckPoint4",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030106,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_6.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_15_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPointSideQuest1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030107,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_7.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "SpecialRallyPos_7",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031101,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031102,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031103,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031104,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_4.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint4",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031105,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_5.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint5",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031106,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_6.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031107,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_7.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint7",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031108,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_8.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_15_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint8",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031109,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_9.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint9",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031110,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_10.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_1_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint10",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031111,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_11.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint11",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031112,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_12.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint12",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501032101,
"BattleEventID": 5010321,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010321_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Zero",
"LLPNKBBJHKL": "PlayerPos_A5_Rally",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501032102,
"BattleEventID": 5010321,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010321_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Zero",
"LLPNKBBJHKL": "PlayerPos_A6_Rally",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501032103,
"BattleEventID": 5010321,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010321_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Zero",
"LLPNKBBJHKL": "PlayerPos_C5_Rally",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501032104,
"BattleEventID": 5010321,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010321_4.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_1_1_Rally",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 503020101,
"BattleEventID": 5030201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030201_1.asset",
"MDMOGIDKCLG": "Stage_MetroArea_4_1_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503020102,
"BattleEventID": 5030201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030201_2.asset",
"MDMOGIDKCLG": "Stage_MetroArea_4_1_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503020103,
"BattleEventID": 5030201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030201_3.asset",
"MDMOGIDKCLG": "Stage_MetroArea_4_1_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503021101,
"BattleEventID": 5030211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030211_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503021102,
"BattleEventID": 5030211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030211_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503021103,
"BattleEventID": 5030211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030211_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,604 @@
[
{
"ID": 500000101,
"BattleEventID": 5000001,
"VariableName": "IsArea1Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000102,
"BattleEventID": 5000001,
"VariableName": "IsArea2Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000103,
"BattleEventID": 5000001,
"VariableName": "IsArea3Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000104,
"BattleEventID": 5000001,
"VariableName": "IsArea4Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000105,
"BattleEventID": 5000001,
"VariableName": "IsArea5Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000106,
"BattleEventID": 5000001,
"VariableName": "IsArea6Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000107,
"BattleEventID": 5000001,
"VariableName": "SideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000108,
"BattleEventID": 5000001,
"VariableName": "SecondSideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000109,
"BattleEventID": 5000001,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 500000201,
"BattleEventID": 5000002,
"VariableName": "whichAreaIn",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 500000202,
"BattleEventID": 5000002,
"VariableName": "IsArea2Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000203,
"BattleEventID": 5000002,
"VariableName": "IsArea3Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000204,
"BattleEventID": 5000002,
"VariableName": "IsArea4Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000205,
"BattleEventID": 5000002,
"VariableName": "IsArea5Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000206,
"BattleEventID": 5000002,
"VariableName": "SecretMission",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 500000207,
"BattleEventID": 5000002,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 500000301,
"BattleEventID": 5000003,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 500000401,
"BattleEventID": 5000004,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501010100,
"BattleEventID": 5010101,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501010101,
"BattleEventID": 5010101,
"VariableName": "SideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501010102,
"BattleEventID": 5010101,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501010103,
"BattleEventID": 5010101,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501010104,
"BattleEventID": 5010101,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501011101,
"BattleEventID": 5010111,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501011102,
"BattleEventID": 5010111,
"VariableName": "SideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501011103,
"BattleEventID": 5010111,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501011104,
"BattleEventID": 5010111,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501011105,
"BattleEventID": 5010111,
"VariableName": "GroundSideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501011106,
"BattleEventID": 5010111,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501012101,
"BattleEventID": 5010121,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "4"
},
{
"ID": 501012102,
"BattleEventID": 5010121,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501020200,
"BattleEventID": 5010201,
"VariableName": "IsEnterTeachLevel",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020201,
"BattleEventID": 5010201,
"VariableName": "IsArea1Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020202,
"BattleEventID": 5010201,
"VariableName": "IsArea2Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020203,
"BattleEventID": 5010201,
"VariableName": "IsArea3Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020204,
"BattleEventID": 5010201,
"VariableName": "IsArea4Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020205,
"BattleEventID": 5010201,
"VariableName": "IsArea5Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020206,
"BattleEventID": 5010201,
"VariableName": "IsArea6Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020207,
"BattleEventID": 5010201,
"VariableName": "IsArea7Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020208,
"BattleEventID": 5010201,
"VariableName": "SideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020209,
"BattleEventID": 5010201,
"VariableName": "IsArea8Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020210,
"BattleEventID": 5010201,
"VariableName": "SecondSideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020211,
"BattleEventID": 5010201,
"VariableName": "StartSecondPhase",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020212,
"BattleEventID": 5010201,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501020213,
"BattleEventID": 5010201,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020214,
"BattleEventID": 5010201,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020215,
"BattleEventID": 5010201,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501020216,
"BattleEventID": 5010201,
"VariableName": "LifeCoinGet",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501021101,
"BattleEventID": 5010211,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501021102,
"BattleEventID": 5010211,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501021103,
"BattleEventID": 5010211,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501021104,
"BattleEventID": 5010211,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501021105,
"BattleEventID": 5010211,
"VariableName": "BossHealth",
"ValueType": "float",
"InitialValue": "1"
},
{
"ID": 501021106,
"BattleEventID": 5010211,
"VariableName": "CargoCartStopped",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501021107,
"BattleEventID": 5010211,
"VariableName": "IsLastAreaFinish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501021301,
"BattleEventID": 5010213,
"VariableName": "whichAreaIn",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501022101,
"BattleEventID": 5010221,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "4"
},
{
"ID": 501022102,
"BattleEventID": 5010221,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030101,
"BattleEventID": 5010301,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501030102,
"BattleEventID": 5010301,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501030103,
"BattleEventID": 5010301,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030104,
"BattleEventID": 5010301,
"VariableName": "DoorStatus1",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030105,
"BattleEventID": 5010301,
"VariableName": "DoorStatus2",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030106,
"BattleEventID": 5010301,
"VariableName": "DoorStatus3",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030107,
"BattleEventID": 5010301,
"VariableName": "DoorStatus4",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030108,
"BattleEventID": 5010301,
"VariableName": "DoorStatus5",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030109,
"BattleEventID": 5010301,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501030110,
"BattleEventID": 5010301,
"VariableName": "IsTrickFinish",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031101,
"BattleEventID": 5010311,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501031102,
"BattleEventID": 5010311,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501031103,
"BattleEventID": 5010311,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031104,
"BattleEventID": 5010311,
"VariableName": "IsAllyRescued",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031105,
"BattleEventID": 5010311,
"VariableName": "IsShortcutOpened",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031106,
"BattleEventID": 5010311,
"VariableName": "IsGuardDefeated",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031107,
"BattleEventID": 5010311,
"VariableName": "DoorStatus1",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031108,
"BattleEventID": 5010311,
"VariableName": "DoorStatus2",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031109,
"BattleEventID": 5010311,
"VariableName": "DoorStatus3",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031110,
"BattleEventID": 5010311,
"VariableName": "DoorStatus4",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031111,
"BattleEventID": 5010311,
"VariableName": "DoorStatus5",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031112,
"BattleEventID": 5010311,
"VariableName": "IsAllyImposterCatched",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031113,
"BattleEventID": 5010311,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031114,
"BattleEventID": 5010311,
"VariableName": "LifeCoinGet",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501032101,
"BattleEventID": 5010321,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "4"
},
{
"ID": 501032102,
"BattleEventID": 5010321,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 503020101,
"BattleEventID": 5030201,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 503021101,
"BattleEventID": 5030211,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,2 +1,2 @@
pub const SERVER_VERSION: &str = "0.1.0";
pub const SERVER_VERSION: &str = "0.2.0";
pub const CLIENT_VERSION: &str = "CNBeta1.1.1";

View file

@ -7,6 +7,8 @@ version.workspace = true
# Serialization
serde.workspace = true
serde_json.workspace = true
jsonc-parser.workspace = true
chrono.workspace = true
prost.workspace = true
# Util

View file

@ -0,0 +1,305 @@
use std::collections::{HashMap, HashSet};
use chrono::{prelude::Local, DateTime};
use serde::{Deserialize, Deserializer};
use tracing;
#[derive(Debug, Default, Deserialize)]
pub struct ExtraItemsPolicy {
pub id: u32,
pub count: u32,
#[serde(default)]
pub apply_on_owned_count: u32,
}
#[derive(Debug, Default, Deserialize)]
pub struct ProbabilityPoint {
pub start_pity: u32,
pub start_chance_percent: f64,
#[serde(default)]
pub increment_percent: f64,
}
#[derive(Debug, Default, Deserialize)]
pub struct ProbabilityModel {
#[serde(default)]
pub clear_status_on_higher_rarity_pulled: bool,
pub points: Vec<ProbabilityPoint>,
// This value is for display only, so it's set when
// the maximum guarantee is not equal to the
// automatically calculated value (commonly, less than).
#[serde(default)]
pub maximum_guarantee_pity: u32,
#[serde(skip_deserializing)]
probability_percents: Vec<f64>,
}
impl ProbabilityModel {
fn get_maximum_guarantee(&self) -> u32 {
self.probability_percents.len() as u32 - 1
}
pub fn post_configure(&mut self, tag: &String) {
self.points.sort_by_key(|point| point.start_pity);
let mut probability_percents: Vec<f64> = vec![0.0];
for (i, point) in self.points.iter().enumerate() {
if i > 0 {
let last_point = &self.points[i - 1];
let last_stop_percent = last_point.start_chance_percent
+ last_point.increment_percent
* (point.start_pity - last_point.start_pity) as f64;
if last_stop_percent > point.start_chance_percent {
tracing::warn!("Gacha - ProbabilityModel '{tag}': The start chance of '{point:?}' is less than the value inherited from the previous point.");
}
}
let mut max_pity = 2000;
if i < self.points.len() - 1 {
let next_point = &self.points[i + 1];
max_pity = next_point.start_pity - 1;
let max_probability = point.start_chance_percent
+ point.increment_percent
* (next_point.start_pity - 1 - point.start_pity) as f64;
assert!(max_probability < 100.0, "Gacha - ProbabilityModel '{tag}': Probability already reached 100% in '{point:?}' (though points with higher pity left)");
}
let mut pity = point.start_pity;
let mut percent = point.start_chance_percent;
while pity <= max_pity {
if max_pity >= 2000 && percent >= 100.0 {
probability_percents.push(100.0);
break;
}
probability_percents.push(percent);
percent += point.increment_percent;
pity += 1;
}
assert!(pity <= 2000, "Gacha - ProbabilityModel '{tag}' (point {i}): Haven't reached 100% guarantee probability at Pity 2001. The current probability is {percent}%. Crazy.");
}
self.probability_percents = probability_percents;
if self.maximum_guarantee_pity <= 0 {
self.maximum_guarantee_pity = self.get_maximum_guarantee();
}
}
pub fn get_chance_percent(&self, pity: &u32) -> f64 {
// The vec length is 1 bigger than the maximum pity (1-based)
let guarantee_pity = self.probability_percents.len() - 1;
let idx = *pity as usize;
if idx > guarantee_pity {
return self.probability_percents[guarantee_pity];
}
self.probability_percents[idx]
}
}
#[allow(dead_code)]
#[derive(Debug, Default, Deserialize)]
pub struct CategoryGuaranteePolicy {
pub included_category_tags: HashSet<String>,
pub trigger_on_failure_times: u32,
pub clear_status_on_target_changed: bool,
pub chooseable: bool,
}
#[derive(Debug, Default, Deserialize)]
pub struct TenPullDiscount {
pub use_limit: u32,
pub discounted_prize: u32,
}
#[derive(Debug, Default, Deserialize)]
pub struct AdvancedGuarantee {
pub use_limit: u32,
pub rarity: u32,
pub guarantee_pity: u32,
}
#[derive(Debug, Default, Deserialize)]
pub struct MustGainItem {
pub use_limit: u32,
pub rarity: u32,
pub category_tag: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct FreeSelectItem {
pub milestones: Vec<u32>,
pub rarity: u32,
pub category_tags: Vec<String>,
pub free_select_progress_record_tag: String,
pub free_select_usage_record_tag: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct DiscountPolicyCollection {
pub ten_pull_discount_map: HashMap<String, TenPullDiscount>,
pub must_gain_item_map: HashMap<String, MustGainItem>,
pub advanced_guarantee_map: HashMap<String, AdvancedGuarantee>,
pub free_select_map: HashMap<String, FreeSelectItem>,
}
impl DiscountPolicyCollection {
pub fn post_configure(&mut self) {
for (tag, ten_pull_discount) in self.ten_pull_discount_map.iter() {
let discounted_prize = ten_pull_discount.discounted_prize;
assert!(discounted_prize < 10, "Gacha - DiscountPolicy '{tag}': ten_pull_discount's value should be smaller than 10 (read {discounted_prize}).");
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum GachaAddedItemType {
#[default]
None = 0,
Weapon = 1,
Character = 2,
Bangboo = 3,
}
impl GachaAddedItemType {
pub fn as_str_name(&self) -> &'static str {
match self {
GachaAddedItemType::None => "GACHA_ADDED_ITEM_TYPE_NONE",
GachaAddedItemType::Weapon => "GACHA_ADDED_ITEM_TYPE_WEAPON",
GachaAddedItemType::Character => "GACHA_ADDED_ITEM_TYPE_CHARACTER",
GachaAddedItemType::Bangboo => "GACHA_ADDED_ITEM_TYPE_BANGBOO",
}
}
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"GACHA_ADDED_ITEM_TYPE_NONE" => Some(Self::None),
"GACHA_ADDED_ITEM_TYPE_WEAPON" => Some(Self::Weapon),
"GACHA_ADDED_ITEM_TYPE_CHARACTER" => Some(Self::Character),
"GACHA_ADDED_ITEM_TYPE_BANGBOO" => Some(Self::Bangboo),
_ => None,
}
}
}
impl From<i32> for GachaAddedItemType {
fn from(value: i32) -> Self {
match value {
1 => Self::Weapon,
2 => Self::Character,
3 => Self::Bangboo,
_ => Self::None
}
}
}
impl Into<i32> for GachaAddedItemType {
fn into(self) -> i32 {
match self {
Self::Weapon => 1,
Self::Character => 2,
Self::Bangboo => 3,
Self::None => 0,
}
}
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaCategoryInfo {
#[serde(default)]
pub is_promotional_items: bool,
pub item_ids: Vec<u32>,
pub category_weight: u32,
#[serde(deserialize_with = "from_str")]
pub item_type: GachaAddedItemType,
}
pub fn from_str<'de, D>(deserializer: D) -> Result<GachaAddedItemType, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let result = GachaAddedItemType::from_str_name(&s);
match result {
Some(val) => Ok(val),
None => Ok(GachaAddedItemType::None)
}
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaAvailableItemsInfo {
pub rarity: u32,
#[serde(default)]
pub extra_items_policy_tags: Vec<String>,
pub categories: HashMap<String, GachaCategoryInfo>,
pub probability_model_tag: String,
#[serde(default)]
pub category_guarantee_policy_tags: Vec<String>,
}
#[allow(dead_code)]
#[derive(Debug, Default, Deserialize)]
pub struct CharacterGachaPool {
pub gacha_schedule_id: u32,
pub gacha_parent_schedule_id: u32,
pub comment: String,
pub gacha_type: u32,
pub cost_item_id: u32,
pub start_time: DateTime<Local>,
pub end_time: DateTime<Local>,
#[serde(default)]
pub discount_policy_tags: Vec<String>,
pub sharing_guarantee_info_category: String,
pub gacha_items: Vec<GachaAvailableItemsInfo>,
}
impl CharacterGachaPool {
pub fn is_still_open(&self, now: &DateTime<Local>) -> bool {
self.start_time <= *now && *now <= self.end_time
}
pub fn post_configure(&mut self, probability_model_map: &HashMap<String, ProbabilityModel>) {
self.gacha_items
.sort_by_key(|item_list| u32::MAX - item_list.rarity);
for items_info in self.gacha_items.iter_mut() {
assert!(probability_model_map.contains_key(&items_info.probability_model_tag), "Gacha - CharacterGachaPool '{}': Specified ProbabilityModel tag '{}' that does not exist.", self.gacha_schedule_id, items_info.probability_model_tag);
}
}
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaCommonProperties {
pub up_item_category_tag: String,
pub s_item_rarity: u32,
pub a_item_rarity: u32,
// TODO: PostConfigure check
pub ten_pull_discount_tag: String,
pub newcomer_advanced_s_tag: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaConfiguration {
pub character_gacha_pool_list: Vec<CharacterGachaPool>,
pub probability_model_map: HashMap<String, ProbabilityModel>,
pub category_guarantee_policy_map: HashMap<String, CategoryGuaranteePolicy>,
pub extra_items_policy_map: HashMap<String, ExtraItemsPolicy>,
pub discount_policies: DiscountPolicyCollection,
pub common_properties: GachaCommonProperties,
}
impl GachaConfiguration {
pub fn post_configure(&mut self) {
assert!(
self.category_guarantee_policy_map
.contains_key(&self.common_properties.up_item_category_tag),
"The UP category should be valid in policy map."
);
for (tag, policy) in self.probability_model_map.iter_mut() {
policy.post_configure(&tag);
}
self.discount_policies.post_configure();
for character_pool in self.character_gacha_pool_list.iter_mut() {
character_pool.post_configure(&self.probability_model_map);
}
}
}

45
nap_data/src/gacha/mod.rs Normal file
View file

@ -0,0 +1,45 @@
use std::sync::OnceLock;
use gacha_config::GachaConfiguration;
use jsonc_parser::{parse_to_serde_value, ParseOptions};
use serde::Deserialize;
use crate::{action::ActionConfig, DataLoadError};
pub mod gacha_config;
const GACHA_CONFIG_NAME: &str = "gacha.jsonc";
static GACHACONF: OnceLock<GachaConfiguration> = OnceLock::new();
#[derive(Deserialize, Debug)]
pub struct EventGraphConfig {
pub event_id: u32,
pub actions: Vec<ActionConfig>,
}
pub(crate) fn load_gacha_config(path: &str) -> Result<(), DataLoadError> {
let jsonc_data = std::fs::read_to_string(format!("{path}/{GACHA_CONFIG_NAME}"))
.map_err(|err| DataLoadError::IoError(err))?;
let json_value = parse_to_serde_value(
&jsonc_data,
&ParseOptions {
allow_comments: true,
allow_loose_object_property_names: false,
allow_trailing_commas: true,
},
)
.map_err(|err| DataLoadError::JsoncParseError(err))?
.unwrap();
let mut result = serde_json::from_value::<GachaConfiguration>(json_value)
.map_err(|err| DataLoadError::FromJsonError(String::from("GachaConfiguration"), err))?;
result.post_configure();
GACHACONF.set(result).unwrap();
Ok(())
}
pub fn global_gacha_config() -> &'static GachaConfiguration {
GACHACONF.get().unwrap()
}

View file

@ -1,9 +1,11 @@
pub mod action;
pub mod event;
pub mod gacha;
pub mod tables;
use std::{collections::HashMap, sync::OnceLock};
use jsonc_parser::errors::ParseError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -12,6 +14,7 @@ pub struct AssetsConfig {
pub filecfg_path: String,
pub event_config_path: String,
pub usm_keys_path: String,
pub gacha_config_path: String,
}
#[derive(Error, Debug)]
@ -20,6 +23,8 @@ pub enum DataLoadError {
IoError(#[from] std::io::Error),
#[error("from_json failed for type {0}, error: {1}")]
FromJsonError(String, serde_json::Error),
#[error("jsonc_parser parse as json error: {0}")]
JsoncParseError(#[from] ParseError),
}
static USM_KEY_MAP: OnceLock<HashMap<u32, u64>> = OnceLock::new();
@ -31,6 +36,7 @@ pub fn init_data(config: &AssetsConfig) -> Result<(), DataLoadError> {
tracing::warn!("failed to load USM keys, in-game cutscenes will not work! Reason: {err}");
USM_KEY_MAP.set(HashMap::new()).unwrap();
}
gacha::load_gacha_config(&config.gacha_config_path)?;
Ok(())
}

View file

@ -1,5 +1,7 @@
use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(ArchiveBattleQuest u32 id);
#[derive(Deserialize, Debug)]
@ -19,7 +21,7 @@ pub struct ArchiveBattleQuestTemplate {
#[serde(rename = "FirstBattleEventID")]
pub first_battle_event_id: u32,
#[serde(rename = "BattleEventID")]
pub battle_event_id: u32,
pub battle_event_id: BattleEventConfigID,
pub battle_rank: String,
pub slot1_avatar: i32,
pub slot2_avatar: i32,

View file

@ -0,0 +1,16 @@
use serde::Deserialize;
use super::OnceRewardID;
template_id!(BattleEventConfig u32 id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct BattleEventConfigTemplate {
#[serde(rename = "ID")]
pub id: BattleEventConfigID,
#[serde(rename = "LevelDesignID")]
pub level_design_id: u32,
pub normal_drop: String,
pub special_drop: Vec<OnceRewardID>,
}

View file

@ -0,0 +1,16 @@
use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(BattleGroupConfig u32 id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct BattleGroupConfigTemplate {
#[serde(rename = "ID")]
pub id: BattleGroupConfigID,
#[serde(rename = "QuestID")]
pub quest_id: u32,
#[serde(rename = "BattleEventID")]
pub battle_event_id: BattleEventConfigID,
}

View file

@ -0,0 +1,10 @@
use serde::Deserialize;
template_id!(HollowConfig u32 id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct HollowConfigTemplate {
#[serde(rename = "ID")]
pub id: HollowConfigID,
}

View file

@ -0,0 +1,13 @@
use serde::Deserialize;
template_id!(HollowQuest u32 id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct HollowQuestTemplate {
#[serde(rename = "ID")]
pub id: HollowQuestID,
#[serde(rename = "ChessBoardID")]
pub chess_board_id: u32,
pub hollow_quest_type: u32,
}

View file

@ -0,0 +1,10 @@
use serde::Deserialize;
template_id!(Item u32 id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ItemTemplate {
#[serde(rename = "ID")]
pub id: ItemID,
}

View file

@ -87,4 +87,15 @@ template_tables! {
MainCityBgmConfigTemplate;
ArchiveFileQuestTemplate;
ArchiveBattleQuestTemplate;
HollowQuestTemplate;
HollowConfigTemplate;
BattleEventConfigTemplate;
BattleGroupConfigTemplate;
SubAreaDataTemplate;
VariableDataTemplate;
OnceRewardTemplate;
QuickAccessTemplate;
QuickFuncTemplate;
TeleportConfigTemplate;
ItemTemplate;
}

View file

@ -0,0 +1,19 @@
use serde::Deserialize;
template_id!(OnceReward u32 reward_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct OnceRewardTemplate {
#[serde(rename = "RewardID")]
pub reward_id: OnceRewardID,
pub reward_list: Vec<RewardItem>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct RewardItem {
#[serde(rename = "RewardItemID")]
pub reward_item_id: u32,
pub reward_amount: u32,
}

View file

@ -0,0 +1,11 @@
use serde::Deserialize;
template_id!(QuickAccess u32 quick_func_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct QuickAccessTemplate {
pub quick_access_index: u32,
#[serde(rename = "QuickFuncID")]
pub quick_func_id: QuickAccessID,
}

View file

@ -0,0 +1,10 @@
use serde::Deserialize;
template_id!(QuickFunc u32 btn_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct QuickFuncTemplate {
#[serde(rename = "BtnID")]
pub btn_id: QuickFuncID,
}

View file

@ -0,0 +1,16 @@
use std::u32;
use serde::Deserialize;
template_id!(SubAreaData u32 area_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct SubAreaDataTemplate {
#[serde(rename = "AreaID")]
pub area_id: SubAreaDataID,
#[serde(rename = "BattleEventID")]
pub battle_event_id: u32,
pub time_period: String,
pub weather: String,
}

View file

@ -0,0 +1,11 @@
use serde::Deserialize;
template_id!(TeleportConfig i32 teleport_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct TeleportConfigTemplate {
#[serde(rename = "TeleportID")]
pub teleport_id: TeleportConfigID,
pub client_visible: u32,
}

View file

@ -1,5 +1,7 @@
use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(TrainingQuest u32 id);
#[derive(Deserialize, Debug)]
@ -7,5 +9,5 @@ template_id!(TrainingQuest u32 id);
pub struct TrainingQuestTemplate {
pub id: TrainingQuestID,
pub training_type: u32,
pub battle_event_id: u32,
pub battle_event_id: BattleEventConfigID,
}

View file

@ -0,0 +1,25 @@
use std::u32;
use serde::Deserialize;
template_id!(VariableData u32 id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct VariableDataTemplate {
#[serde(rename = "ID")]
pub id: VariableDataID,
#[serde(rename = "BattleEventID")]
pub battle_event_id: u32,
pub variable_name: String,
pub value_type: VariableValueType,
pub initial_value: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VariableValueType {
Int,
Bool,
Float,
}

View file

@ -15,6 +15,7 @@ rbase64.workspace = true
toml.workspace = true
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true
# Database
sqlx.workspace = true

View file

@ -87,5 +87,6 @@ impl CommandManager {
player::procedure "[player_uid] [procedure_id]" "changes current beginner procedure id, parameter -1 can be used for skipping it";
avatar::add "[player_uid] [avatar_id]" "gives avatar with specified id to player";
item::add_weapon "[player_uid] [weapon_id]" "gives weapon with specified id to player";
player::kick "[player_uid] [reason]" "kick the specified player (reason is optional)";
}
}

View file

@ -1,5 +1,5 @@
use data::tables::{AvatarBaseID, ProcedureConfigID};
use proto::PlayerSyncScNotify;
use proto::{DisconnectReason, DisconnectScNotify, PlayerSyncScNotify};
use crate::ServerState;
@ -126,3 +126,45 @@ pub async fn procedure(
"successfully changed procedure_id to {procedure_id:?}"
))
}
pub async fn kick(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: player kick [player_uid]";
if args.len() > 2 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let default_reason = DisconnectReason::ServerKick.into();
let reason = match args.get(1) {
Some(arg) => match arg.parse::<i32>() {
Ok(val) => val,
Err(_err) => default_reason,
},
None => default_reason,
};
let reason_str = match DisconnectReason::try_from(reason) {
Ok(converted_enum) => converted_enum.as_str_name().to_owned(),
Err(_err) => reason.to_string(),
};
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let session_id = player_lock.lock().await.current_session_id();
if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() {
session
.notify(DisconnectScNotify { reason: reason })
.await?;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
session.shutdown().await?;
Ok(format!("kicked player, uid: {uid}, reason: {reason_str}"))
} else {
Ok(format!("player uid: {uid} is not online yet."))
}
}

View file

@ -21,6 +21,7 @@ impl Default for NapGSConfig {
filecfg_path: String::from("assets/FileCfg"),
event_config_path: String::from("assets/EventConfig"),
usm_keys_path: String::from("assets/VideoUSMEncKeys.json"),
gacha_config_path: String::from("assets/GachaConfig"),
},
}
}

View file

@ -2,7 +2,7 @@ use crate::logic::{EOperator, ESystem};
use super::*;
use data::tables;
use data::tables::{self, QuickFuncID};
pub async fn on_get_tips_info(
_session: &NetSession,
@ -21,23 +21,34 @@ pub async fn on_get_client_systems_info(
player: &mut Player,
_req: GetClientSystemsInfoCsReq,
) -> NetResult<GetClientSystemsInfoScRsp> {
Ok(GetClientSystemsInfoScRsp {
retcode: Retcode::RetSucc.into(),
info: Some(ClientSystemsInfo {
post_girl_data: Some(PostGirlData {
selected_post_girl_id_list: tables::post_girl_config_template_tb::iter()
.map(|template| template.id.value())
.collect(),
let mut post_girl_data = PostGirlData {
post_girl_list: tables::post_girl_config_template_tb::iter()
.map(|template| PostGirlItem {
template_id: template.id.value(),
unlock_time: 1000,
unlock_time: 1720052644,
})
.collect(),
..Default::default()
}),
};
if let Some(post_girl_id) = player.basic_data_model.selected_post_girl_id {
post_girl_data
.selected_post_girl_id_list
.push(post_girl_id.value());
}
Ok(GetClientSystemsInfoScRsp {
retcode: Retcode::RetSucc.into(),
info: Some(ClientSystemsInfo {
post_girl_data: Some(post_girl_data),
unlock_data: Some(player.lock_model.to_client()),
hbhfjgbahgf: Some(Aboegnnepmi::default()),
calling_card_data: Some(CallingCardData::default()),
teleport_data: Some(TeleportData {
unlock_id_list: tables::teleport_config_template_tb::iter()
.filter(|template| template.client_visible > 0)
.map(|template| template.teleport_id.value())
.collect(),
..Default::default()
}),
..Default::default()
}),
})
@ -54,6 +65,16 @@ pub async fn on_get_news_stand_data(
})
}
pub async fn on_news_stand_seen(
_session: &NetSession,
_player: &mut Player,
_req: NewsStandSeenCsReq,
) -> NetResult<NewsStandSeenScRsp> {
Ok(NewsStandSeenScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_get_trashbin_hermit_data(
_session: &NetSession,
_player: &mut Player,
@ -173,3 +194,70 @@ pub async fn on_interact_with_scene_object(
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_mod_quick_menu(
_session: &NetSession,
_player: &mut Player,
_req: ModQuickMenuCsReq,
) -> NetResult<ModQuickMenuScRsp> {
let mut quick_access_data_list: Vec<QuickAccessData> = vec![];
for data in _req.quick_access_data_list.iter() {
quick_access_data_list.push(
_player
.lock_model
.mod_quick_access(data.quick_access_index, QuickFuncID::new(data.btn_id)),
);
}
_session
.notify(PlayerSyncScNotify {
client_systems_sync: Some(ClientSystemsSync {
quick_access_data_list: _player.lock_model.quick_access_to_client(),
..Default::default()
}),
..Default::default()
})
.await?;
Ok(ModQuickMenuScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
})
}
pub async fn on_change_post_girl(
_session: &NetSession,
_player: &mut Player,
_req: ChangePostGirlCsReq,
) -> NetResult<ChangePostGirlScRsp> {
if _req.new_selected_post_girl_id_list.len() != 1 {
return Ok(ChangePostGirlScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
};
match tables::PostGirlConfigID::new(*_req.new_selected_post_girl_id_list.get(0).unwrap()) {
Some(post_girl_id) => {
_player.basic_data_model.selected_post_girl_id = Some(post_girl_id);
_session
.notify(PlayerSyncScNotify {
client_systems_sync: Some(ClientSystemsSync {
post_girl_data: Some(PostGirlSync {
selected_post_girl_id_list: vec![post_girl_id.value()],
..Default::default()
}),
..Default::default()
}),
..Default::default()
})
.await?;
Ok(ChangePostGirlScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
})
}
None => Ok(ChangePostGirlScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
}),
}
}

View file

@ -1,13 +1,235 @@
use data::{
gacha::{
gacha_config::{CharacterGachaPool, GachaAddedItemType},
global_gacha_config,
},
tables::{AvatarBaseID, ItemID, WeaponID},
};
use proto::{GainItemInfo, GetGachaDataScRsp};
use super::*;
use crate::{
handlers::core::NetError,
logic::{item::ItemModel, role::RoleModel},
};
use chrono::{DateTime, Local};
pub async fn on_get_gacha_data(
_session: &NetSession,
_player: &mut Player,
req: GetGachaDataCsReq,
) -> NetResult<GetGachaDataScRsp> {
if req.gacha_type != 3 {
// tracing::info!("non-supported gacha type {}", body.gacha_type);
Ok(GetGachaDataScRsp {
retcode: Retcode::RetSucc.into(),
gacha_type: req.gacha_type,
gacha_data: Some(GachaData::default()),
})
} else {
// tracing::info!("construct gacha info");
Ok(GetGachaDataScRsp {
retcode: Retcode::RetSucc.into(),
gacha_type: req.gacha_type,
gacha_data: Some(_player.gacha_model.to_client(&Local::now())),
})
}
}
pub async fn on_do_gacha(
_session: &NetSession,
_player: &mut Player,
req: DoGachaCsReq,
) -> NetResult<DoGachaScRsp> {
let gachaconf = global_gacha_config();
let gacha_model = &mut _player.gacha_model;
let item_model = &mut _player.item_model;
let role_model = &mut _player.role_model;
let pull_time = Local::now();
let target_pool = get_gacha_pool(
&gachaconf.character_gacha_pool_list,
&req.gacha_parent_schedule_id,
&pull_time,
)?;
// tracing::info!("cost_item_count: {}", req.cost_item_count);
let pull_count = if req.cost_item_count > 1 { 10 } else { 1 };
let mut cost_count = gacha_model.get_actual_cost_count(target_pool, &pull_count);
if pull_count != req.cost_item_count {
tracing::info!(
"refuse gacha because: expected cost item {cost_count}, found {}",
req.cost_item_count
);
return Err(NetError::from(Retcode::RetFail));
} else {
// TODO: cost resource
}
let mut gain_item_list: Vec<GainItemInfo> = vec![];
while cost_count > 0 {
let pull_result = gacha_model.perform_pull_pool(&pull_time, target_pool);
let uid = add_item(
role_model,
item_model,
&pull_result.obtained_item_id,
&pull_result.item_type,
)?;
let (mut extra_item_id, mut extra_item_count) = (0, 0);
if let Some(extra_resources) = &pull_result.extra_resources {
(extra_item_id, extra_item_count) = (
extra_resources.extra_item_id.value(),
extra_resources.extra_item_count.clone(),
);
item_model.add_resource(extra_item_id, extra_item_count);
}
gain_item_list.push(GainItemInfo {
item_id: pull_result.obtained_item_id.value(),
extra_item_id,
extra_item_count,
uid,
num: 1,
..GainItemInfo::default()
});
cost_count -= 1;
gacha_model.gacha_records.push(pull_result);
}
_session
.notify(construct_sync(role_model, item_model))
.await?;
Ok(DoGachaScRsp {
retcode: Retcode::RetSucc.into(),
gain_item_list,
gacha_data: Some(gacha_model.to_client(&pull_time)),
cost_item_count: req.cost_item_count,
})
}
pub async fn on_gacha_free_agent(
_session: &NetSession,
_player: &mut Player,
req: GachaFreeAgentCsReq,
) -> NetResult<GachaFreeAgentScRsp> {
let gachaconf = global_gacha_config();
let gacha_model = &mut _player.gacha_model;
let role_model = &mut _player.role_model;
let item_model = &mut _player.item_model;
let pull_time = Local::now();
let target_pool = get_gacha_pool(
&gachaconf.character_gacha_pool_list,
&req.gacha_parent_schedule_id,
&pull_time,
)?;
let item_id = ItemID::new(req.avatar_id);
if let None = item_id {
return Err(NetError::from(Retcode::RetFail));
}
let item_id = item_id.unwrap();
let item_type = gacha_model.request_free_agent(target_pool, &item_id);
if item_type == GachaAddedItemType::None {
return Err(NetError::from(Retcode::RetFail));
}
let _ = add_item(role_model, item_model, &item_id, &item_type);
_session
.notify(construct_sync(role_model, item_model))
.await?;
Ok(GachaFreeAgentScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_choose_gacha_up(
_session: &NetSession,
_player: &mut Player,
req: ChooseGachaUpCsReq,
) -> NetResult<ChooseGachaUpScRsp> {
let gachaconf = global_gacha_config();
let gacha_model = &mut _player.gacha_model;
let pull_time = Local::now();
let target_pool = get_gacha_pool(
&gachaconf.character_gacha_pool_list,
&req.gacha_parent_schedule_id,
&pull_time,
)?;
let item_id = ItemID::new(req.item_id);
if let None = item_id {
return Err(NetError::from(Retcode::RetFail));
}
let item_id = item_id.unwrap();
Ok(ChooseGachaUpScRsp {
retcode: if gacha_model.choose_gacha_up(target_pool, &item_id) {
Retcode::RetSucc.into()
} else {
Retcode::RetFail.into()
},
..Default::default()
})
}
fn get_gacha_pool<'conf>(
character_gacha_pool_list: &'conf Vec<CharacterGachaPool>,
gacha_parent_schedule_id: &u32,
pull_time: &DateTime<Local>,
) -> NetResult<&'conf CharacterGachaPool> {
for target_pool in character_gacha_pool_list.iter() {
if &target_pool.gacha_parent_schedule_id == gacha_parent_schedule_id
&& target_pool.is_still_open(pull_time)
{
return Ok(target_pool);
}
}
tracing::info!(
"refuse gacha op because: pool of parent_schedule_id {} not found or isn't in open time",
gacha_parent_schedule_id
);
Err(NetError::from(Retcode::RetFail))
}
/// Return is item UID (weapon specific)
fn add_item(
role_model: &mut RoleModel,
item_model: &mut ItemModel,
item_id: &ItemID,
item_type: &GachaAddedItemType,
) -> NetResult<u32> {
match item_type {
GachaAddedItemType::Character => match AvatarBaseID::new(item_id.value()) {
Some(avatar_id) => {
role_model.add_avatar(avatar_id);
Ok(0)
}
None => {
tracing::info!("add item failed for avatar id {item_id}");
Err(NetError::from(Retcode::RetFail))
}
},
GachaAddedItemType::Weapon => match WeaponID::new(item_id.value()) {
Some(weapon_id) => Ok(item_model.add_weapon(weapon_id).value()),
None => {
tracing::info!("add item failed for weapon id {item_id}");
Err(NetError::from(Retcode::RetFail))
}
},
GachaAddedItemType::Bangboo => Ok(0),
_ => {
tracing::info!(
"add item failed due to undefined item type (from {item_id}) in configuration"
);
Err(NetError::from(Retcode::RetFail))
}
}
}
fn construct_sync(role_model: &RoleModel, item_model: &ItemModel) -> PlayerSyncScNotify {
PlayerSyncScNotify {
avatar: Some(role_model.avatar_sync()),
item_sync: Some(item_model.item_sync()),
..Default::default()
}
}

View file

@ -98,7 +98,10 @@ req_handlers! {
world::LeaveCurDungeon;
world::InteractWithUnit;
world::EnterSection;
world::ArchiveQuestsSeen;
world::JumpPageSystem;
world::StartHollowQuest;
world::FinishHollowBattleEvent;
world::LongFightProgressUpdate;
client_systems::ReportUiLayoutPlatform;
client_systems::PlayerOperation;
client_systems::UnlockNewbieGroup;
@ -107,6 +110,7 @@ req_handlers! {
client_systems::ReportSystemSettingsChange;
client_systems::InteractWithSceneObject;
client_systems::PlayerSystemParameterChange;
client_systems::NewsStandSeen;
perform::PerformTrigger;
perform::PerformEnd;
perform::PerformJump;
@ -119,6 +123,12 @@ req_handlers! {
event_graph::RunEventGraph;
quest::BeginArchiveBattleQuest;
quest::FinishArchiveQuest;
gacha::DoGacha;
gacha::ChooseGachaUp;
gacha::GachaFreeAgent;
player::ModNickname;
client_systems::ModQuickMenu;
client_systems::ChangePostGirl;
}
notify_handlers! {

View file

@ -58,6 +58,25 @@ pub async fn on_get_player_transaction(
})
}
pub async fn on_mod_nickname(
session: &NetSession,
_player: &mut Player,
_req: ModNicknameCsReq,
) -> NetResult<ModNicknameScRsp> {
_player.basic_data_model.nick_name = Some(_req.nick_name.to_string());
session
.notify(PlayerSyncScNotify {
basic_info: Some(_player.basic_data_model.player_basic_info()),
..Default::default()
})
.await?;
Ok(ModNicknameScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
})
}
pub async fn on_keep_alive(
_session: &NetSession,
_player: &mut Player,

View file

@ -16,13 +16,22 @@ pub async fn on_get_quest_data(
retcode: Retcode::RetSucc.into(),
quest_type: req.quest_type,
quest_data: Some(QuestData {
quest_collection_list: vec![QuestCollection {
quest_collection_list: vec![
QuestCollection {
quest_type: EQuestType::ArchiveFile as u32,
quest_id_list: tables::archive_file_quest_template_tb::iter()
.map(|tmpl| tmpl.id.value())
.collect(),
..Default::default()
}],
},
QuestCollection {
quest_type: EQuestType::Hollow as u32,
quest_id_list: tables::hollow_quest_template_tb::iter()
.map(|tmpl| tmpl.id.value())
.collect(),
..Default::default()
},
],
}),
})
}
@ -35,22 +44,19 @@ pub async fn on_get_yorozuya_info(
Ok(GetYorozuyaInfoScRsp {
retcode: Retcode::RetSucc.into(),
yorozuya_info: Some(YorozuyaInfo {
odohdljfdlf: vec![1001],
apmojjlcooa: vec![1001],
npgjhahijkb: vec![100001, 100101, 100401, 109901],
eoljpdnjgeg: vec![
Ofhlkjeakif {
nnkcanmllod: 199030,
odohdljfdlf: vec![1000, 1001, 1002, 1003, 1004],
apmojjlcooa: vec![1000, 1001, 1002, 1003, 1004],
akiddbalfoa: vec![10010001, 10010002, 10010004],
npgjhahijkb: tables::hollow_config_template_tb::iter()
.map(|tmpl| tmpl.id.value())
.collect(),
eoljpdnjgeg: tables::hollow_quest_template_tb::iter()
.map(|tmpl| Ofhlkjeakif {
nnkcanmllod: tmpl.id.value(),
kkjlnkehddj: Some(Cgpajijemlj::default()),
..Default::default()
},
Ofhlkjeakif {
nnkcanmllod: 199035,
ggnbpiofdpp: 110103,
kkjlnkehddj: Some(Cgpajijemlj::default()),
..Default::default()
},
],
})
.collect(),
..Default::default()
}),
})

View file

@ -1,12 +1,14 @@
use data::{
event,
tables::{ProcedureConfigID, SectionConfigID, TrainingQuestID},
tables::{HollowQuestID, ProcedureConfigID, SectionConfigID, TrainingQuestID},
};
use super::core::NetError;
use crate::{
logic::{game::*, procedure::ProcedureAction, ELocalPlayType, ENPCInteraction},
logic::{
game::*, procedure::ProcedureAction, EHollowQuestType, ELocalPlayType, ENPCInteraction,
},
net::NetSessionState,
};
@ -182,10 +184,42 @@ pub async fn on_start_trial_fighting_mission(
}
pub async fn on_end_battle(
_session: &NetSession,
_player: &mut Player,
_req: EndBattleCsReq,
session: &NetSession,
player: &mut Player,
req: EndBattleCsReq,
) -> NetResult<EndBattleScRsp> {
match &mut player.game_instance {
GameInstance::Hollow(hollow) if hollow.quest_manager.has_active_quests() => {
hollow
.quest_manager
.finish_quest(hollow.battle_event_id.value())
.map_err(LogicError::from)?;
session
.notify(DungeonQuestFinishedScNotify {
result: req.battle_result.unwrap().result as u32,
quest_id: hollow.quest_id,
..Default::default()
})
.await?;
}
GameInstance::LongFight(fight) => {
fight
.quest_manager
.finish_quest(fight.battle_event_id.value())
.map_err(LogicError::from)?;
session
.notify(DungeonQuestFinishedScNotify {
result: req.battle_result.unwrap().result as u32,
quest_id: fight.quest_id,
..Default::default()
})
.await?;
}
_ => (),
};
Ok(EndBattleScRsp {
battle_reward: Some(BattleRewardInfo::default()),
retcode: Retcode::RetSucc.into(),
@ -259,13 +293,79 @@ pub async fn on_enter_section(
})
}
pub async fn on_archive_quests_seen(
pub async fn on_jump_page_system(
_session: &NetSession,
_player: &mut Player,
_req: ArchiveQuestsSeenCsReq,
) -> NetResult<ArchiveQuestsSeenScRsp> {
Ok(ArchiveQuestsSeenScRsp {
_req: JumpPageSystemCsReq,
) -> NetResult<JumpPageSystemScRsp> {
Ok(JumpPageSystemScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
})
}
pub async fn on_start_hollow_quest(
session: &NetSession,
player: &mut Player,
req: StartHollowQuestCsReq,
) -> NetResult<StartHollowQuestScRsp> {
use crate::logic::{TimePeriodType, WeatherType};
let quest_id = HollowQuestID::new(req.quest_id).ok_or(Retcode::RetFail)?;
let quest_type = EHollowQuestType::from(quest_id.template().hollow_quest_type);
match quest_type {
EHollowQuestType::RallyBattle => {
player.game_instance = GameInstance::LongFight(
LongFightGame::create_rally_game(
quest_id,
&req.avatars,
req.buddy_id,
TimePeriodType::from_str(&req.quest_time_period),
WeatherType::from_str(&req.quest_weather),
)
.map_err(LogicError::from)?,
)
}
_ => {
player.game_instance = GameInstance::Hollow(
HollowGame::create_pure_hollow_battle(
quest_id,
&req.avatars,
req.buddy_id,
TimePeriodType::from_str(&req.quest_time_period),
WeatherType::from_str(&req.quest_weather),
)
.map_err(LogicError::from)?,
)
}
}
let world_init_notify = player.game_instance.create_world_init_notify()?;
session.notify(world_init_notify).await?;
Ok(StartHollowQuestScRsp {
retcode: Retcode::RetSucc.into(),
quest_id: 0,
})
}
pub async fn on_finish_hollow_battle_event(
_session: &NetSession,
_player: &mut Player,
_req: FinishHollowBattleEventCsReq,
) -> NetResult<FinishHollowBattleEventScRsp> {
Ok(FinishHollowBattleEventScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_long_fight_progress_update(
_session: &NetSession,
_player: &mut Player,
_req: LongFightProgressUpdateCsReq,
) -> NetResult<LongFightProgressUpdateScRsp> {
Ok(LongFightProgressUpdateScRsp {
retcode: Retcode::RetSucc.into(),
})
}

View file

@ -0,0 +1,67 @@
use std::collections::HashMap;
use data::tables::{BattleEventConfigID, OnceRewardID};
use proto::{FightDropInfo, FightReward, RewardItems};
pub struct FightDropPool {
special_drop: Vec<SpecialReward>,
}
pub struct SpecialReward {
pub reward_id: OnceRewardID,
pub item_count_map: HashMap<u32, u32>,
}
impl FightDropPool {
pub fn new(battle_event_id: BattleEventConfigID) -> Self {
let template = battle_event_id.template();
Self {
special_drop: template
.special_drop
.iter()
.map(|id| SpecialReward::new(*id))
.collect(),
}
}
pub fn to_client(&self) -> FightDropInfo {
FightDropInfo {
normal_drop_list: Vec::new(),
special_drop_list: self
.special_drop
.iter()
.map(SpecialReward::to_client)
.collect(),
..Default::default()
}
}
}
impl SpecialReward {
pub fn new(template_id: OnceRewardID) -> Self {
let template = template_id.template();
Self {
reward_id: template_id,
item_count_map: template
.reward_list
.iter()
.map(|r| (r.reward_item_id, r.reward_amount))
.collect(),
}
}
pub fn to_client(&self) -> FightReward {
FightReward {
reward_id: self.reward_id.value(),
fight_reward_map: HashMap::from([(
0,
RewardItems {
reward_item_map: self.item_count_map.clone(),
},
)]),
..Default::default()
}
}
}

View file

@ -0,0 +1,2 @@
mod fight_drop;
pub use fight_drop::FightDropPool;

View file

@ -1,7 +1,9 @@
mod avatar;
mod buddy;
pub mod drop;
mod quest;
mod team;
pub mod unit;
mod variable;
pub use avatar::InLevelAvatarDataItem;
pub use buddy::EquippedBuddyDataItem;
pub use team::TeamDataItem;
pub use quest::{DungeonQuestError, DungeonQuestManager};
pub use team::{BuddyParam, TeamDataItem};
pub use variable::LogicVariableTable;

View file

@ -0,0 +1,44 @@
use data::tables::BattleGroupConfigID;
use proto::DungeonQuestInfo;
use thiserror::Error;
#[derive(Default)]
pub struct DungeonQuestManager {
inner_quests: Vec<u32>,
}
#[derive(Error, Debug)]
pub enum DungeonQuestError {
#[error("dungeon inner quest with id {0} is not active")]
QuestNotActive(u32),
}
impl DungeonQuestManager {
pub fn new_for_battle_group(battle_group_id: BattleGroupConfigID) -> Self {
Self {
inner_quests: vec![battle_group_id.template().battle_event_id.value()],
}
}
pub fn finish_quest(&mut self, quest_id: u32) -> Result<(), DungeonQuestError> {
let idx = self
.inner_quests
.iter()
.position(|id| *id == quest_id)
.ok_or(DungeonQuestError::QuestNotActive(quest_id))?;
self.inner_quests.remove(idx);
Ok(())
}
pub fn has_active_quests(&self) -> bool {
!self.inner_quests.is_empty()
}
pub fn to_client(&self) -> DungeonQuestInfo {
DungeonQuestInfo {
inner_quest_id_list: self.inner_quests.clone(),
..Default::default()
}
}
}

View file

@ -2,30 +2,33 @@ use std::collections::HashMap;
use crate::logic::BuddyTeamType;
use super::{EquippedBuddyDataItem, InLevelAvatarDataItem};
use super::unit::{AvatarUnit, BuddyUnit};
pub struct TeamDataItem {
pub avatar_member_list: Vec<InLevelAvatarDataItem>,
pub equipped_buddy_list: Vec<EquippedBuddyDataItem>,
pub avatar_member_list: Vec<AvatarUnit>,
pub equipped_buddy_list: Vec<BuddyUnit>,
}
pub struct BuddyParam(pub u32, pub BuddyTeamType);
impl TeamDataItem {
pub fn new(avatars: &[u32], buddy_id: u32) -> Self {
pub fn new(avatars: &[u32], buddy_params: &[BuddyParam]) -> Self {
Self {
avatar_member_list: avatars
.iter()
.map(|id| InLevelAvatarDataItem {
.map(|id| AvatarUnit {
avatar_id: *id,
mp_property_override: HashMap::new(),
})
.collect(),
equipped_buddy_list: (buddy_id != 0)
.then_some(vec![EquippedBuddyDataItem {
buddy_id,
buddy_team: BuddyTeamType::Assisting,
equipped_buddy_list: buddy_params
.iter()
.map(|param| BuddyUnit {
buddy_id: param.0,
buddy_team: param.1,
override_property_map: HashMap::new(),
}])
.unwrap_or_default(),
})
.collect(),
}
}
}

View file

@ -1,16 +1,16 @@
use std::collections::HashMap;
use crate::logic::BaseProperty;
use proto::InLevelAvatarData;
use proto::AvatarUnitInfo;
pub struct InLevelAvatarDataItem {
pub struct AvatarUnit {
pub avatar_id: u32,
pub mp_property_override: HashMap<BaseProperty, i32>,
}
impl InLevelAvatarDataItem {
pub fn to_client(&self) -> InLevelAvatarData {
InLevelAvatarData {
impl AvatarUnit {
pub fn to_client(&self) -> AvatarUnitInfo {
AvatarUnitInfo {
avatar_id: self.avatar_id,
mp_property_override_map: self
.mp_property_override

View file

@ -1,18 +1,18 @@
use std::collections::HashMap;
use proto::EquippedBuddyData;
use proto::BuddyUnitInfo;
use crate::logic::{BaseProperty, BuddyTeamType};
pub struct EquippedBuddyDataItem {
pub struct BuddyUnit {
pub buddy_id: u32,
pub buddy_team: BuddyTeamType,
pub override_property_map: HashMap<BaseProperty, i32>,
}
impl EquippedBuddyDataItem {
pub fn to_client(&self) -> EquippedBuddyData {
EquippedBuddyData {
impl BuddyUnit {
pub fn to_client(&self) -> BuddyUnitInfo {
BuddyUnitInfo {
buddy_id: self.buddy_id,
r#type: self.buddy_team.to_protocol().into(),
mp_property_override_map: self

View file

@ -0,0 +1,5 @@
mod avatar;
mod buddy;
pub use avatar::AvatarUnit;
pub use buddy::BuddyUnit;

View file

@ -0,0 +1,74 @@
use std::collections::HashMap;
use data::tables::{self, BattleEventConfigID, VariableDataID, VariableValueType};
use proto::FightVariable;
pub struct LogicVariableTable {
variable_map: HashMap<String, LogicVariable>,
}
impl LogicVariableTable {
pub fn new(battle_event_id: BattleEventConfigID) -> Self {
let event_id = battle_event_id.value();
Self {
variable_map: tables::variable_data_template_tb::iter()
.filter(|tmpl| tmpl.battle_event_id == event_id)
.map(|tmpl| (tmpl.variable_name.clone(), LogicVariable::new(tmpl.id)))
.collect(),
}
}
pub fn to_client(&self) -> HashMap<String, FightVariable> {
self.variable_map
.iter()
.map(|(name, var)| (name.clone(), var.to_client()))
.collect()
}
}
pub enum LogicVariable {
Int(i64),
Float(f64),
Bool(bool),
}
impl LogicVariable {
pub fn new(id: VariableDataID) -> Self {
let template = id.template();
match template.value_type {
VariableValueType::Int => Self::Int(template.initial_value.parse().unwrap_or_default()),
VariableValueType::Bool => Self::Bool(
template
.initial_value
.to_lowercase()
.parse()
.unwrap_or_default(),
),
VariableValueType::Float => {
Self::Float(template.initial_value.parse().unwrap_or_default())
}
}
}
pub fn to_client(&self) -> FightVariable {
match self {
Self::Int(val) => FightVariable {
r#type: 1,
int_value: *val,
..Default::default()
},
Self::Bool(val) => FightVariable {
r#type: 4,
int_value: *val as i64,
..Default::default()
},
Self::Float(val) => FightVariable {
r#type: 2,
float_value: *val,
..Default::default()
},
}
}
}

View file

@ -12,6 +12,7 @@ pub enum ESceneType {
Fight = 3,
Fresh = 4,
MultiFight = 5,
LongFight = 7,
}
#[allow(dead_code)]
@ -26,6 +27,28 @@ pub enum EQuestType {
HollowChallenge = 6,
ArchiveBattle = 7,
Knowledge = 8,
Daily = 9,
}
#[allow(dead_code)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)]
#[repr(u32)]
pub enum EHollowQuestType {
ChallengeChaos = 6,
BossRushBattle = 14,
World = 8,
MainQuest = 1,
SideQuest = 2,
#[default]
Common = 0,
Challenge = 5,
PromoteBattle = 11,
DifficutyBattle = 12,
AvatarSide = 7,
RallyBattle = 13,
Urgent = 3,
UrgentSupplement = 4,
NormalBattle = 10,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, FromPrimitive)]
@ -102,6 +125,17 @@ pub enum TimePeriodType {
Night = 2,
}
impl TimePeriodType {
pub fn from_str(s: &str) -> Self {
match s {
"Morning" => Self::Morning,
"Evening" => Self::Evening,
"Night" => Self::Night,
_ => Self::Morning,
}
}
}
impl Display for TimePeriodType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
@ -122,6 +156,21 @@ pub enum WeatherType {
None = 0xFFFFFFFF,
}
impl WeatherType {
pub fn from_str(s: &str) -> Self {
match s {
"SunShine" => Self::SunShine,
"Fog" => Self::Fog,
"Cloudy" => Self::Cloudy,
"Rain" => Self::Rain,
"Thunder" => Self::Thunder,
"ThickFog" => Self::ThickFog,
"ThickCloudy" => Self::ThickCloudy,
_ => Self::SunShine,
}
}
}
impl Display for WeatherType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
@ -264,7 +313,7 @@ pub enum BaseProperty {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum BuddyTeamType {
Unknown = 0,
RallyGuidance = 0,
Fighting = 1,
Assisting = 2,
}
@ -272,7 +321,7 @@ pub enum BuddyTeamType {
impl BuddyTeamType {
pub fn to_protocol(&self) -> ::proto::BuddyTeamType {
match *self {
Self::Unknown => ::proto::BuddyTeamType::Unknown,
Self::RallyGuidance => ::proto::BuddyTeamType::RallyGuidance,
Self::Fighting => ::proto::BuddyTeamType::Fighting,
Self::Assisting => ::proto::BuddyTeamType::Assisting,
}

View file

@ -0,0 +1,440 @@
use data::gacha::gacha_config::*;
use data::gacha::global_gacha_config;
use data::tables::ItemID;
use chrono::{DateTime, Local};
use proto::{Gacha, GachaData, GachaPool, NeedItemInfo};
use std::{
cmp::min,
collections::{
hash_map::Entry::{Occupied, Vacant},
HashSet,
},
};
use super::GachaModel;
impl GachaModel {
pub fn to_client(&self, now: &DateTime<Local>) -> GachaData {
let gachaconf = global_gacha_config();
let mut gacha_list: Vec<Gacha> = vec![];
for target_pool in gachaconf.character_gacha_pool_list.iter() {
if target_pool.is_still_open(now) {
gacha_list.push(
self.generate_gacha_info_from_pool(target_pool, &gachaconf.common_properties),
);
}
}
// tracing::info!("gacha_list: {:?}", gacha_list);
GachaData {
random_number: 6167,
gacha_pool: Some(GachaPool { gacha_list }),
..GachaData::default()
}
}
fn generate_gacha_info_from_pool(
&self,
target_pool: &CharacterGachaPool,
common_properties: &GachaCommonProperties,
) -> Gacha {
let gachaconf = data::gacha::global_gacha_config();
let sharing_guarantee_category_tag = &target_pool.sharing_guarantee_info_category;
let status_bin = self
.gacha_status_map
.get(sharing_guarantee_category_tag)
.unwrap();
let pity_s = status_bin
.rarity_status_map
.get(&common_properties.s_item_rarity)
.unwrap()
.pity;
let pity_a = status_bin
.rarity_status_map
.get(&common_properties.a_item_rarity)
.unwrap()
.pity;
let mut discount_ten_roll_prize: u32 = 0;
let mut discount_avaliable_num: u32 = 0;
let mut advanced_s_guarantee: u32 = 0;
let mut free_select_progress: u32 = 0;
let mut free_select_required_pull: u32 = 0;
let mut free_select_policy: Option<&FreeSelectItem> = None;
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if common_properties.newcomer_advanced_s_tag == *discount_policy_tag {
let policy = gachaconf
.discount_policies
.advanced_guarantee_map
.get(discount_policy_tag)
.unwrap();
if status_bin
.discount_usage_map
.get(discount_policy_tag)
.unwrap()
< &policy.use_limit
{
advanced_s_guarantee = policy.guarantee_pity - pity_s + 1;
}
} else if common_properties.ten_pull_discount_tag == *discount_policy_tag {
let policy = gachaconf
.discount_policies
.ten_pull_discount_map
.get(discount_policy_tag)
.unwrap();
let discount_usage = status_bin
.discount_usage_map
.get(discount_policy_tag)
.unwrap();
if discount_usage < &policy.use_limit {
discount_ten_roll_prize = policy.discounted_prize;
discount_avaliable_num = policy.use_limit - discount_usage;
}
} else if gachaconf
.discount_policies
.free_select_map
.contains_key(discount_policy_tag)
{
let policy = gachaconf
.discount_policies
.free_select_map
.get(discount_policy_tag)
.unwrap();
let free_select_demand_idx = usize::try_from(
*(status_bin
.discount_usage_map
.get(&policy.free_select_usage_record_tag)
.unwrap()),
)
.unwrap();
if policy.milestones.len() <= free_select_demand_idx {
continue;
}
let free_select_actual_progress = status_bin
.discount_usage_map
.get(&policy.free_select_progress_record_tag)
.unwrap();
free_select_policy = Some(policy);
free_select_required_pull = policy
.milestones
.get(free_select_demand_idx)
.unwrap()
.to_owned();
free_select_progress = min(free_select_required_pull, *free_select_actual_progress);
}
}
let mut up_s_item_list: Vec<u32> = vec![];
let mut up_a_item_list: Vec<u32> = vec![];
let mut free_select_item_list: Vec<u32> = vec![];
let mut chooseable_up_list: Vec<u32> = vec![];
let mut chosen_up_item: u32 = 0;
let mut s_guarantee: u32 = 0;
let mut a_guarantee: u32 = 0;
for rarity_items in target_pool.gacha_items.iter() {
let mut chooseable_up_included_category_tags: Option<&HashSet<String>> = None;
let mut chooseable_policy_tag: Option<&String> = None;
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
chooseable_policy_tag = Some(guarantee_policy_tag);
chooseable_up_included_category_tags =
Some(&category_guarantee_policy.included_category_tags);
if let Some(item) = status_bin
.rarity_status_map
.get(&rarity_items.rarity)
.unwrap()
.categories_chosen_guarantee_item_map
.get(guarantee_policy_tag)
{
chosen_up_item = item.clone();
}
}
for (category_tag, category) in rarity_items.categories.iter() {
let probability_model = gachaconf
.probability_model_map
.get(&rarity_items.probability_model_tag)
.unwrap();
let maximum_pity = &probability_model.maximum_guarantee_pity;
if rarity_items.rarity == common_properties.s_item_rarity {
if category.is_promotional_items {
up_s_item_list = category.item_ids.clone();
}
// tracing::info!("pity_s: {pity_s}");
// thread 'tokio-runtime-worker' panicked at nap_gameserver\src\handlers\gacha.rs:369:31:
// attempt to subtract with overflow
s_guarantee = maximum_pity - min(pity_s, maximum_pity.clone()) + 1;
}
if rarity_items.rarity == common_properties.a_item_rarity {
if category.is_promotional_items {
up_a_item_list = category.item_ids.clone();
}
// tracing::info!("pity_a: {pity_a}");
a_guarantee = maximum_pity - min(pity_a, maximum_pity.clone()) + 1;
}
if let Some(val) = free_select_policy {
if val.rarity == rarity_items.rarity && val.category_tags.contains(category_tag)
{
free_select_item_list.append(&mut category.item_ids.clone());
}
}
if let Some(tags) = chooseable_up_included_category_tags {
if tags.contains(category_tag) {
chooseable_up_list.append(&mut category.item_ids.clone());
}
}
}
if let Some(_priority_policy_tag) = chooseable_policy_tag {
// if let Some(item) = status_bin
// .rarity_status_map
// .get(&rarity_items.rarity)
// .unwrap()
// .categories_chosen_guarantee_item_map
// .get(priority_policy_tag)
// {
if rarity_items.rarity == gachaconf.common_properties.s_item_rarity {
up_s_item_list = chooseable_up_list.clone();
} else if rarity_items.rarity == gachaconf.common_properties.a_item_rarity {
up_a_item_list = vec![];
}
// }
}
}
let need_item_info_list: Vec<NeedItemInfo> = vec![NeedItemInfo {
need_item_id: target_pool.cost_item_id,
need_item_count: 1,
}];
let mut result = Gacha {
gacha_schedule_id: target_pool.gacha_schedule_id,
gacha_parent_schedule_id: target_pool.gacha_parent_schedule_id,
gacha_type: target_pool.gacha_type,
start_timestamp: target_pool.start_time.timestamp(),
end_timestamp: target_pool.end_time.timestamp(),
discount_avaliable_num,
discount_ten_roll_prize,
advanced_s_guarantee,
s_guarantee,
a_guarantee,
need_item_info_list,
free_select_progress,
free_select_required_pull,
free_select_item_list,
chosen_up_item,
// nammdglepbk: 563,
// hgmcofcjmbg: 101,
// akggbhgkifd: chooseable_up_list.clone(),
chooseable_up_list,
..Gacha::default()
};
if up_s_item_list.len() > 0 {
result.up_s_item_list = up_s_item_list;
}
if up_a_item_list.len() > 0 {
result.up_a_item_list = up_a_item_list;
}
result
}
/// Get the actual item cost count (counting discount).
pub fn get_actual_cost_count<'bin, 'conf>(
&'bin mut self,
target_pool: &'conf CharacterGachaPool,
pull_count: &u32,
) -> u32 {
let gachaconf = global_gacha_config();
if *pull_count == 10 {
let discount_tag = &gachaconf.common_properties.ten_pull_discount_tag;
if target_pool.discount_policy_tags.contains(&discount_tag) {
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let discount_policy = gachaconf
.discount_policies
.ten_pull_discount_map
.get(discount_tag)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(discount_tag).unwrap();
if *usage < discount_policy.use_limit {
*usage += 1;
return discount_policy.discounted_prize;
}
}
}
pull_count.clone()
}
pub fn request_free_agent<'bin, 'conf>(
&'bin mut self,
target_pool: &'conf CharacterGachaPool,
item_id: &ItemID,
) -> GachaAddedItemType {
let gachaconf = global_gacha_config();
let sharing_guarantee_category_tag = &target_pool.sharing_guarantee_info_category;
let status_bin = self
.gacha_status_map
.get_mut(sharing_guarantee_category_tag)
.unwrap();
let item_id = item_id.value();
let mut free_select_policy: Option<&FreeSelectItem> = None;
let mut free_select_progress: u32 = 0;
let mut free_select_required_pull: u32 = 0;
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if gachaconf
.discount_policies
.free_select_map
.contains_key(discount_policy_tag)
{
let policy = gachaconf
.discount_policies
.free_select_map
.get(discount_policy_tag)
.unwrap();
let free_select_demand_idx = usize::try_from(
*(status_bin
.discount_usage_map
.get(&policy.free_select_usage_record_tag)
.unwrap()),
)
.unwrap();
if policy.milestones.len() <= free_select_demand_idx {
continue;
}
let free_select_actual_progress = status_bin
.discount_usage_map
.get(&policy.free_select_progress_record_tag)
.unwrap();
free_select_policy = Some(policy);
free_select_required_pull = policy
.milestones
.get(free_select_demand_idx)
.unwrap()
.to_owned();
free_select_progress = min(free_select_required_pull, *free_select_actual_progress);
}
}
if let None = free_select_policy {
tracing::info!(
"refuse free agent because: pool of parent_schedule_id {} hasn't defined free agent discount yet (or used up chance)",
target_pool.gacha_parent_schedule_id
);
return GachaAddedItemType::None;
} else if free_select_progress < free_select_required_pull {
tracing::info!(
"refuse free agent because: use pulled {free_select_progress} (after last free agent) in parent_schedule_id {}, required {free_select_required_pull}",
target_pool.gacha_parent_schedule_id
);
return GachaAddedItemType::None;
}
let free_select_policy = free_select_policy.unwrap();
let mut item_type: GachaAddedItemType = GachaAddedItemType::None;
for rarity_items in target_pool.gacha_items.iter() {
if rarity_items.rarity != free_select_policy.rarity {
continue;
}
for (category_tag, category) in rarity_items.categories.iter() {
if !free_select_policy.category_tags.contains(category_tag) {
continue;
}
if category.item_ids.contains(&item_id) {
item_type = category.item_type.clone();
}
}
}
if item_type != GachaAddedItemType::None {
(*status_bin
.discount_usage_map
.get_mut(&free_select_policy.free_select_usage_record_tag)
.unwrap()) += 1;
(*status_bin
.discount_usage_map
.get_mut(&free_select_policy.free_select_progress_record_tag)
.unwrap()) -= free_select_required_pull;
}
item_type
}
pub fn choose_gacha_up<'bin, 'conf>(
&'bin mut self,
target_pool: &'conf CharacterGachaPool,
item_id: &ItemID,
) -> bool {
let gachaconf = global_gacha_config();
let item_id = item_id.value();
for rarity_items in target_pool.gacha_items.iter() {
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
let mut up_category: Option<&String> = None;
for (category_tag, category) in rarity_items.categories.iter() {
if category.item_ids.contains(&item_id) {
up_category = Some(category_tag);
break;
}
}
if let None = up_category {
continue;
};
let up_category = up_category.unwrap();
let progress_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap()
.rarity_status_map
.get_mut(&rarity_items.rarity)
.unwrap();
match progress_bin
.categories_chosen_guarantee_item_map
.entry(guarantee_policy_tag.clone())
{
Occupied(mut occupied_entry) => {
occupied_entry.insert(item_id);
}
Vacant(vacant_entry) => {
vacant_entry.insert(item_id);
}
};
match progress_bin
.categories_chosen_guarantee_category_map
.entry(up_category.clone())
{
Occupied(mut occupied_entry) => {
occupied_entry.insert(up_category.clone());
}
Vacant(vacant_entry) => {
vacant_entry.insert(up_category.clone());
}
};
return true;
}
}
false
}
}

View file

@ -0,0 +1,531 @@
use super::record::*;
use super::stat::*;
use data::gacha;
use data::gacha::gacha_config::*;
use data::tables::ItemID;
use chrono::{DateTime, Local};
use proto::GachaModelBin;
use rand::{thread_rng, Rng};
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasher, Hash};
pub struct GachaModel {
pub gacha_status_map: HashMap<String, GachaStatus>,
pub gacha_records: Vec<GachaRecord>,
}
impl Default for GachaModel {
fn default() -> GachaModel {
let result = GachaModel {
gacha_status_map: HashMap::new(),
gacha_records: vec![],
};
result.post_deserialize()
}
}
impl GachaModel {
pub fn from_bin(gacha_bin: GachaModelBin) -> Self {
let result = Self {
gacha_status_map: gacha_bin
.gacha_status_map
.into_iter()
.map(|(k, v)| (k, GachaStatus::from_bin(v)))
.collect(),
gacha_records: gacha_bin
.gacha_records
.into_iter()
.map(|x| GachaRecord::from_bin(x))
.collect(),
};
result.post_deserialize()
}
pub fn to_bin(&self) -> GachaModelBin {
GachaModelBin {
gacha_status_map: self
.gacha_status_map
.iter()
.map(|(k, v)| (k.clone(), v.to_bin()))
.collect(),
gacha_records: self.gacha_records.iter().map(|x| x.to_bin()).collect(),
..Default::default()
}
}
pub fn post_deserialize(mut self) -> GachaModel {
let gachaconf = gacha::global_gacha_config();
for gacha_pool in gachaconf.character_gacha_pool_list.iter() {
let mut gacha_status_map = &mut self.gacha_status_map;
let status_bin = get_or_add(
&mut gacha_status_map,
&gacha_pool.sharing_guarantee_info_category,
);
for rarity_items in gacha_pool.gacha_items.iter() {
let progress_bin =
get_or_add(&mut status_bin.rarity_status_map, &rarity_items.rarity);
if progress_bin.pity <= 0 {
progress_bin.pity = 1;
}
for category_guarantee_policy_tag in
rarity_items.category_guarantee_policy_tags.iter()
{
get_or_add(
&mut progress_bin.categories_progress_map,
&category_guarantee_policy_tag,
);
let guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(category_guarantee_policy_tag)
.unwrap();
if !guarantee_policy.chooseable {
continue;
}
get_or_add(
&mut progress_bin.categories_chosen_guarantee_progress_map,
&category_guarantee_policy_tag,
);
}
}
for discount_policy_tag in gacha_pool.discount_policy_tags.iter() {
if gachaconf
.discount_policies
.free_select_map
.contains_key(discount_policy_tag)
{
let policy = gachaconf
.discount_policies
.free_select_map
.get(discount_policy_tag)
.unwrap();
get_or_add(
&mut status_bin.discount_usage_map,
&policy.free_select_progress_record_tag,
);
get_or_add(
&mut status_bin.discount_usage_map,
&policy.free_select_usage_record_tag,
);
} else {
get_or_add(&mut status_bin.discount_usage_map, &discount_policy_tag);
}
}
}
self
}
pub fn perform_pull_pool<'bin, 'conf>(
&'bin mut self,
pull_time: &DateTime<Local>,
target_pool: &'conf CharacterGachaPool,
) -> GachaRecord {
let (rarity_items, progress_bin, status_bin, probability_model) =
self.determine_rarity(target_pool);
let (category_tag, category) =
self.determine_category(rarity_items, progress_bin, target_pool);
let result = determine_gacha_result(
pull_time,
category,
target_pool,
status_bin,
progress_bin,
rarity_items,
);
self.update_pity(rarity_items, probability_model, target_pool);
self.update_category_guarantee_info(rarity_items, &category_tag, target_pool);
self.update_discount(target_pool, &category_tag, rarity_items);
result
}
fn rand_rarity<'bin, 'conf>(
&'bin self,
target_pool: &'conf CharacterGachaPool,
status_bin: &'bin GachaStatus,
) -> (
&'conf GachaAvailableItemsInfo,
&'bin GachaProgress,
&'conf ProbabilityModel,
) {
let gachaconf = gacha::global_gacha_config();
let mut rng = thread_rng();
let rarity_status_map = &status_bin.rarity_status_map;
// gacha_items is already sorted by rarity descendingly in its post_configure.
for rarity_items in target_pool.gacha_items.iter() {
// Surely any judgement should be made on the current pity.
let progress_bin = rarity_status_map.get(&rarity_items.rarity).unwrap();
let pity = progress_bin.pity;
let probability_model = gachaconf
.probability_model_map
.get(&rarity_items.probability_model_tag)
.unwrap();
if rng.gen_range(0.0..100.0) <= probability_model.get_chance_percent(&pity) {
return (rarity_items, progress_bin, probability_model);
}
}
panic!("The user failed to get any items.");
}
fn determine_rarity<'bin, 'conf>(
&'bin self,
target_pool: &'conf CharacterGachaPool,
) -> (
&'conf GachaAvailableItemsInfo,
&'bin GachaProgress,
&'bin GachaStatus,
&'conf ProbabilityModel,
) {
let gachaconf = gacha::global_gacha_config();
let status_bin = self
.gacha_status_map
.get(&target_pool.sharing_guarantee_info_category)
.expect(&format!(
"post_deserialize forgot StatusBin/sharing_guarantee_info_category: {}",
target_pool.sharing_guarantee_info_category
));
let (mut rarity_items, mut progress_bin, mut probability_model) =
self.rand_rarity(target_pool, &status_bin);
// We should take AdvancedGuarantee discount into consideration.
for discount_tag in target_pool.discount_policy_tags.iter() {
if let Some(discount) = gachaconf
.discount_policies
.advanced_guarantee_map
.get(discount_tag)
{
if discount.rarity <= rarity_items.rarity {
continue;
}
if status_bin
.discount_usage_map
.get(discount_tag)
.expect(&format!(
"post_deserialize forgot StatusBin/discount_usage_map: {}",
discount_tag
))
>= &discount.use_limit
{
continue;
}
let higher_progress_bin = status_bin
.rarity_status_map
.get(&discount.rarity)
.expect(&format!(
"post_deserialize forgot StatusBin/rarity_status_map: {}",
&discount.rarity
));
if higher_progress_bin.pity >= discount.guarantee_pity {
let mut found_rarity_items = false;
for gacha_items in target_pool.gacha_items.iter() {
if gacha_items.rarity == discount.rarity {
rarity_items = gacha_items;
probability_model = gachaconf
.probability_model_map
.get(&gacha_items.probability_model_tag)
.unwrap();
found_rarity_items = true;
break;
}
}
assert!(found_rarity_items, "Handle AdvancedGuarantee Discount ({discount_tag}) error: The target rarity does not exist in this pool.");
progress_bin = higher_progress_bin;
}
}
}
(rarity_items, progress_bin, status_bin, probability_model)
}
fn determine_category<'bin, 'conf>(
&'bin self,
rarity_items: &'conf GachaAvailableItemsInfo,
progress_bin: &'bin GachaProgress,
target_pool: &'conf CharacterGachaPool,
) -> (String, &'conf GachaCategoryInfo) {
let gachaconf = gacha::global_gacha_config();
let mut category_tag_inited = false;
let mut category_tag_result: HashSet<String> = HashSet::new();
// First of all, if there's a chooseable category and
// it is SELECTED then we MUST give that category's item.
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
// As we found a policy defined chooseable, we
// should head to look whether the user chose
// the category he want.
if let Some(category_tag) = progress_bin
.categories_chosen_guarantee_category_map
.get(guarantee_policy_tag)
{
// User chose a category; our work are done here.
category_tag_result.insert(category_tag.clone());
category_tag_inited = true;
}
}
// Then we should take a look at MustGainItem.
if !category_tag_inited {
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if let Some(discount) = gachaconf
.discount_policies
.must_gain_item_map
.get(discount_policy_tag)
{
if discount.rarity != rarity_items.rarity {
continue;
}
category_tag_result.insert(discount.category_tag.clone());
category_tag_inited = true;
}
}
}
// Otherwise, just select as normal.
if !category_tag_inited {
for tag in rarity_items.categories.keys() {
category_tag_result.insert(tag.clone());
}
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
let failure_times = progress_bin.categories_progress_map
.get(guarantee_policy_tag)
.expect(&format!("post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}", &rarity_items.rarity, guarantee_policy_tag));
if failure_times >= &category_guarantee_policy.trigger_on_failure_times {
category_tag_result = category_tag_result
.intersection(&category_guarantee_policy.included_category_tags)
.cloned()
.collect();
}
}
// category_tag_inited = true;
}
let mut categories: Vec<(String, &GachaCategoryInfo)> = vec![];
let mut weight_sum = 0;
for result_tag in category_tag_result {
let category = rarity_items.categories.get(&result_tag).unwrap();
categories.push((result_tag, category));
weight_sum += category.category_weight;
}
let randomnum = rand::thread_rng().gen_range(0..weight_sum);
let mut enumerated_ranges_end = 0;
for category in categories.into_iter() {
if randomnum <= enumerated_ranges_end + category.1.category_weight {
return (category.0, category.1);
}
enumerated_ranges_end += category.1.category_weight;
}
panic!("No category is chosen.");
}
fn update_pity<'bin, 'conf>(
&'bin mut self,
rarity_items: &'conf GachaAvailableItemsInfo,
probability_model: &'conf ProbabilityModel,
target_pool: &'conf CharacterGachaPool,
) {
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
for (rarity, rarity_status) in status_bin.rarity_status_map.iter_mut() {
if (rarity == &rarity_items.rarity)
|| (probability_model.clear_status_on_higher_rarity_pulled
&& rarity < &rarity_items.rarity)
{
rarity_status.pity = 1;
} else {
rarity_status.pity += 1;
}
}
}
fn update_category_guarantee_info<'bin, 'conf>(
&'bin mut self,
rarity_items: &'conf GachaAvailableItemsInfo,
category_tag: &String,
target_pool: &'conf CharacterGachaPool,
) {
let gachaconf = gacha::global_gacha_config();
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let progress_bin = status_bin
.rarity_status_map
.get_mut(&rarity_items.rarity)
.unwrap();
for policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let policy = gachaconf
.category_guarantee_policy_map
.get(policy_tag)
.unwrap();
// TODO: Chooseable guarantee not implemented
let prev_failure = progress_bin
.categories_progress_map
.get_mut(policy_tag)
.expect(&format!(
"post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}",
rarity_items.rarity, policy_tag
));
if policy.included_category_tags.contains(category_tag) {
*prev_failure = 0;
} else {
*prev_failure += 1;
}
}
}
fn update_discount<'bin, 'conf>(
&'bin mut self,
target_pool: &'conf CharacterGachaPool,
category_tag: &String,
rarity_items: &GachaAvailableItemsInfo,
) {
let gachaconf = gacha::global_gacha_config();
for (policy_tag, policy) in gachaconf.discount_policies.must_gain_item_map.iter() {
if *category_tag != policy.category_tag {
continue;
}
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap();
if *usage < policy.use_limit {
*usage += 1;
}
}
for (policy_tag, policy) in gachaconf.discount_policies.advanced_guarantee_map.iter() {
if rarity_items.rarity != policy.rarity {
continue;
}
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap();
if *usage < policy.use_limit {
*usage += 1;
}
}
for (policy_tag, policy) in gachaconf.discount_policies.free_select_map.iter() {
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let progress = status_bin
.discount_usage_map
.get_mut(&policy.free_select_progress_record_tag)
.unwrap();
*progress += 1;
}
}
}
fn get_or_add<'a, K: Eq + PartialEq + Hash + Clone, V: Default, S: BuildHasher>(
map: &'a mut HashMap<K, V, S>,
key: &K,
) -> &'a mut V {
if !map.contains_key(key) {
map.insert(key.clone(), V::default());
}
map.get_mut(key).unwrap()
}
fn determine_gacha_result<'bin, 'conf>(
pull_time: &DateTime<Local>,
category: &'conf GachaCategoryInfo,
target_pool: &'conf CharacterGachaPool,
status_bin: &'bin GachaStatus,
progress_bin: &'bin GachaProgress,
rarity_items: &'conf GachaAvailableItemsInfo,
) -> GachaRecord {
let gachaconf = gacha::global_gacha_config();
let item_pool_len = category.item_ids.len() as u32;
let mut item_id: Option<&u32> = None;
// We should see whether user's search priority exists.
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
// Firstly, judge whether the user failed enough times.
// The user is limited to get only this category's item,
// so we should record the user's failure to get his
// selected item elsewhere.
if progress_bin
.categories_chosen_guarantee_progress_map
.get(guarantee_policy_tag)
.unwrap()
< &category_guarantee_policy.trigger_on_failure_times
{
continue;
}
// We directly look whether user chose an UP item.
if let Some(item) = progress_bin
.categories_chosen_guarantee_item_map
.get(guarantee_policy_tag)
{
item_id = Some(item);
}
}
let item_id = match item_id {
Some(val) => val,
None => category
.item_ids
.get(rand::thread_rng().gen_range(0..item_pool_len) as usize)
.unwrap(),
};
let mut extra_item_id: Option<ItemID> = None;
let mut extra_item_count: u32 = 0;
for extra_items_policy_tag in rarity_items.extra_items_policy_tags.iter() {
let extra_items_policy = gachaconf
.extra_items_policy_map
.get(extra_items_policy_tag)
.unwrap();
// TODO: apply_on_owned_count in a context with bag
// TODO: That's what RoleModel should do, not me.
if extra_items_policy.apply_on_owned_count == 0 {
extra_item_id = ItemID::new(extra_items_policy.id);
extra_item_count = extra_items_policy.count;
}
}
let extra_resources = match extra_item_id {
Some(item_id) => Some(GachaExtraResources {
extra_item_id: item_id,
extra_item_count,
}),
None => None,
};
GachaRecord {
pull_timestamp: pull_time.timestamp(),
obtained_item_id: ItemID::new_unchecked(item_id.clone()),
gacha_id: target_pool.gacha_schedule_id.clone(),
progress_map: status_bin.rarity_status_map.clone(),
extra_resources,
item_type: category.item_type.clone(),
}
}

View file

@ -0,0 +1,6 @@
mod client_op;
mod gacha_model;
mod record;
mod stat;
pub use gacha_model::GachaModel;

View file

@ -0,0 +1,85 @@
use data::gacha::gacha_config::*;
use data::tables::ItemID;
use proto::{GachaExtraItemBin, GachaRecordBin};
use std::collections::HashMap;
use super::stat::*;
#[derive(Debug, Clone)]
pub struct GachaExtraResources {
pub extra_item_id: ItemID,
pub extra_item_count: u32,
}
impl GachaExtraResources {
pub fn from_bin_opt(bin_opt: Option<GachaExtraItemBin>) -> Option<Self> {
match bin_opt {
None => None,
Some(bin) => {
let item_id_opt = ItemID::new(bin.extra_item_id);
match item_id_opt {
None => None,
Some(extra_item_id) => Some(Self {
extra_item_id,
extra_item_count: bin.extra_item_count,
}),
}
}
}
}
pub fn to_bin_opt(opt: &Option<Self>) -> Option<GachaExtraItemBin> {
match opt {
None => None,
Some(this) => Some(GachaExtraItemBin {
extra_item_id: this.extra_item_id.value(),
extra_item_count: this.extra_item_count.clone(),
..Default::default()
}),
}
}
}
#[derive(Debug, Clone)]
pub struct GachaRecord {
pub pull_timestamp: i64,
pub obtained_item_id: ItemID,
pub gacha_id: u32,
/// The progress BEFORE this gacha is performed. key is rarity.
pub progress_map: HashMap<u32, GachaProgress>,
pub extra_resources: Option<GachaExtraResources>,
pub item_type: GachaAddedItemType,
}
impl GachaRecord {
pub fn from_bin(bin: GachaRecordBin) -> Self {
Self {
pull_timestamp: bin.pull_timestamp,
obtained_item_id: ItemID::new_unchecked(bin.obtained_item_id),
gacha_id: bin.gacha_id,
progress_map: bin
.progress_map
.into_iter()
.map(|(k, v)| (k, GachaProgress::from_bin(v)))
.collect(),
extra_resources: GachaExtraResources::from_bin_opt(bin.extra_item_bin),
item_type: GachaAddedItemType::from(bin.item_type),
}
}
pub fn to_bin(&self) -> GachaRecordBin {
GachaRecordBin {
pull_timestamp: self.pull_timestamp.clone(),
obtained_item_id: self.obtained_item_id.value(),
gacha_id: self.gacha_id.clone(),
progress_map: self
.progress_map
.iter()
.map(|(k, v)| (k.clone(), v.to_bin()))
.collect(),
extra_item_bin: GachaExtraResources::to_bin_opt(&self.extra_resources),
item_type: self.item_type.clone().into(),
}
}
}

View file

@ -0,0 +1,73 @@
use proto::*;
use std::collections::HashMap;
#[derive(Debug, Default, Clone)]
pub struct GachaProgress {
/// The pity (counting how many pulls) of this pull (in previous record) or the next pull (in status).
pub pity: u32,
/// The failure times of this category.
pub categories_progress_map: HashMap<String, u32>,
/// The selected priority (category) for a Chooseable category.
pub categories_chosen_guarantee_category_map: HashMap<String, String>,
/// The selectedpriority (a specified item) for a Chooseable category.
pub categories_chosen_guarantee_item_map: HashMap<String, u32>,
/// The failure times for selected priority (a specified item).
pub categories_chosen_guarantee_progress_map: HashMap<String, u32>,
}
impl GachaProgress {
pub fn from_bin(bin: GachaProgressBin) -> Self {
Self {
pity: bin.pity,
categories_progress_map: bin.categories_progress_map,
categories_chosen_guarantee_category_map: bin.categories_chosen_guarantee_category_map,
categories_chosen_guarantee_item_map: bin.categories_chosen_guarantee_item_map,
categories_chosen_guarantee_progress_map: bin.categories_chosen_guarantee_progress_map,
}
}
pub fn to_bin(&self) -> GachaProgressBin {
GachaProgressBin {
pity: self.pity,
categories_progress_map: self.categories_progress_map.clone(),
categories_chosen_guarantee_category_map: self
.categories_chosen_guarantee_category_map
.clone(),
categories_chosen_guarantee_item_map: self.categories_chosen_guarantee_item_map.clone(),
categories_chosen_guarantee_progress_map: self
.categories_chosen_guarantee_progress_map
.clone(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct GachaStatus {
pub rarity_status_map: HashMap<u32, GachaProgress>,
pub discount_usage_map: HashMap<String, u32>,
}
impl GachaStatus {
pub fn from_bin(bin: GachaStatusBin) -> Self {
Self {
rarity_status_map: bin
.rarity_status_map
.into_iter()
.map(|(k, v)| (k, GachaProgress::from_bin(v)))
.collect(),
discount_usage_map: bin.discount_usage_map,
}
}
pub fn to_bin(&self) -> GachaStatusBin {
GachaStatusBin {
rarity_status_map: self
.rarity_status_map
.iter()
.map(|(k, v)| (k.clone(), v.to_bin()))
.collect(),
discount_usage_map: self.discount_usage_map.clone(),
}
}
}

View file

@ -1,26 +1,39 @@
use common::util;
use data::tables::{ArchiveBattleQuestID, TrainingQuestID};
use data::tables::{
self, ArchiveBattleQuestID, BattleEventConfigID, HollowQuestID, TrainingQuestID,
};
use proto::{DungeonInfo, DungeonItemData, FightSceneInfo, SceneInfo, WeatherPoolInfo};
use thiserror::Error;
use crate::logic::{
battle::{EquippedBuddyDataItem, InLevelAvatarDataItem, TeamDataItem},
ELocalPlayType, ESceneType, TimePeriodType, WeatherType,
battle::{
drop::FightDropPool,
unit::{AvatarUnit, BuddyUnit},
BuddyParam, DungeonQuestManager, TeamDataItem,
},
BuddyTeamType, EHollowQuestType, ELocalPlayType, ESceneType, TimePeriodType, WeatherType,
};
use super::NapGameMode;
#[derive(Error, Debug)]
pub enum HollowGameError {}
pub enum HollowGameError {
#[error("Quest ({0}) type is not supported: {1:?}")]
QuestTypeNotSupported(u32, EHollowQuestType),
#[error("Battle group not found, quest id: {0}")]
BattleGroupNotFound(u32),
}
pub struct HollowGame {
pub quest_id: u32,
pub battle_event_id: u32,
pub battle_event_id: BattleEventConfigID,
pub time_period: TimePeriodType,
pub weather: WeatherType,
pub play_type: ELocalPlayType,
pub start_timestamp: i64,
pub team_data: TeamDataItem,
pub fight_drop_pool: FightDropPool,
pub quest_manager: DungeonQuestManager,
}
impl HollowGame {
@ -38,7 +51,9 @@ impl HollowGame {
weather: WeatherType::SunShine,
start_timestamp: util::cur_timestamp() as i64,
play_type,
team_data: TeamDataItem::new(avatars, 0),
team_data: TeamDataItem::new(avatars, &[]),
fight_drop_pool: FightDropPool::new(template.battle_event_id),
quest_manager: DungeonQuestManager::default(),
})
}
@ -57,9 +72,66 @@ impl HollowGame {
weather: WeatherType::SunShine,
start_timestamp: util::cur_timestamp() as i64,
play_type,
team_data: TeamDataItem::new(avatars, buddy_id),
team_data: TeamDataItem::new(
avatars,
&(buddy_id != 0)
.then_some(vec![BuddyParam(buddy_id, BuddyTeamType::Fighting)])
.unwrap_or_default(),
),
fight_drop_pool: FightDropPool::new(template.battle_event_id),
quest_manager: DungeonQuestManager::default(),
})
}
pub fn create_pure_hollow_battle(
quest_id: HollowQuestID,
avatars: &[u32],
buddy_id: u32,
time_period: TimePeriodType,
weather: WeatherType,
) -> Result<Self, HollowGameError> {
let template = quest_id.template();
if template.chess_board_id != 0 {
return Err(HollowGameError::QuestTypeNotSupported(
template.id.value(),
EHollowQuestType::from(template.hollow_quest_type),
));
}
let Some(battle_group) = tables::battle_group_config_template_tb::iter()
.find(|tmpl| tmpl.quest_id == template.id.value())
else {
return Err(HollowGameError::BattleGroupNotFound(template.id.value()));
};
Ok(Self {
quest_id: template.id.value(),
battle_event_id: battle_group.battle_event_id,
time_period,
weather,
start_timestamp: util::cur_timestamp() as i64,
play_type: Self::get_play_type_by_quest_type(EHollowQuestType::from(
template.hollow_quest_type,
)),
team_data: TeamDataItem::new(
avatars,
&(buddy_id != 0)
.then_some(vec![BuddyParam(buddy_id, BuddyTeamType::Fighting)])
.unwrap_or_default(),
),
fight_drop_pool: FightDropPool::new(battle_group.battle_event_id),
quest_manager: DungeonQuestManager::new_for_battle_group(battle_group.id),
})
}
pub fn get_play_type_by_quest_type(quest_type: EHollowQuestType) -> ELocalPlayType {
match quest_type {
EHollowQuestType::NormalBattle => ELocalPlayType::PureHollowBattle,
EHollowQuestType::BossRushBattle => ELocalPlayType::BossRushBattle,
EHollowQuestType::DifficutyBattle => ELocalPlayType::PureHollowBattleHardmode,
_ => ELocalPlayType::PureHollowBattle,
}
}
}
impl NapGameMode for HollowGame {
@ -70,14 +142,16 @@ impl NapGameMode for HollowGame {
fn scene_info(&self) -> Option<SceneInfo> {
Some(SceneInfo {
scene_type: self.scene_type() as u32,
battle_event_id: self.battle_event_id,
battle_event_id: self.battle_event_id.value(),
play_type: self.play_type as u32,
fight_scene_info: Some(FightSceneInfo {
nmhdkmcabjg: true,
weather_pool: Some(WeatherPoolInfo {
time_period: self.time_period.to_string(),
weather: self.weather.to_string(),
..Default::default()
}),
fight_drop_info: Some(self.fight_drop_pool.to_client()),
..Default::default()
}),
..Default::default()
@ -93,14 +167,15 @@ impl NapGameMode for HollowGame {
.team_data
.avatar_member_list
.iter()
.map(InLevelAvatarDataItem::to_client)
.map(AvatarUnit::to_client)
.collect(),
buddy_list: self
.team_data
.equipped_buddy_list
.iter()
.map(EquippedBuddyDataItem::to_client)
.map(BuddyUnit::to_client)
.collect(),
dungeon_quest_info: Some(self.quest_manager.to_client()),
..Default::default()
})
}

View file

@ -0,0 +1,141 @@
use common::util;
use data::tables::{self, BattleEventConfigID, HollowQuestID};
use proto::{
DungeonInfo, DungeonItemData, FightQuestInfo, LongFightInfo, LongFightSceneInfo, SceneInfo,
WeatherPoolInfo,
};
use thiserror::Error;
use crate::logic::{
battle::{
drop::FightDropPool,
unit::{AvatarUnit, BuddyUnit},
BuddyParam, DungeonQuestManager, LogicVariableTable, TeamDataItem,
},
BuddyTeamType, EHollowQuestType, ELocalPlayType, ESceneType, TimePeriodType, WeatherType,
};
use super::NapGameMode;
#[derive(Error, Debug)]
pub enum LongFightGameError {
#[error("Tried to run quest of type {0:?} using LongFight logic")]
InvalidQuestType(EHollowQuestType),
#[error("Battle group not found, quest id: {0}")]
BattleGroupNotFound(u32),
}
pub struct LongFightGame {
pub quest_id: u32,
pub battle_event_id: BattleEventConfigID,
pub play_type: ELocalPlayType,
pub time_period: TimePeriodType,
pub weather: WeatherType,
pub start_timestamp: i64,
pub team_data: TeamDataItem,
pub variable_table: LogicVariableTable,
pub fight_drop_pool: FightDropPool,
pub quest_manager: DungeonQuestManager,
}
impl LongFightGame {
const RALLY_GUIDANCE_BUDDY_ID: u32 = 50001;
pub fn create_rally_game(
quest_id: HollowQuestID,
avatars: &[u32],
buddy_id: u32,
time_period: TimePeriodType,
weather: WeatherType,
) -> Result<Self, LongFightGameError> {
let template = quest_id.template();
let quest_type = EHollowQuestType::from(template.hollow_quest_type);
if quest_type != EHollowQuestType::RallyBattle {
return Err(LongFightGameError::InvalidQuestType(quest_type));
}
let Some(battle_group) = tables::battle_group_config_template_tb::iter()
.find(|tmpl| tmpl.quest_id == template.id.value())
else {
return Err(LongFightGameError::BattleGroupNotFound(template.id.value()));
};
let mut buddy_params = vec![BuddyParam(
Self::RALLY_GUIDANCE_BUDDY_ID,
BuddyTeamType::RallyGuidance,
)];
if buddy_id != 0 {
buddy_params.push(BuddyParam(buddy_id, BuddyTeamType::Fighting));
}
Ok(Self {
quest_id: template.id.value(),
battle_event_id: battle_group.battle_event_id,
play_type: ELocalPlayType::RallyLongFight,
time_period,
weather,
start_timestamp: util::cur_timestamp() as i64,
team_data: TeamDataItem::new(avatars, &buddy_params),
variable_table: LogicVariableTable::new(battle_group.battle_event_id),
fight_drop_pool: FightDropPool::new(battle_group.battle_event_id),
quest_manager: DungeonQuestManager::new_for_battle_group(battle_group.id),
})
}
}
impl NapGameMode for LongFightGame {
fn scene_type(&self) -> ESceneType {
ESceneType::LongFight
}
fn scene_info(&self) -> Option<SceneInfo> {
Some(SceneInfo {
scene_type: self.scene_type() as u32,
battle_event_id: self.battle_event_id.value(),
play_type: self.play_type as u32,
long_fight_scene_info: Some(LongFightSceneInfo {
fight_data: Some(LongFightInfo {
fight_quest_info: Some(FightQuestInfo {
fight_variable_map: self.variable_table.to_client(),
..Default::default()
}),
..Default::default()
}),
fight_drop_info: Some(self.fight_drop_pool.to_client()),
weather_pool: Some(WeatherPoolInfo {
time_period: self.time_period.to_string(),
weather: self.weather.to_string(),
febgjinpcbp: true,
bejeblcfcha: true,
..Default::default()
}),
..Default::default()
}),
..Default::default()
})
}
fn dungeon_info(&self) -> Option<DungeonInfo> {
Some(DungeonInfo {
quest_id: self.quest_id,
start_timestamp: self.start_timestamp,
dungeon_item_data: Some(DungeonItemData::default()),
avatar_list: self
.team_data
.avatar_member_list
.iter()
.map(AvatarUnit::to_client)
.collect(),
buddy_list: self
.team_data
.equipped_buddy_list
.iter()
.map(BuddyUnit::to_client)
.collect(),
dungeon_quest_info: Some(self.quest_manager.to_client()),
..Default::default()
})
}
}

View file

@ -1,20 +1,23 @@
mod fresh;
mod frontend;
mod hollow;
mod long_fight;
pub use fresh::*;
pub use frontend::*;
pub use hollow::*;
pub use long_fight::*;
use proto::{DungeonInfo, SceneInfo, WorldInitScNotify};
use thiserror::Error;
use super::{procedure::ProcedureError, ESceneType};
use super::{battle::DungeonQuestError, procedure::ProcedureError, ESceneType};
#[derive(Default)]
pub enum GameInstance {
Frontend(FrontendGame),
Fresh(FreshGame),
Hollow(HollowGame),
LongFight(LongFightGame),
#[default]
Null,
}
@ -29,6 +32,10 @@ pub enum LogicError {
Procedure(#[from] ProcedureError),
#[error("hollow error: {0}")]
Hollow(#[from] HollowGameError),
#[error("longfight error: {0}")]
LongFight(#[from] LongFightGameError),
#[error("dungeon quest error: {0}")]
DungeonQuest(#[from] DungeonQuestError),
}
impl GameInstance {
@ -41,6 +48,7 @@ impl GameInstance {
Self::Frontend(game) => game,
Self::Fresh(game) => game,
Self::Hollow(game) => game,
Self::LongFight(game) => game,
Self::Null => return Err(LogicError::GameIsNull),
}))
}

View file

@ -1,5 +1,6 @@
pub mod battle;
mod enums;
pub mod gacha;
pub mod game;
pub mod item;
pub mod math;

View file

@ -1,4 +1,4 @@
use data::tables::{AvatarBaseID, ProcedureConfigID};
use data::tables::{AvatarBaseID, PostGirlConfigID, ProcedureConfigID};
use proto::{BasicDataModelBin, PlayerBasicInfo};
pub struct BasicDataModel {
@ -8,6 +8,7 @@ pub struct BasicDataModel {
pub nick_name: Option<String>,
pub frontend_avatar_id: Option<AvatarBaseID>,
pub beginner_procedure_id: Option<ProcedureConfigID>,
pub selected_post_girl_id: Option<PostGirlConfigID>,
}
impl Default for BasicDataModel {
@ -19,6 +20,7 @@ impl Default for BasicDataModel {
nick_name: None,
frontend_avatar_id: None,
beginner_procedure_id: Some(ProcedureConfigID::new_unchecked(1)),
selected_post_girl_id: None,
}
}
}
@ -37,6 +39,10 @@ impl BasicDataModel {
avatar_id,
frontend_avatar_id: avatar_id,
kbjleelonfe: self.profile_icon,
has_nickname: match &self.nick_name {
Some(_name) => 1,
None => 0,
},
..Default::default()
}
}
@ -54,6 +60,7 @@ impl BasicDataModel {
1.. => ProcedureConfigID::new(bin.beginner_procedure_id as u32),
_ => None,
},
selected_post_girl_id: PostGirlConfigID::new(bin.selected_post_girl_id),
nick_name: match bin.nick_name.is_empty() {
true => None,
false => Some(bin.nick_name),
@ -75,6 +82,10 @@ impl BasicDataModel {
.beginner_procedure_id
.map(|i| i.value() as i32)
.unwrap_or(-1),
selected_post_girl_id: match self.selected_post_girl_id {
Some(post_girl_id) => post_girl_id.value(),
None => 0
},
}
}
}

View file

@ -1,11 +1,12 @@
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashMap};
use data::tables::UnlockConfigID;
use proto::{LockModelBin, UnlockData};
use data::tables::{quick_access_template_tb, QuickFuncID, UnlockConfigID};
use proto::{LockModelBin, QuickAccessData, QuickAccessType, UnlockData};
#[derive(Default)]
pub struct LockModel {
unlock_list: BTreeSet<UnlockConfigID>,
quick_access_list: HashMap<u32, QuickFuncID>,
}
impl LockModel {
@ -16,6 +17,11 @@ impl LockModel {
.into_iter()
.map(UnlockConfigID::new_unchecked)
.collect(),
quick_access_list: bin
.quick_access_list
.into_iter()
.map(|(k, v)| (k, QuickFuncID::new_unchecked(v)))
.collect(),
}
}
@ -27,10 +33,39 @@ impl LockModel {
.into_iter()
.map(|i| i.value())
.collect(),
quick_access_list: self
.quick_access_list
.iter()
.map(|(k, v)| (k.clone(), v.value()))
.collect(),
}
}
pub fn quick_access_to_client(&self) -> Vec<QuickAccessData> {
let mut quick_access_data_list: Vec<QuickAccessData> = vec![];
for quick_access in quick_access_template_tb::iter() {
quick_access_data_list.push(QuickAccessData {
r#type: QuickAccessType::Direct.into(),
quick_access_index: quick_access.quick_access_index,
btn_id: quick_access.quick_func_id.value(),
});
}
for quick_access_index in 1..8 {
let mut btn_id = 0;
if let Some(id) = self.quick_access_list.get(&quick_access_index) {
btn_id = id.value();
}
quick_access_data_list.push(QuickAccessData {
r#type: QuickAccessType::QuickMenu.into(),
quick_access_index,
btn_id,
});
}
quick_access_data_list
}
pub fn to_client(&self) -> UnlockData {
UnlockData {
unlock_id_list: self
.unlock_list
@ -38,6 +73,7 @@ impl LockModel {
.into_iter()
.map(|i| i.value())
.collect(),
quick_access_data_list: self.quick_access_to_client(),
..Default::default()
}
}
@ -49,4 +85,28 @@ impl LockModel {
pub fn is_unlock(&self, id: UnlockConfigID) -> bool {
self.unlock_list.contains(&id)
}
pub fn mod_quick_access(&mut self, index: u32, id: Option<QuickFuncID>) -> QuickAccessData {
let btn_id = match id {
Some(quick_access_id) => {
if self.quick_access_list.contains_key(&index) {
*self.quick_access_list.get_mut(&index).unwrap() = quick_access_id;
} else {
self.quick_access_list.insert(index, quick_access_id);
}
quick_access_id.value()
}
None => {
if self.quick_access_list.contains_key(&index) {
self.quick_access_list.remove(&index);
}
0
}
};
QuickAccessData {
r#type: QuickAccessType::QuickMenu.into(),
quick_access_index: index,
btn_id,
}
}
}

View file

@ -1,6 +1,7 @@
use data::tables::{self, AvatarBaseID};
use proto::{ItemStatic, PlayerDataBin, Retcode};
use super::gacha::GachaModel;
use super::game::{FrontendGame, FrontendGameError, GameInstance, LogicError};
use super::item::{ItemModel, ItemUID};
use super::main_city_model::MainCityModel;
@ -16,6 +17,7 @@ pub struct Player {
pub role_model: RoleModel,
pub item_model: ItemModel,
pub main_city_model: MainCityModel,
pub gacha_model: GachaModel,
}
impl Player {
@ -28,6 +30,7 @@ impl Player {
role_model: Some(self.role_model.to_bin()),
item_model: Some(self.item_model.to_bin()),
main_city_model: Some(self.main_city_model.to_bin()),
gacha_model: Some(self.gacha_model.to_bin()),
}
}
@ -44,6 +47,7 @@ impl Player {
.main_city_model
.map(MainCityModel::from_bin)
.unwrap_or_default(),
gacha_model: bin.gacha_model.map(GachaModel::from_bin).unwrap_or_default(),
..Default::default()
}
}
@ -58,6 +62,12 @@ impl Player {
.add_resource(ItemStatic::FrontendGold as u32, 1_000_000);
self.item_model
.add_resource(ItemStatic::GameDiamond as u32, 1_000_000);
self.item_model
.add_resource(ItemStatic::GachaTicketEvent as u32, 30_000);
self.item_model
.add_resource(ItemStatic::GachaTicketStandard as u32, 30_000);
self.item_model
.add_resource(ItemStatic::GachaTicketBangboo as u32, 30_000);
self.item_model.add_resource(ItemStatic::Energy as u32, 240);
}

View file

@ -247,4 +247,8 @@ impl NetSession {
pub fn set_state(&self, state: NetSessionState) {
self.state.store(state, std::sync::atomic::Ordering::SeqCst);
}
pub async fn shutdown(&self) -> Result<(), std::io::Error> {
self.writer.lock().await.shutdown().await
}
}

File diff suppressed because it is too large Load diff

View file

@ -14,12 +14,16 @@ pub struct BasicDataModelBin {
pub frontend_avatar_id: i32,
#[prost(int32, tag = "6")]
pub beginner_procedure_id: i32,
#[prost(uint32, tag = "7")]
pub selected_post_girl_id: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LockModelBin {
#[prost(int32, repeated, tag = "1")]
pub unlock_list: ::prost::alloc::vec::Vec<i32>,
#[prost(map = "uint32, uint32", tag = "2")]
pub quick_access_list: ::std::collections::HashMap<u32, u32>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
@ -117,6 +121,92 @@ pub struct MainCityModelBin {
#[prost(uint32, tag = "3")]
pub section_id: u32,
}
/// The progress record of a specified rarity. All maps' keys are category_guarantee_policy_tag.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GachaProgressBin {
/// The pity (counting how many pulls) of this pull (in previous record) or the next pull (in status).
#[prost(uint32, tag = "1")]
pub pity: u32,
/// The failure times of this category.
#[prost(map = "string, uint32", tag = "2")]
pub categories_progress_map: ::std::collections::HashMap<
::prost::alloc::string::String,
u32,
>,
/// The selected priority (category) for a Chooseable category.
#[prost(map = "string, string", tag = "3")]
pub categories_chosen_guarantee_category_map: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost::alloc::string::String,
>,
/// The selectedpriority (a specified item) for a Chooseable category.
#[prost(map = "string, uint32", tag = "4")]
pub categories_chosen_guarantee_item_map: ::std::collections::HashMap<
::prost::alloc::string::String,
u32,
>,
/// The failure times for selected priority (a specified item).
#[prost(map = "string, uint32", tag = "5")]
pub categories_chosen_guarantee_progress_map: ::std::collections::HashMap<
::prost::alloc::string::String,
u32,
>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GachaExtraItemBin {
#[prost(uint32, tag = "1")]
pub extra_item_id: u32,
#[prost(uint32, tag = "2")]
pub extra_item_count: u32,
/// How many objects of the main item obtained in gacha is present in the player's bag (before gacha).
/// This is used for something like converting when there're extra characters.
#[prost(uint32, tag = "3")]
pub currently_gained: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GachaRecordBin {
#[prost(int64, tag = "1")]
pub pull_timestamp: i64,
#[prost(uint32, tag = "2")]
pub obtained_item_id: u32,
#[prost(uint32, tag = "3")]
pub gacha_id: u32,
/// The progress BEFORE this gacha is performed. uint32 is rarity.
#[prost(map = "uint32, message", tag = "4")]
pub progress_map: ::std::collections::HashMap<u32, GachaProgressBin>,
#[prost(message, optional, tag = "5")]
pub extra_item_bin: ::core::option::Option<GachaExtraItemBin>,
#[prost(enumeration = "GachaAddedItemType", tag = "6")]
pub item_type: i32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GachaStatusBin {
#[prost(map = "uint32, message", tag = "1")]
pub rarity_status_map: ::std::collections::HashMap<u32, GachaProgressBin>,
#[prost(map = "string, uint32", tag = "2")]
pub discount_usage_map: ::std::collections::HashMap<
::prost::alloc::string::String,
u32,
>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GachaModelBin {
/// Gacha Status query. string is sharing_guarantee_info_category.
#[prost(map = "string, message", tag = "1")]
pub gacha_status_map: ::std::collections::HashMap<
::prost::alloc::string::String,
GachaStatusBin,
>,
#[prost(message, repeated, tag = "2")]
pub gacha_records: ::prost::alloc::vec::Vec<GachaRecordBin>,
#[prost(uint32, tag = "3")]
pub random_number: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PlayerDataBin {
@ -130,4 +220,38 @@ pub struct PlayerDataBin {
pub item_model: ::core::option::Option<ItemModelBin>,
#[prost(message, optional, tag = "5")]
pub main_city_model: ::core::option::Option<MainCityModelBin>,
#[prost(message, optional, tag = "6")]
pub gacha_model: ::core::option::Option<GachaModelBin>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum GachaAddedItemType {
None = 0,
Weapon = 1,
Character = 2,
Bangboo = 3,
}
impl GachaAddedItemType {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
GachaAddedItemType::None => "GACHA_ADDED_ITEM_TYPE_NONE",
GachaAddedItemType::Weapon => "GACHA_ADDED_ITEM_TYPE_WEAPON",
GachaAddedItemType::Character => "GACHA_ADDED_ITEM_TYPE_CHARACTER",
GachaAddedItemType::Bangboo => "GACHA_ADDED_ITEM_TYPE_BANGBOO",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"GACHA_ADDED_ITEM_TYPE_NONE" => Some(Self::None),
"GACHA_ADDED_ITEM_TYPE_WEAPON" => Some(Self::Weapon),
"GACHA_ADDED_ITEM_TYPE_CHARACTER" => Some(Self::Character),
"GACHA_ADDED_ITEM_TYPE_BANGBOO" => Some(Self::Bangboo),
_ => None,
}
}
}