浏览代码

弱网处理

baxi 2 周之前
父节点
当前提交
a59ba7bc3f
共有 41 个文件被更改,包括 1605 次插入 和 260 次删除
  1. 2 2
      assets/lobbyRes/ui/dialog/UIDialog.prefab
  2. 235 73
      assets/lobbyRes/ui/store/OrderView.prefab
  3. 326 15
      assets/lobbyRes/ui/store/UIFirstPack.prefab
  4. 1 1
      assets/scripts/app/AppConfig.ts
  5. 2 1
      assets/scripts/app/AppEvents.ts
  6. 4 0
      assets/scripts/define/PayDefine.ts
  7. 20 0
      assets/scripts/framework/ui/UIManager.ts
  8. 1 1
      assets/scripts/framework/ui/UIPopup.ts
  9. 5 1
      assets/scripts/i18n/en/en1.ts
  10. 12 1
      assets/scripts/i18n/index.ts
  11. 34 9
      assets/scripts/modules/lobby/ui/HomeView/HomeView.ts
  12. 1 0
      assets/scripts/modules/lobby/ui/LoginView/LoginView.ts
  13. 2 2
      assets/scripts/modules/lobby/ui/StoreView/GoodsItem.ts
  14. 12 10
      assets/scripts/modules/lobby/ui/StoreView/OrderView.ts
  15. 7 2
      assets/scripts/modules/lobby/ui/StoreView/StoreView.ts
  16. 53 14
      assets/scripts/modules/lobby/ui/StoreView/UIFirstPack.ts
  17. 1 0
      assets/scripts/modules/lobby/ui/UserInfoView/UserInfoView.ts
  18. 64 5
      assets/scripts/network/colyseus/ColyseusRoomClient.ts
  19. 259 7
      assets/scripts/platform/FaceBook/FBAndroidImpl.ts
  20. 61 2
      assets/scripts/platform/FaceBook/FBH5Impl.ts
  21. 35 2
      assets/scripts/platform/FaceBook/FBIOSImpl.ts
  22. 34 2
      assets/scripts/platform/FaceBook/FacebookImpl.ts
  23. 167 4
      assets/scripts/platform/Google/GoogleAndroidImpl.ts
  24. 12 8
      assets/scripts/platform/Google/GoogleH5Impl.ts
  25. 31 13
      assets/scripts/request/HttpClient.ts
  26. 28 0
      assets/scripts/request/RequestTypes.ts
  27. 81 8
      assets/scripts/services/AppLobby.ts
  28. 8 7
      assets/scripts/services/hall/HallClient.ts
  29. 11 6
      assets/scripts/services/login/LoginService.ts
  30. 13 0
      assets/scripts/services/pay/PaymentServices.ts
  31. 1 1
      assets/scripts/store/checkInStore/CheckInStore.ts
  32. 30 21
      assets/scripts/store/gameStore/GameStore.ts
  33. 2 1
      assets/scripts/store/gameStore/data.ts
  34. 1 1
      assets/scripts/store/mailStore/MailStore.ts
  35. 6 13
      assets/scripts/store/mainStore/MainStore.ts
  36. 22 15
      assets/scripts/store/payStore/PayStore.ts
  37. 9 9
      assets/scripts/store/payStore/data.ts
  38. 2 0
      assets/scripts/utils/Helper.ts
  39. 5 1
      assets/scripts/utils/VipHelper.ts
  40. 3 0
      package.json
  41. 2 2
      settings/v2/packages/project.json

+ 2 - 2
assets/lobbyRes/ui/dialog/UIDialog.prefab

@@ -439,7 +439,7 @@
       "a": 255
     },
     "_spriteFrame": {
-      "__uuid__": "ce2a5976-ed66-4814-b76c-071d6a99c8c5@f9941",
+      "__uuid__": "28ed5316-23a1-43be-b1cf-188791c21a8f@f9941",
       "__expectedType__": "cc.SpriteFrame"
     },
     "_type": 1,
@@ -1099,7 +1099,7 @@
       "a": 255
     },
     "_spriteFrame": {
-      "__uuid__": "d99cf9a4-d31b-4f83-bd6b-b250f3a92052@f9941",
+      "__uuid__": "3cda8cc0-6de9-4c12-ac5a-b1d1c10707e8@f9941",
       "__expectedType__": "cc.SpriteFrame"
     },
     "_type": 0,

+ 235 - 73
assets/lobbyRes/ui/store/OrderView.prefab

@@ -28,20 +28,20 @@
     "_active": true,
     "_components": [
       {
-        "__id__": 170
+        "__id__": 176
       },
       {
-        "__id__": 172
+        "__id__": 178
       },
       {
-        "__id__": 174
+        "__id__": 180
       },
       {
-        "__id__": 176
+        "__id__": 182
       }
     ],
     "_prefab": {
-      "__id__": 178
+      "__id__": 184
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -330,23 +330,23 @@
         "__id__": 113
       },
       {
-        "__id__": 131
+        "__id__": 137
       },
       {
-        "__id__": 145
+        "__id__": 151
       }
     ],
     "_active": true,
     "_components": [
       {
-        "__id__": 165
+        "__id__": 171
       },
       {
-        "__id__": 167
+        "__id__": 173
       }
     ],
     "_prefab": {
-      "__id__": 169
+      "__id__": 175
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -1799,7 +1799,7 @@
     "_lpos": {
       "__type__": "cc.Vec3",
       "x": 26,
-      "y": -26.413,
+      "y": -26.5,
       "z": 0
     },
     "_lrot": {
@@ -2773,19 +2773,22 @@
       },
       {
         "__id__": 120
+      },
+      {
+        "__id__": 126
       }
     ],
     "_active": true,
     "_components": [
       {
-        "__id__": 126
+        "__id__": 132
       },
       {
-        "__id__": 128
+        "__id__": 134
       }
     ],
     "_prefab": {
-      "__id__": 130
+      "__id__": 136
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -2976,7 +2979,7 @@
     "_lpos": {
       "__type__": "cc.Vec3",
       "x": 26,
-      "y": -2.835,
+      "y": -2,
       "z": 0
     },
     "_lrot": {
@@ -3114,6 +3117,165 @@
     "targetOverrides": null,
     "nestedPrefabInstanceRoots": null
   },
+  {
+    "__type__": "cc.Node",
+    "_name": "lblDesc",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "_parent": {
+      "__id__": 113
+    },
+    "_children": [],
+    "_active": true,
+    "_components": [
+      {
+        "__id__": 127
+      },
+      {
+        "__id__": 129
+      }
+    ],
+    "_prefab": {
+      "__id__": 131
+    },
+    "_lpos": {
+      "__type__": "cc.Vec3",
+      "x": 26,
+      "y": -26.5,
+      "z": 0
+    },
+    "_lrot": {
+      "__type__": "cc.Quat",
+      "x": 0,
+      "y": 0,
+      "z": 0,
+      "w": 1
+    },
+    "_lscale": {
+      "__type__": "cc.Vec3",
+      "x": 1,
+      "y": 1,
+      "z": 1
+    },
+    "_mobility": 0,
+    "_layer": 33554432,
+    "_euler": {
+      "__type__": "cc.Vec3",
+      "x": 0,
+      "y": 0,
+      "z": 0
+    },
+    "_id": ""
+  },
+  {
+    "__type__": "cc.UITransform",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 126
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 128
+    },
+    "_contentSize": {
+      "__type__": "cc.Size",
+      "width": 384.093375,
+      "height": 27.72
+    },
+    "_anchorPoint": {
+      "__type__": "cc.Vec2",
+      "x": 0.5,
+      "y": 0.5
+    },
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "696Ku35e1AU74srNh+xTQr"
+  },
+  {
+    "__type__": "cc.Label",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 126
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 130
+    },
+    "_customMaterial": null,
+    "_srcBlendFactor": 2,
+    "_dstBlendFactor": 4,
+    "_color": {
+      "__type__": "cc.Color",
+      "r": 255,
+      "g": 255,
+      "b": 255,
+      "a": 255
+    },
+    "_string": "BASE 50K + 15% BONUS 7.500",
+    "_horizontalAlign": 1,
+    "_verticalAlign": 1,
+    "_actualFontSize": 20,
+    "_fontSize": 20,
+    "_fontFamily": "Arial",
+    "_lineHeight": 22,
+    "_overflow": 1,
+    "_enableWrapText": false,
+    "_font": null,
+    "_isSystemFontUsed": true,
+    "_spacingX": 0,
+    "_isItalic": false,
+    "_isBold": false,
+    "_isUnderline": false,
+    "_underlineHeight": 2,
+    "_cacheMode": 0,
+    "_enableOutline": false,
+    "_outlineColor": {
+      "__type__": "cc.Color",
+      "r": 0,
+      "g": 0,
+      "b": 0,
+      "a": 255
+    },
+    "_outlineWidth": 2,
+    "_enableShadow": false,
+    "_shadowColor": {
+      "__type__": "cc.Color",
+      "r": 0,
+      "g": 0,
+      "b": 0,
+      "a": 255
+    },
+    "_shadowOffset": {
+      "__type__": "cc.Vec2",
+      "x": 2,
+      "y": 2
+    },
+    "_shadowBlur": 2,
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "67ANAUTGpHT4M5x82YmAap"
+  },
+  {
+    "__type__": "cc.PrefabInfo",
+    "root": {
+      "__id__": 1
+    },
+    "asset": {
+      "__id__": 0
+    },
+    "fileId": "c9dC6/B0xBKpP4iySzSuPb",
+    "instance": null,
+    "targetOverrides": null,
+    "nestedPrefabInstanceRoots": null
+  },
   {
     "__type__": "cc.UITransform",
     "_name": "",
@@ -3124,7 +3286,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 127
+      "__id__": 133
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -3152,7 +3314,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 129
+      "__id__": 135
     },
     "_customMaterial": null,
     "_srcBlendFactor": 2,
@@ -3210,23 +3372,23 @@
     },
     "_children": [
       {
-        "__id__": 132
+        "__id__": 138
       }
     ],
     "_active": true,
     "_components": [
       {
-        "__id__": 138
+        "__id__": 144
       },
       {
-        "__id__": 140
+        "__id__": 146
       },
       {
-        "__id__": 142
+        "__id__": 148
       }
     ],
     "_prefab": {
-      "__id__": 144
+      "__id__": 150
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -3263,20 +3425,20 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "_parent": {
-      "__id__": 131
+      "__id__": 137
     },
     "_children": [],
     "_active": true,
     "_components": [
       {
-        "__id__": 133
+        "__id__": 139
       },
       {
-        "__id__": 135
+        "__id__": 141
       }
     ],
     "_prefab": {
-      "__id__": 137
+      "__id__": 143
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -3313,11 +3475,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 132
+      "__id__": 138
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 134
+      "__id__": 140
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -3341,11 +3503,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 132
+      "__id__": 138
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 136
+      "__id__": 142
     },
     "_customMaterial": null,
     "_srcBlendFactor": 2,
@@ -3422,11 +3584,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 131
+      "__id__": 137
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 139
+      "__id__": 145
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -3450,11 +3612,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 131
+      "__id__": 137
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 141
+      "__id__": 147
     },
     "_customMaterial": null,
     "_srcBlendFactor": 2,
@@ -3495,11 +3657,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 131
+      "__id__": 137
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 143
+      "__id__": 149
     },
     "clickEvents": [],
     "_interactable": true,
@@ -3568,20 +3730,20 @@
     },
     "_children": [
       {
-        "__id__": 146
+        "__id__": 152
       }
     ],
     "_active": true,
     "_components": [
       {
-        "__id__": 160
+        "__id__": 166
       },
       {
-        "__id__": 162
+        "__id__": 168
       }
     ],
     "_prefab": {
-      "__id__": 164
+      "__id__": 170
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -3618,27 +3780,27 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "_parent": {
-      "__id__": 145
+      "__id__": 151
     },
     "_children": [
       {
-        "__id__": 147
+        "__id__": 153
       }
     ],
     "_active": false,
     "_components": [
       {
-        "__id__": 153
+        "__id__": 159
       },
       {
-        "__id__": 155
+        "__id__": 161
       },
       {
-        "__id__": 157
+        "__id__": 163
       }
     ],
     "_prefab": {
-      "__id__": 159
+      "__id__": 165
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -3675,20 +3837,20 @@
     "_objFlags": 512,
     "__editorExtras__": {},
     "_parent": {
-      "__id__": 146
+      "__id__": 152
     },
     "_children": [],
     "_active": true,
     "_components": [
       {
-        "__id__": 148
+        "__id__": 154
       },
       {
-        "__id__": 150
+        "__id__": 156
       }
     ],
     "_prefab": {
-      "__id__": 152
+      "__id__": 158
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -3725,11 +3887,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 147
+      "__id__": 153
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 149
+      "__id__": 155
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -3753,11 +3915,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 147
+      "__id__": 153
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 151
+      "__id__": 157
     },
     "_customMaterial": null,
     "_srcBlendFactor": 2,
@@ -3834,11 +3996,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 146
+      "__id__": 152
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 154
+      "__id__": 160
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -3862,11 +4024,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 146
+      "__id__": 152
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 156
+      "__id__": 162
     },
     "_customMaterial": null,
     "_srcBlendFactor": 2,
@@ -3907,11 +4069,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 146
+      "__id__": 152
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 158
+      "__id__": 164
     },
     "clickEvents": [],
     "_interactable": true,
@@ -3963,7 +4125,7 @@
     "_duration": 0.1,
     "_zoomScale": 1.2,
     "_target": {
-      "__id__": 146
+      "__id__": 152
     },
     "_id": ""
   },
@@ -3990,11 +4152,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 145
+      "__id__": 151
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 161
+      "__id__": 167
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -4018,11 +4180,11 @@
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 145
+      "__id__": 151
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 163
+      "__id__": 169
     },
     "_resizeMode": 1,
     "_layoutType": 1,
@@ -4073,7 +4235,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 166
+      "__id__": 172
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -4101,7 +4263,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 168
+      "__id__": 174
     },
     "_customMaterial": null,
     "_srcBlendFactor": 2,
@@ -4159,7 +4321,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 171
+      "__id__": 177
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -4187,7 +4349,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 173
+      "__id__": 179
     },
     "_alignFlags": 45,
     "_target": null,
@@ -4223,7 +4385,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 175
+      "__id__": 181
     },
     "_id": ""
   },
@@ -4241,7 +4403,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 177
+      "__id__": 183
     },
     "popupMask": {
       "__id__": 11
@@ -4262,10 +4424,10 @@
       "__id__": 56
     },
     "lblButton": {
-      "__id__": 135
+      "__id__": 141
     },
     "btnPay": {
-      "__id__": 131
+      "__id__": 137
     },
     "scNode": {
       "__id__": 59

+ 326 - 15
assets/lobbyRes/ui/store/UIFirstPack.prefab

@@ -28,20 +28,20 @@
     "_active": true,
     "_components": [
       {
-        "__id__": 164
+        "__id__": 176
       },
       {
-        "__id__": 166
+        "__id__": 178
       },
       {
-        "__id__": 168
+        "__id__": 180
       },
       {
-        "__id__": 170
+        "__id__": 182
       }
     ],
     "_prefab": {
-      "__id__": 172
+      "__id__": 184
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -331,19 +331,22 @@
       },
       {
         "__id__": 145
+      },
+      {
+        "__id__": 159
       }
     ],
     "_active": true,
     "_components": [
       {
-        "__id__": 159
+        "__id__": 171
       },
       {
-        "__id__": 161
+        "__id__": 173
       }
     ],
     "_prefab": {
-      "__id__": 163
+      "__id__": 175
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -2999,7 +3002,7 @@
     "_lpos": {
       "__type__": "cc.Vec3",
       "x": 0,
-      "y": 324.378,
+      "y": -302.483,
       "z": 0
     },
     "_lrot": {
@@ -3834,18 +3837,317 @@
     "targetOverrides": null,
     "nestedPrefabInstanceRoots": null
   },
+  {
+    "__type__": "cc.Node",
+    "_name": "btnDisabled",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "_parent": {
+      "__id__": 14
+    },
+    "_children": [
+      {
+        "__id__": 160
+      }
+    ],
+    "_active": true,
+    "_components": [
+      {
+        "__id__": 166
+      },
+      {
+        "__id__": 168
+      }
+    ],
+    "_prefab": {
+      "__id__": 170
+    },
+    "_lpos": {
+      "__type__": "cc.Vec3",
+      "x": 113,
+      "y": -203,
+      "z": 0
+    },
+    "_lrot": {
+      "__type__": "cc.Quat",
+      "x": 0,
+      "y": 0,
+      "z": 0,
+      "w": 1
+    },
+    "_lscale": {
+      "__type__": "cc.Vec3",
+      "x": 1,
+      "y": 1,
+      "z": 1
+    },
+    "_mobility": 0,
+    "_layer": 33554432,
+    "_euler": {
+      "__type__": "cc.Vec3",
+      "x": 0,
+      "y": 0,
+      "z": 0
+    },
+    "_id": ""
+  },
+  {
+    "__type__": "cc.Node",
+    "_name": "llblDisabled",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "_parent": {
+      "__id__": 159
+    },
+    "_children": [],
+    "_active": true,
+    "_components": [
+      {
+        "__id__": 161
+      },
+      {
+        "__id__": 163
+      }
+    ],
+    "_prefab": {
+      "__id__": 165
+    },
+    "_lpos": {
+      "__type__": "cc.Vec3",
+      "x": 0,
+      "y": 5,
+      "z": 0
+    },
+    "_lrot": {
+      "__type__": "cc.Quat",
+      "x": 0,
+      "y": 0,
+      "z": 0,
+      "w": 1
+    },
+    "_lscale": {
+      "__type__": "cc.Vec3",
+      "x": 1,
+      "y": 1,
+      "z": 1
+    },
+    "_mobility": 0,
+    "_layer": 33554432,
+    "_euler": {
+      "__type__": "cc.Vec3",
+      "x": 0,
+      "y": 0,
+      "z": 0
+    },
+    "_id": ""
+  },
   {
     "__type__": "cc.UITransform",
     "_name": "",
     "_objFlags": 0,
     "__editorExtras__": {},
     "node": {
-      "__id__": 14
+      "__id__": 160
     },
     "_enabled": true,
     "__prefab": {
+      "__id__": 162
+    },
+    "_contentSize": {
+      "__type__": "cc.Size",
+      "width": 196.25,
+      "height": 56.4
+    },
+    "_anchorPoint": {
+      "__type__": "cc.Vec2",
+      "x": 0.5,
+      "y": 0.5
+    },
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "24uS0EomRH/pFKXdo31ap0"
+  },
+  {
+    "__type__": "cc.Label",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
       "__id__": 160
     },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 164
+    },
+    "_customMaterial": null,
+    "_srcBlendFactor": 2,
+    "_dstBlendFactor": 4,
+    "_color": {
+      "__type__": "cc.Color",
+      "r": 255,
+      "g": 255,
+      "b": 255,
+      "a": 255
+    },
+    "_string": "BUYNOW $1",
+    "_horizontalAlign": 1,
+    "_verticalAlign": 1,
+    "_actualFontSize": 32,
+    "_fontSize": 32,
+    "_fontFamily": "Arial",
+    "_lineHeight": 40,
+    "_overflow": 0,
+    "_enableWrapText": true,
+    "_font": null,
+    "_isSystemFontUsed": true,
+    "_spacingX": 0,
+    "_isItalic": false,
+    "_isBold": true,
+    "_isUnderline": false,
+    "_underlineHeight": 2,
+    "_cacheMode": 0,
+    "_enableOutline": true,
+    "_outlineColor": {
+      "__type__": "cc.Color",
+      "r": 50,
+      "g": 47,
+      "b": 47,
+      "a": 255
+    },
+    "_outlineWidth": 3,
+    "_enableShadow": false,
+    "_shadowColor": {
+      "__type__": "cc.Color",
+      "r": 0,
+      "g": 0,
+      "b": 0,
+      "a": 255
+    },
+    "_shadowOffset": {
+      "__type__": "cc.Vec2",
+      "x": 2,
+      "y": 2
+    },
+    "_shadowBlur": 2,
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "5741d+n7dEi5jtfDlqn8wV"
+  },
+  {
+    "__type__": "cc.PrefabInfo",
+    "root": {
+      "__id__": 1
+    },
+    "asset": {
+      "__id__": 0
+    },
+    "fileId": "acJHabi4dOdJnCTllmONpc",
+    "instance": null,
+    "targetOverrides": null,
+    "nestedPrefabInstanceRoots": null
+  },
+  {
+    "__type__": "cc.UITransform",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 159
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 167
+    },
+    "_contentSize": {
+      "__type__": "cc.Size",
+      "width": 334,
+      "height": 94
+    },
+    "_anchorPoint": {
+      "__type__": "cc.Vec2",
+      "x": 0.5,
+      "y": 0.5
+    },
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "1dKjRuUZ9CiagU6/cAnmWB"
+  },
+  {
+    "__type__": "cc.Sprite",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 159
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 169
+    },
+    "_customMaterial": null,
+    "_srcBlendFactor": 2,
+    "_dstBlendFactor": 4,
+    "_color": {
+      "__type__": "cc.Color",
+      "r": 255,
+      "g": 255,
+      "b": 255,
+      "a": 255
+    },
+    "_spriteFrame": {
+      "__uuid__": "152d8443-b30b-48b0-85e3-92dd87898c8d@f9941",
+      "__expectedType__": "cc.SpriteFrame"
+    },
+    "_type": 0,
+    "_fillType": 0,
+    "_sizeMode": 1,
+    "_fillCenter": {
+      "__type__": "cc.Vec2",
+      "x": 0,
+      "y": 0
+    },
+    "_fillStart": 0,
+    "_fillRange": 0,
+    "_isTrimmedMode": true,
+    "_useGrayscale": true,
+    "_atlas": null,
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "41VvWBzVhFlIoirZUPYzs2"
+  },
+  {
+    "__type__": "cc.PrefabInfo",
+    "root": {
+      "__id__": 1
+    },
+    "asset": {
+      "__id__": 0
+    },
+    "fileId": "f7hpQPRy5F/4UOOuZPR8/b",
+    "instance": null,
+    "targetOverrides": null,
+    "nestedPrefabInstanceRoots": null
+  },
+  {
+    "__type__": "cc.UITransform",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 14
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 172
+    },
     "_contentSize": {
       "__type__": "cc.Size",
       "width": 1599,
@@ -3872,7 +4174,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 162
+      "__id__": 174
     },
     "_customMaterial": null,
     "_srcBlendFactor": 2,
@@ -3930,7 +4232,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 165
+      "__id__": 177
     },
     "_contentSize": {
       "__type__": "cc.Size",
@@ -3958,7 +4260,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 167
+      "__id__": 179
     },
     "_alignFlags": 45,
     "_target": null,
@@ -3994,7 +4296,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 169
+      "__id__": 181
     },
     "_id": ""
   },
@@ -4012,7 +4314,7 @@
     },
     "_enabled": true,
     "__prefab": {
-      "__id__": 171
+      "__id__": 183
     },
     "popupMask": {
       "__id__": 11
@@ -4038,6 +4340,15 @@
     "btnPay": {
       "__id__": 156
     },
+    "btnDisabled": {
+      "__id__": 159
+    },
+    "lblDisabled": {
+      "__id__": 163
+    },
+    "testGears": {
+      "__id__": 125
+    },
     "_id": ""
   },
   {

+ 1 - 1
assets/scripts/app/AppConfig.ts

@@ -16,7 +16,7 @@ const AppConfig = {
      * 非空时使用本地 Manifest 的资源列表与该地址下的远端 Manifest 做检测。
      */
     HotUpdateUrl: "",
-    HotUpdateEnable: true,
+    HotUpdateEnable: false,
 
 
     FBAppID: "1056149470661855",

+ 2 - 1
assets/scripts/app/AppEvents.ts

@@ -16,7 +16,8 @@ export interface AppEventMap {
   'hall:disconnected': { code?: number };
   'hall:timeout': undefined;
   'hall:kicked': HallMessage;
-  'hall:message': { type: string; message: HallMessage };
   'hall:pay-finished': HallPayFinishMessage;
   'hall:call-client': HallCallClientMessage;
+  'user:update': undefined,
+  'firstPack:update': undefined,
 }

+ 4 - 0
assets/scripts/define/PayDefine.ts

@@ -0,0 +1,4 @@
+const GiftsIDs = {
+    /** 新手礼包 */
+    FirstPack: 308
+}

+ 20 - 0
assets/scripts/framework/ui/UIManager.ts

@@ -26,6 +26,8 @@ export class UIManager {
   private readonly roots = new Map<UILayer, Node>();
   private loadingCount = 0;
 
+  public get isLoading() { return this.loadingCount > 0 }
+
   constructor(private readonly resources: ResourceManager) { }
 
   bindLayer(layer: UILayer, root: Node): void {
@@ -281,6 +283,7 @@ export class UIManager {
       return null;
     }
 
+    const loadingCountBeforeOpen = this.loadingCount;
     let allowOpen: boolean;
     try {
       allowOpen = await view.internalOpenBefore(params);
@@ -297,6 +300,7 @@ export class UIManager {
       return null;
     }
     if (!allowOpen) {
+      await this.closeLoadingOpenedDuring(loadingCountBeforeOpen);
       node.destroy();
       record.view = null;
       record.state = UIViewState.None;
@@ -321,6 +325,7 @@ export class UIManager {
     const view = record.view;
     if (!view?.node?.isValid) return null;
 
+    const loadingCountBeforeOpen = this.loadingCount;
     let allowOpen: boolean;
     try {
       allowOpen = await view.internalOpenBefore(params);
@@ -330,6 +335,7 @@ export class UIManager {
     }
     if (generation !== record.generation || !view.node.isValid) return null;
     if (!allowOpen) {
+      await this.closeLoadingOpenedDuring(loadingCountBeforeOpen);
       this.restoreCachedRecord(record);
       return null;
     }
@@ -356,6 +362,13 @@ export class UIManager {
     }
   }
 
+  /** 仅回收 onBeforeOpen 阶段额外开启的 Loading,不影响本次 open 的自动 Loading。 */
+  private closeLoadingOpenedDuring(loadingCountBeforeOpen: number) {
+    while (this.loadingCount > loadingCountBeforeOpen) {
+      this.loading(false);
+    }
+  }
+
   private async performClose(record: UIViewRecord, reason: UICloseReason): Promise<boolean> {
     if (!record.view?.node?.isValid) {
       this.cancelDelayedDestroy(record);
@@ -483,6 +496,13 @@ export class UIManager {
   }
 
   async dialog(config: IDialogConfig) {
+    this.loadingCount = 0;
+    this.close(UIViewID.LoadingView);
+    const panel = this.get<UIDialog>(UIViewID.UIDialog);
+    if (panel && panel.node.active) {
+      panel.internalOpen(config);
+      return;
+    }
     await AppKernel.ui.open<UIDialog>(UIViewID.UIDialog, config);
   }
 }

+ 1 - 1
assets/scripts/framework/ui/UIPopup.ts

@@ -19,7 +19,7 @@ export class UIPopup<TParams = unknown> extends UIView<TParams> {
     private maskTween: Tween;
     private rootTween: Tween;
 
-    internalCreate(): Promise<void> {
+    internalCreate() {
         const sptMask = this.popupMask.getComponent(Sprite);
         if (sptMask) sptMask.color = color(0, 0, 0, 191);
 

+ 5 - 1
assets/scripts/i18n/en/en1.ts

@@ -18,7 +18,11 @@ export const home = {
         title: "Log Out",
         content: "Are you sure you want to log out?"
     },
-    notices: "{nickName} won {coin} {value}"
+    notices: "{nickName} won {coin} {value}",
+    offline: "You are already online somewhere else and are forced offline here.",
+    reconnect: "Reconnect",
+    disconnected: "The network is disconnected. Do you want to reconnect?",
+    networkError: "Network error. Please try again.",
 };
 
 export const dialog = {

+ 12 - 1
assets/scripts/i18n/index.ts

@@ -6,10 +6,11 @@ type I18nParams = readonly unknown[] | Record<string, unknown>;
  * 读取语言文本,并替换位置占位符({0})或命名占位符({name})。
  */
 export function t(path: string, ...params: unknown[]): string {
+    const language = en;
     const value = path.split('.').reduce<unknown>((current, key) => {
         if (!current || typeof current !== 'object') return undefined;
         return (current as Record<string, unknown>)[key];
-    }, en);
+    }, language);
 
     if (typeof value !== 'string') return path;
     if (params.length === 0) return value;
@@ -24,6 +25,16 @@ export function t(path: string, ...params: unknown[]): string {
     });
 }
 
+export function has(path: string) {
+    const language = en;
+    const value = path.split('.').reduce<unknown>((current, key) => {
+        if (!current || typeof current !== 'object') return undefined;
+        return (current as Record<string, unknown>)[key];
+    }, language);
+
+    return !!value;
+}
+
 function getReplacements(params: unknown[]): I18nParams {
     const [firstParam] = params;
     if (params.length === 1 && Array.isArray(firstParam)) return firstParam;

+ 34 - 9
assets/scripts/modules/lobby/ui/HomeView/HomeView.ts

@@ -3,7 +3,7 @@ import { AppKernel } from '@scripts/app/AppKernel';
 import { UIView } from '@scripts/framework/ui/UIView';
 import { UIViewID } from '@scripts/define/UIDefine';
 import { addBtnClick, addTogClick, setAlpha } from '@scripts/utils/UIHelper';
-import { formatVip } from '@scripts/utils/VipHelper';
+import { formatVipLv } from '@scripts/utils/VipHelper';
 import { CURRENCY_TYPE, CurrencyType, GAME_TABS } from '@scripts/define/Define';
 import { UISwiper } from '@scripts/components/swiper/UISwiper';
 import VirtualList from '@scripts/components/virtualList/VirtualList';
@@ -59,6 +59,9 @@ export class HomeView extends UIView {
 
     protected onCreate(): void | Promise<void> {
         AppKernel.events.on('login:success', this.updateView, this);
+        AppKernel.events.on('user:update', this.updateUserInfo, this);
+        AppKernel.events.on('firstPack:update', this.onUpdateFirstPack, this);
+
         this.homeGamesScroll.renderEvent.target = this.node;
         this.homeGamesScroll.renderEvent.component = "HomeView";
         this.homeGamesScroll.renderEvent.handler = "onGameItemRender";
@@ -129,8 +132,9 @@ export class HomeView extends UIView {
         AppKernel.redpoint.on(RED_POINTS.DailyReward, dailyPoint, this);
     }
 
-    protected onDestroy(): void {
-        AppKernel.redpoint.offTarget(this);
+    private onUpdateFirstPack() {
+        const payStore = AppKernel.stores.payStore;
+        this.btnFirst.active = payStore.firstPackVisible;
     }
 
     private updateGameModel(type: CurrencyType) {
@@ -151,9 +155,27 @@ export class HomeView extends UIView {
         });
     }
 
-    private updateGameList() {
+    private async getRecentGames(tabIdx = -1) {
+        tabIdx = tabIdx === -1 ? this.gameTabIndex : tabIdx;
+        if (tabIdx !== 3) return;
+
+        const mainStore = AppKernel.stores.mainStore;
+        const gameStore = AppKernel.stores.gameStore;
+        const games = gameStore.getRecentGames(mainStore.currencyKind);
+        if (!games) {
+            AppKernel.ui.loading(true);
+            await gameStore.reqRecentGames(mainStore.currencyKind);
+            AppKernel.ui.loading(false);
+        }
+    }
+
+    private async updateGameList() {
         const currency = AppKernel.stores.mainStore.currencyKind;
         const tag = GameTabValus[this.gameTabIndex];
+        if (tag === GAME_TABS.Recent) {
+            await this.getRecentGames();
+        }
+
         const isHome = tag === GAME_TABS.Home;
         const gameStore = AppKernel.stores.gameStore;
         const games = gameStore.filterGames(tag, currency);
@@ -271,19 +293,22 @@ export class HomeView extends UIView {
         this.updateGameList();
     }
 
-    private updateView() {
+    private updateUserInfo() {
         const mainStore = AppKernel.stores.mainStore;
         const userInfo = mainStore.userInfo;
-        this.userNode.lblVip.string = formatVip(userInfo.vip);
+        this.userNode.lblVip.string = formatVipLv(userInfo.vip);
         this.scModel.lblValue.string = formatSCNum(mainStore.scScore);
         this.gcModel.lblValue.string = formatGCNum(mainStore.gcScore);
+    }
+
+    private updateView() {
+        const mainStore = AppKernel.stores.mainStore;
+        this.updateUserInfo();
         this.updateGameTabs(0);
         this.updateGameModel(mainStore.currencyKind);
         this.updateGameList();
         this.bannerNode.init(5);
-
-        const payStore = AppKernel.stores.payStore;
-        this.btnFirst.active = payStore.firstPackVisible;
+        this.onUpdateFirstPack();
     }
 
     private onBtnMailClick() {

+ 1 - 0
assets/scripts/modules/lobby/ui/LoginView/LoginView.ts

@@ -141,6 +141,7 @@ export class LoginView extends UIView {
 
   protected onDestroy(): void {
     this._googleWebBtn?.clear();
+    super.onDestroy();
   }
 
   private async onBtnGuestClick() {

+ 2 - 2
assets/scripts/modules/lobby/ui/StoreView/GoodsItem.ts

@@ -2,7 +2,7 @@ import { IGoodsConfig } from '@scripts/store/payStore/data';
 import { _decorator, Button, Component, Label, math, Node, Sprite } from 'cc';
 import { GoodsValue } from './GoodsValue';
 import { formatGoodsNum } from '@scripts/utils/Helper';
-import { formatVip } from '@scripts/utils/VipHelper';
+import { formatVipLv } from '@scripts/utils/VipHelper';
 import { AppKernel } from '@scripts/app/AppKernel';
 import { UIViewID } from '@scripts/define/UIDefine';
 import { setGoodsIcon } from '@scripts/utils/ResHelper';
@@ -58,7 +58,7 @@ export class GoodsItem extends Component {
         setGoodsIcon(this.sptIcon, goods.icon_id, this.isGCOnly);
         this.scValue.initValue(goods.sc, formatGoodsNum(goods.sc), goods.sc_reward, goods.sc_reward_display_percent, true);
         this.gcValue.initValue(goods.gc, formatGoodsNum(goods.gc), goods.gc_reward, goods.gc_reward_display_percent);
-        this.vipValue.initValue(goods.cp, formatVip(goods.cp), goods.cp_reward, goods.cp_reward_display_percent);
+        this.vipValue.initValue(goods.cp, formatVipLv(goods.cp), goods.cp_reward, goods.cp_reward_display_percent);
     }
 
     getIconName(index: number) {

+ 12 - 10
assets/scripts/modules/lobby/ui/StoreView/OrderView.ts

@@ -2,9 +2,9 @@ import { AppKernel } from '@scripts/app/AppKernel';
 import { UIViewID } from '@scripts/define/UIDefine';
 import { UIPopup } from '@scripts/framework/ui/UIPopup';
 import { IGoodsConfig } from '@scripts/store/payStore/data';
-import { formatGoodsNum } from '@scripts/utils/Helper';
+import { formatGCNum, formatGoodsNum, formatSCNum } from '@scripts/utils/Helper';
 import { addBtnClick } from '@scripts/utils/UIHelper';
-import { formatVip } from '@scripts/utils/VipHelper';
+import { formatCP, formatVipLv } from '@scripts/utils/VipHelper';
 import { _decorator, Component, find, instantiate, Label, Node, Sprite, SpriteFrame, tween, Tween } from 'cc';
 const { ccclass, property } = _decorator;
 
@@ -80,8 +80,6 @@ export class OrderView extends UIPopup<IOrderViewArg> {
 
         if (icon) {
             this.sptIcon.spriteFrame = icon;
-        } else {
-
         }
 
         const money = this.goods.money;
@@ -90,7 +88,8 @@ export class OrderView extends UIPopup<IOrderViewArg> {
 
         this.scValue.updateView(goods.sc, goods.sc_reward, goods.sc_reward_display_percent);
         this.gcValue.updateView(goods.gc, goods.gc_reward, goods.gc_reward_display_percent);
-        this.vipValue.lblValue.string = formatVip(goods.cp);
+        this.vipValue.updateView(goods.cp, goods.cp_reward, goods.cp_reward_display_percent);
+        this.vipValue.lblValue.string = "LV." + formatGoodsNum(goods.cp + goods.cp_reward);
 
         this.scNode.active = goods.sc > 0;
         this.gcNode.active = goods.gc > 0;
@@ -144,11 +143,14 @@ class ItemValue {
         if (descNode) this.lblDesc = descNode.getComponent(Label);
     }
 
-    updateView(value: number, bonus: number, percent: number) {
-        const total = formatGoodsNum(value + bonus);
-        this.lblValue.string = formatGoodsNum(value);
-        if (this.lblDesc) {
-            this.lblDesc.string = `BASE ${total} + ${percent} BONUS ${bonus}`;
+    updateView(value: number, bonus: number, percent: number, formater = formatGoodsNum) {
+        const hasBonus = this.lblDesc && percent > 0 && bonus > 0;
+        this.lblDesc.node.active = hasBonus;
+        this.lblValue.node.y = hasBonus ? 11.5 : -2;
+        this.lblValue.string = formater(value + bonus);
+        if (hasBonus) {
+            const base = formater(value);
+            this.lblDesc.string = `BASE ${base} + ${percent}% BONUS ${formater(bonus)}`;
         }
     }
 }

+ 7 - 2
assets/scripts/modules/lobby/ui/StoreView/StoreView.ts

@@ -43,14 +43,19 @@ export class StoreView extends UIView {
         this.goodsScroll.node.on(ScrollView.EventType.SCROLLING, this.onGoodsScrolling, this);
         this.scrollMask.opacity = 0;
         // addBtnClick(this.btnPayment, this.onBtnPaymentClick, this);
+
+        AppKernel.events.on('user:update', this.updateUserInfo, this);
     }
 
     protected onOpen(_params: unknown): void | Promise<void> {
+        this.updateUserInfo();
+        this.updateGoods();
+    }
+
+    private updateUserInfo() {
         const mainStore = AppKernel.stores.mainStore;
         this.lblSC.string = formatSCNum(mainStore.scScore);
         this.lblGC.string = formatGCNum(mainStore.gcScore);
-
-        this.updateGoods();
     }
 
     private updateGoods() {

+ 53 - 14
assets/scripts/modules/lobby/ui/StoreView/UIFirstPack.ts

@@ -1,10 +1,11 @@
+import { checkResponse } from '@scripts/api/ApiHelper';
 import { AppKernel } from '@scripts/app/AppKernel';
 import { UIPopup } from '@scripts/framework/ui/UIPopup';
 import { IFirstPack } from '@scripts/store/payStore/data';
 import { formatGCNum, formatSCNum } from '@scripts/utils/Helper';
 import { addBtnClick } from '@scripts/utils/UIHelper';
-import { formatVip } from '@scripts/utils/VipHelper';
-import { _decorator, Button, Component, Label, Node } from 'cc';
+import { formatVipLv } from '@scripts/utils/VipHelper';
+import { _decorator, Button, Component, find, instantiate, Label, Node } from 'cc';
 const { ccclass, property } = _decorator;
 
 @ccclass('UIFirstPack')
@@ -27,6 +28,15 @@ export class UIFirstPack extends UIPopup {
     @property(Button)
     btnPay: Button;
 
+    @property(Node)
+    btnDisabled: Node;
+
+    @property(Label)
+    lblDisabled: Label;
+
+    @property(Node)
+    testGears: Node;
+
     private _packInfo: IFirstPack;
 
     protected onCreate(): void | Promise<void> {
@@ -36,9 +46,10 @@ export class UIFirstPack extends UIPopup {
 
     protected async onBeforeOpen(_params: unknown) {
         const payStore = AppKernel.stores.payStore;
-        const pack = await payStore.reqFirstPackInfo();
-        this._packInfo = pack;
-        return !!pack;
+        const res = await payStore.reqFirstPackInfo();
+        if (!checkResponse(res)) return false;
+        this._packInfo = res.data;
+        return !!res.data;
     }
 
     private onBtnPayClick() {
@@ -46,20 +57,48 @@ export class UIFirstPack extends UIPopup {
     }
 
     protected onOpen(_params: unknown): void | Promise<void> {
-        if (!this._packInfo) {
+        const data = this._packInfo?.list?.[0];
+        if (!this._packInfo || !data) {
             this.lblGC.string = "";
             this.lblSC.string = "";
             this.lblVIP.string = "";
+            this.btnDisabled.active = true;
             return;
         }
-        const next_sequence = this._packInfo.next_sequence;
-        const current = next_sequence - 1;
-        const data = this._packInfo.list[current];
-        this.lblGC.string = formatGCNum(data.gc);
-        this.lblSC.string = formatSCNum(data.sc);
-        this.lblVIP.string = data.cp.toString();
-
-        this.lblButton.string = AppKernel.i18n.t("store.buynow", Number(data.money));
+
+
+        const gc = data.gc + data.gc_reward;
+        const sc = data.sc + data.sc_reward;
+        const cp = data.cp + data.cp_reward;
+        this.lblGC.string = formatGCNum(gc);
+        this.lblSC.string = formatSCNum(sc);
+        this.lblVIP.string = cp.toString();
+
+        const btn_str = AppKernel.i18n.t("store.buynow", Number(data.money));
+        this.lblButton.string = btn_str;
+        this.lblDisabled.string = btn_str;
+        this.btnDisabled.active = data.purchased;
+
+        //// Test
+        if (!data.purchased) {
+            for (let i = 0; i < data.gears.length; ++i) {
+                const gearIdx = i;
+                const gear = data.gears[i];
+                const gearPrefab = find("Button", this.testGears);
+                const gearItem = instantiate(gearPrefab);
+                gearItem.parent = this.testGears;
+                gearItem.active = true;
+
+                const lblName = find("Label", gearItem).getComponent(Label);
+                lblName.string = gear.name;
+
+                addBtnClick(gearItem, () => {
+                    const payServices = AppKernel.services.paymentServices;
+                    payServices.startPayment(data, gearIdx);
+                })
+            }
+
+        }
     }
 }
 

+ 1 - 0
assets/scripts/modules/lobby/ui/UserInfoView/UserInfoView.ts

@@ -141,6 +141,7 @@ export class UserInfoView extends UIView {
 
   protected onDestroy(): void {
     this._googleWebBtn?.clear();
+    super.onDestroy();
   }
 
   private async onBtnGuestClick() {

+ 64 - 5
assets/scripts/network/colyseus/ColyseusRoomClient.ts

@@ -33,6 +33,8 @@ export interface RoomClientCallbacks {
 }
 
 export interface RoomClientOptions {
+  /** Maximum time spent waiting for one joinOrCreate attempt. */
+  connectTimeoutMs?: number;
   pingIntervalMs?: number;
   pingTimeoutMax?: number;
   retryDelayMs?: number;
@@ -40,6 +42,13 @@ export interface RoomClientOptions {
   maxRetries?: number;
 }
 
+class ConnectionTimeoutError extends Error {
+  constructor(roomName: string, timeoutMs: number) {
+    super(`[ColyseusRoomClient] Timed out connecting to ${roomName} after ${timeoutMs}ms`);
+    this.name = 'ConnectionTimeoutError';
+  }
+}
+
 function parseColyseusEndpoint(rawUrl: string): ColyseusEndpoint {
   const value = rawUrl.trim();
   const match = value.match(
@@ -67,6 +76,7 @@ function parseColyseusEndpoint(rawUrl: string): ColyseusEndpoint {
  */
 export class ColyseusRoomClient {
   private readonly client: ColyseusClient;
+  private readonly connectTimeoutMs: number;
   private readonly pingIntervalMs: number;
   private readonly pingTimeoutMax: number;
   private readonly retryDelayMs: number;
@@ -91,11 +101,12 @@ export class ColyseusRoomClient {
   ) {
     const endpoint = parseColyseusEndpoint(serverUrl);
     this.client = new Colyseus.Client(endpoint) as ColyseusClient;
+    this.connectTimeoutMs = options.connectTimeoutMs ?? 15_000;
     this.pingIntervalMs = options.pingIntervalMs ?? 10_000;
     this.pingTimeoutMax = options.pingTimeoutMax ?? 3;
     this.retryDelayMs = options.retryDelayMs ?? 1_000;
     this.retryStepMs = options.retryStepMs ?? 2_000;
-    this.maxRetries = options.maxRetries ?? 5;
+    this.maxRetries = options.maxRetries ?? 3;
   }
 
   get isConnected(): boolean {
@@ -162,15 +173,17 @@ export class ColyseusRoomClient {
       }
 
       try {
-        const room = await this.client.joinOrCreate(this.roomName, joinOptions);
+        const room = await this.joinOrCreateWithTimeout(generation, joinOptions);
         if (!this.isCurrent(generation)) {
-          room.removeAllListeners();
-          await room.leave(true);
+          this.discardRoom(room);
           return false;
         }
         this.attachRoom(room);
         return true;
       } catch (error) {
+        // The SDK does not reject joinOrCreate while its underlying socket is
+        // reconnecting. Retrying here would create more hanging connections.
+        if (error instanceof ConnectionTimeoutError) break;
         console.warn(
           `[ColyseusRoomClient] Failed to join ${this.roomName} (${attempt + 1}/${this.maxRetries + 1})`,
           error,
@@ -185,12 +198,58 @@ export class ColyseusRoomClient {
     return false;
   }
 
+  private joinOrCreateWithTimeout(
+    generation: number,
+    joinOptions: Record<string, unknown>,
+  ): Promise<ColyseusRoom> {
+    let timedOut = false;
+    const joinTask = this.client.joinOrCreate(this.roomName, joinOptions);
+
+    // A timed-out SDK request may still resolve later. Close that stale room
+    // immediately so it cannot become an orphaned connection.
+    void joinTask.then(
+      (room) => {
+        if (timedOut || !this.isCurrent(generation)) void this.discardRoom(room);
+      },
+      () => undefined,
+    );
+
+    return new Promise<ColyseusRoom>((resolve, reject) => {
+      const timeout = setTimeout(() => {
+        timedOut = true;
+        reject(new ConnectionTimeoutError(this.roomName, this.connectTimeoutMs));
+      }, this.connectTimeoutMs);
+
+      void joinTask.then(
+        (room) => {
+          if (timedOut) return;
+          clearTimeout(timeout);
+          resolve(room);
+        },
+        (error: unknown) => {
+          if (timedOut) return;
+          clearTimeout(timeout);
+          reject(error);
+        },
+      );
+    });
+  }
+
+  private discardRoom(room: ColyseusRoom): void {
+    room.removeAllListeners();
+    // A room which has not completed the join handshake never emits onLeave,
+    // so waiting for leave() here can leave the caller stuck again.
+    void room.leave(false).catch((error) => {
+      console.warn(`[ColyseusRoomClient] Failed to close stale ${this.roomName}`, error);
+    });
+  }
+
   private attachRoom(room: ColyseusRoom): void {
     this.room = room;
     this.pendingPings = 0;
 
     room.onMessage('*', (type: string, message: unknown) => {
-      //console.log('[Hall ← Server]', type, message);
+      console.log('[Hall ← Server]', type, message);
       if (type === 'PONG') {
         this.pendingPings = Math.max(0, this.pendingPings - 1);
         return;

+ 259 - 7
assets/scripts/platform/FaceBook/FBAndroidImpl.ts

@@ -1,15 +1,267 @@
-import { FacebookImpl } from "./FacebookImpl";
+import { native } from "cc";
+import {
+    FacebookImpl,
+    IFBEventParams,
+    IFBLoginRes,
+    IFBShareOptions,
+    IFBShareRes,
+} from "./FacebookImpl";
+
+const REQUEST_EVENT = "facebook.request";
+const RESPONSE_EVENT = "facebook.response";
+const DEFAULT_TIMEOUT_MS = 15_000;
+const USER_OPERATION_TIMEOUT_MS = 180_000;
+
+interface NativeError {
+    code?: string;
+    message?: string;
+}
+
+interface NativeResponse<T = Record<string, never>> {
+    requestId: string;
+    method: string;
+    success: boolean;
+    cancelled?: boolean;
+    data?: T;
+    error?: NativeError;
+}
+
+interface LoginData {
+    accessToken?: string;
+    userId?: string;
+}
+
+interface ShareData {
+    postId?: string;
+}
+
+interface EventData {
+    accepted?: boolean;
+}
+
+interface PendingRequest {
+    method: string;
+    resolve: (response: NativeResponse<any>) => void;
+    reject: (error: Error) => void;
+    timer: ReturnType<typeof setTimeout>;
+}
 
 export class FBAndroidImpl extends FacebookImpl {
-    init() {
+    private _requestSeed = 0;
+    private _pendingRequests = new Map<string, PendingRequest>();
+    private _initPromise: Promise<boolean> | null = null;
+    private readonly _responseListener = (payload: string) => this.onNativeResponse(payload);
+
+    init(): void {
+        if (this._initPromise) return;
+
+        native.jsbBridgeWrapper.addNativeEventListener(RESPONSE_EVENT, this._responseListener);
+        this._initPromise = this.request("init")
+            .then(() => {
+                this.onInited();
+                return true;
+            })
+            .catch((error) => {
+                console.error("[Facebook][Android] 初始化失败:", error);
+                return false;
+            });
+    }
+
+    protected async onLogin(): Promise<IFBLoginRes> {
+        if (!(await this.ensureInitialized())) {
+            return this.loginFailure("Facebook Android SDK 尚未初始化。");
+        }
+
+        try {
+            const response = await this.request<LoginData>(
+                "login",
+                { permissions: ["public_profile", "email"] },
+                USER_OPERATION_TIMEOUT_MS,
+            );
+            if (response.cancelled) {
+                return {
+                    accessToken: "",
+                    isCancel: true,
+                    status: "cancelled",
+                };
+            }
+
+            const accessToken = response.data?.accessToken || "";
+            if (!accessToken) {
+                return this.loginFailure("Facebook 登录未返回 Access Token。");
+            }
+
+            return {
+                accessToken,
+                isCancel: false,
+                status: "connected",
+            };
+        } catch (error) {
+            return this.loginFailure(this.errorMessage(error));
+        }
+    }
+
+    async logOut(): Promise<boolean> {
+        if (!(await this.ensureInitialized())) return false;
+        try {
+            await this.request("logout");
+            return true;
+        } catch (error) {
+            console.error("[Facebook][Android] 登出失败:", error);
+            return false;
+        }
+    }
+
+    async share(options: IFBShareOptions): Promise<IFBShareRes> {
+        if (!(await this.ensureInitialized())) {
+            return {
+                success: false,
+                isCancel: false,
+                reason: "Facebook Android SDK 尚未初始化。",
+            };
+        }
+
+        try {
+            const response = await this.request<ShareData>(
+                "share",
+                {
+                    url: options?.url || "",
+                    hashtag: options?.hashtag || "",
+                    quote: options?.quote || "",
+                },
+                USER_OPERATION_TIMEOUT_MS,
+            );
+            return {
+                success: !response.cancelled,
+                isCancel: Boolean(response.cancelled),
+                postId: response.data?.postId,
+            };
+        } catch (error) {
+            return {
+                success: false,
+                isCancel: false,
+                reason: this.errorMessage(error),
+            };
+        }
+    }
+
+    async logEvent(
+        eventName: string,
+        parameters: IFBEventParams = {},
+        valueToSum?: number,
+    ): Promise<boolean> {
+        if (!(await this.ensureInitialized())) return false;
+        try {
+            const params: Record<string, unknown> = { eventName, parameters };
+            if (valueToSum !== undefined) params.valueToSum = valueToSum;
+            const response = await this.request<EventData>("logEvent", params);
+            return response.data?.accepted === true;
+        } catch (error) {
+            console.error(`[Facebook][Android] 事件 ${eventName} 记录失败:`, error);
+            return false;
+        }
+    }
+
+    async logPurchase(
+        amount: number,
+        currency: string,
+        parameters: IFBEventParams = {},
+    ): Promise<boolean> {
+        if (!(await this.ensureInitialized())) return false;
+        try {
+            const response = await this.request<EventData>("logPurchase", {
+                amount,
+                currency,
+                parameters,
+            });
+            return response.data?.accepted === true;
+        } catch (error) {
+            console.error("[Facebook][Android] 购买事件记录失败:", error);
+            return false;
+        }
+    }
+
+    async flushEvents(): Promise<boolean> {
+        if (!(await this.ensureInitialized())) return false;
+        try {
+            const response = await this.request<EventData>("flushEvents");
+            return response.data?.accepted === true;
+        } catch (error) {
+            console.error("[Facebook][Android] App Events 刷新失败:", error);
+            return false;
+        }
+    }
+
+    private async ensureInitialized(): Promise<boolean> {
+        if (!this._initPromise) this.init();
+        return this._initPromise!;
+    }
+
+    private request<T = Record<string, never>>(
+        method: string,
+        params: Record<string, unknown> = {},
+        timeoutMs = DEFAULT_TIMEOUT_MS,
+    ): Promise<NativeResponse<T>> {
+        const requestId = `${Date.now()}-${++this._requestSeed}`;
+        return new Promise<NativeResponse<T>>((resolve, reject) => {
+            const timer = setTimeout(() => {
+                this._pendingRequests.delete(requestId);
+                reject(new Error(`Facebook ${method} 请求超时。`));
+            }, timeoutMs);
+
+            this._pendingRequests.set(requestId, {
+                method,
+                resolve,
+                reject,
+                timer,
+            });
+
+            try {
+                native.jsbBridgeWrapper.dispatchEventToNative(
+                    REQUEST_EVENT,
+                    JSON.stringify({ requestId, method, params }),
+                );
+            } catch (error) {
+                clearTimeout(timer);
+                this._pendingRequests.delete(requestId);
+                reject(error instanceof Error ? error : new Error(String(error)));
+            }
+        });
+    }
+
+    private onNativeResponse(payload: string): void {
+        let response: NativeResponse<any>;
+        try {
+            response = JSON.parse(payload) as NativeResponse<any>;
+        } catch (error) {
+            console.error("[Facebook][Android] 无法解析原生响应:", error);
+            return;
+        }
+
+        const pending = this._pendingRequests.get(response.requestId);
+        if (!pending) return;
+
+        clearTimeout(pending.timer);
+        this._pendingRequests.delete(response.requestId);
+        if (response.success) {
+            pending.resolve(response);
+            return;
+        }
 
+        const code = response.error?.code ? `[${response.error.code}] ` : "";
+        pending.reject(new Error(`${code}${response.error?.message || `${pending.method} 执行失败。`}`));
     }
 
-    protected async onLogin() {
-        return { accessToken: "", isCancel: false };
+    private loginFailure(reason: string): IFBLoginRes {
+        return {
+            accessToken: "",
+            isCancel: false,
+            status: "error",
+            reason,
+        };
     }
 
-    async logOut() {
-        return true;
+    private errorMessage(error: unknown): string {
+        return error instanceof Error ? error.message : String(error);
     }
-}
+}

+ 61 - 2
assets/scripts/platform/FaceBook/FBH5Impl.ts

@@ -1,5 +1,11 @@
 import AppConfig from "@scripts/app/AppConfig";
-import { FacebookImpl, IFBLoginRes } from "./FacebookImpl";
+import {
+    FacebookImpl,
+    IFBEventParams,
+    IFBLoginRes,
+    IFBShareOptions,
+    IFBShareRes,
+} from "./FacebookImpl";
 
 export class FBH5Impl extends FacebookImpl {
     private _isLogined = false;
@@ -92,4 +98,57 @@ export class FBH5Impl extends FacebookImpl {
             })
         })
     }
-}
+
+    async share(options: IFBShareOptions): Promise<IFBShareRes> {
+        return new Promise<IFBShareRes>((resolve) => {
+            //@ts-ignore
+            FB.ui({
+                method: 'share',
+                href: options.url,
+                hashtag: options.hashtag,
+                quote: options.quote,
+            }, (response) => {
+                resolve({
+                    success: Boolean(response),
+                    isCancel: !response,
+                    postId: response?.post_id,
+                });
+            });
+        });
+    }
+
+    async logEvent(
+        eventName: string,
+        parameters: IFBEventParams = {},
+        valueToSum?: number,
+    ): Promise<boolean> {
+        try {
+            //@ts-ignore
+            FB.AppEvents.logEvent(eventName, valueToSum, parameters);
+            return true;
+        } catch (error) {
+            console.error(`[Facebook][Web] 事件 ${eventName} 记录失败:`, error);
+            return false;
+        }
+    }
+
+    async logPurchase(
+        amount: number,
+        currency: string,
+        parameters: IFBEventParams = {},
+    ): Promise<boolean> {
+        try {
+            //@ts-ignore
+            FB.AppEvents.logPurchase(amount, currency, parameters);
+            return true;
+        } catch (error) {
+            console.error('[Facebook][Web] 购买事件记录失败:', error);
+            return false;
+        }
+    }
+
+    async flushEvents(): Promise<boolean> {
+        // Facebook JavaScript SDK 没有公开的手动 flush API,由 SDK 自行批量上报。
+        return true;
+    }
+}

+ 35 - 2
assets/scripts/platform/FaceBook/FBIOSImpl.ts

@@ -1,4 +1,9 @@
-import { FacebookImpl } from "./FacebookImpl";
+import {
+    FacebookImpl,
+    IFBEventParams,
+    IFBShareOptions,
+    IFBShareRes,
+} from "./FacebookImpl";
 
 export class FBIOSImpl extends FacebookImpl {
     init() {
@@ -13,4 +18,32 @@ export class FBIOSImpl extends FacebookImpl {
     async logOut() {
         return true;
     }
-}
+
+    async share(_options: IFBShareOptions): Promise<IFBShareRes> {
+        return {
+            success: false,
+            isCancel: false,
+            reason: 'Facebook iOS 原生实现尚未接入。',
+        };
+    }
+
+    async logEvent(
+        _eventName: string,
+        _parameters: IFBEventParams = {},
+        _valueToSum?: number,
+    ): Promise<boolean> {
+        return false;
+    }
+
+    async logPurchase(
+        _amount: number,
+        _currency: string,
+        _parameters: IFBEventParams = {},
+    ): Promise<boolean> {
+        return false;
+    }
+
+    async flushEvents(): Promise<boolean> {
+        return false;
+    }
+}

+ 34 - 2
assets/scripts/platform/FaceBook/FacebookImpl.ts

@@ -5,12 +5,28 @@ export interface IFBLoginRes {
     reason?: string;
 }
 
+export type IFBEventValue = string | number;
+export type IFBEventParams = Record<string, IFBEventValue>;
+
+export interface IFBShareOptions {
+    url: string;
+    hashtag?: string;
+    quote?: string;
+}
+
+export interface IFBShareRes {
+    success: boolean;
+    isCancel: boolean;
+    postId?: string;
+    reason?: string;
+}
+
 export abstract class FacebookImpl {
     private _isInitialized = false;
 
     public get isInitialized() { return this._isInitialized; }
 
-    abstract init();
+    abstract init(): void;
 
     protected onInited() {
         this._isInitialized = true;
@@ -24,4 +40,20 @@ export abstract class FacebookImpl {
     protected abstract onLogin(): Promise<IFBLoginRes>;
 
     abstract logOut(): Promise<boolean>;
-}
+
+    abstract share(options: IFBShareOptions): Promise<IFBShareRes>;
+
+    abstract logEvent(
+        eventName: string,
+        parameters?: IFBEventParams,
+        valueToSum?: number,
+    ): Promise<boolean>;
+
+    abstract logPurchase(
+        amount: number,
+        currency: string,
+        parameters?: IFBEventParams,
+    ): Promise<boolean>;
+
+    abstract flushEvents(): Promise<boolean>;
+}

+ 167 - 4
assets/scripts/platform/Google/GoogleAndroidImpl.ts

@@ -1,14 +1,177 @@
+import { native } from "cc";
 import { GoogleImpl, IGoogleLoginRes } from "./GoogleImpl";
+import AppConfig from "@scripts/app/AppConfig";
+
+const REQUEST_EVENT = "google.request";
+const RESPONSE_EVENT = "google.response";
+const DEFAULT_TIMEOUT_MS = 15_000;
+const LOGIN_TIMEOUT_MS = 180_000;
+// const GOOGLE_CLIENTID = "984811369314-tgai3uj7lb3dmvpuisehm7fb98o60vsq.apps.googleusercontent.com";
+
+interface NativeResponse<T = Record<string, never>> {
+    requestId: string;
+    method: string;
+    success: boolean;
+    cancelled?: boolean;
+    data?: T;
+    error?: {
+        code?: string;
+        message?: string;
+    };
+}
+
+interface GoogleLoginData {
+    idToken?: string;
+    email?: string;
+    displayName?: string;
+    givenName?: string;
+    familyName?: string;
+    profilePictureUrl?: string;
+}
+
+interface PendingRequest {
+    method: string;
+    resolve: (response: NativeResponse<any>) => void;
+    reject: (error: Error) => void;
+    timer: ReturnType<typeof setTimeout>;
+}
 
 export class GoogleAndroidImpl extends GoogleImpl {
-    init() {
+    private _requestSeed = 0;
+    private _pendingRequests = new Map<string, PendingRequest>();
+    private _initPromise: Promise<boolean> | null = null;
+    private readonly _responseListener = (payload: string) => this.onNativeResponse(payload);
+
+    init(): void {
+        if (this._initPromise) return;
 
+        native.jsbBridgeWrapper.addNativeEventListener(RESPONSE_EVENT, this._responseListener);
+        this._initPromise = this.request("init", { webClientId: AppConfig.GoogleID })
+            .then(() => {
+                this.onInited();
+                return true;
+            })
+            .catch((error) => {
+                console.error("[Google][Android] 初始化失败:", error);
+                return false;
+            });
     }
+
     protected async onLogin(): Promise<IGoogleLoginRes> {
-        return null;
+        if (!(await this.ensureInitialized())) {
+            return this.loginFailure("Google Android 登录尚未初始化。");
+        }
+
+        try {
+            const response = await this.request<GoogleLoginData>(
+                "login",
+                {},
+                LOGIN_TIMEOUT_MS,
+            );
+            if (response.cancelled) {
+                return {
+                    accessToken: "",
+                    isCancel: true,
+                    status: "cancelled",
+                };
+            }
+
+            const idToken = response.data?.idToken || "";
+            if (!idToken) {
+                return this.loginFailure("Google 登录未返回 ID Token。");
+            }
+            return {
+                // 与 H5 保持一致:accessToken 字段承载需要交给服务端验证的 Google ID Token。
+                accessToken: idToken,
+                isCancel: false,
+                status: "connected",
+            };
+        } catch (error) {
+            return this.loginFailure(this.errorMessage(error));
+        }
     }
+
     async logOut(): Promise<boolean> {
-        return true
+        if (!(await this.ensureInitialized())) return false;
+        try {
+            await this.request("logout");
+            return true;
+        } catch (error) {
+            console.error("[Google][Android] 登出失败:", error);
+            return false;
+        }
     }
 
-}
+    private async ensureInitialized(): Promise<boolean> {
+        if (!this._initPromise) this.init();
+        return this._initPromise!;
+    }
+
+    private request<T = Record<string, never>>(
+        method: string,
+        params: Record<string, unknown> = {},
+        timeoutMs = DEFAULT_TIMEOUT_MS,
+    ): Promise<NativeResponse<T>> {
+        const requestId = `${Date.now()}-${++this._requestSeed}`;
+        return new Promise<NativeResponse<T>>((resolve, reject) => {
+            const timer = setTimeout(() => {
+                this._pendingRequests.delete(requestId);
+                reject(new Error(`Google ${method} 请求超时。`));
+            }, timeoutMs);
+
+            this._pendingRequests.set(requestId, {
+                method,
+                resolve,
+                reject,
+                timer,
+            });
+
+            try {
+                native.jsbBridgeWrapper.dispatchEventToNative(
+                    REQUEST_EVENT,
+                    JSON.stringify({ requestId, method, params }),
+                );
+            } catch (error) {
+                clearTimeout(timer);
+                this._pendingRequests.delete(requestId);
+                reject(error instanceof Error ? error : new Error(String(error)));
+            }
+        });
+    }
+
+    private onNativeResponse(payload: string): void {
+        let response: NativeResponse<any>;
+        try {
+            response = JSON.parse(payload) as NativeResponse<any>;
+        } catch (error) {
+            console.error("[Google][Android] 无法解析原生响应:", error);
+            return;
+        }
+
+        const pending = this._pendingRequests.get(response.requestId);
+        if (!pending) return;
+
+        clearTimeout(pending.timer);
+        this._pendingRequests.delete(response.requestId);
+        if (response.success) {
+            pending.resolve(response);
+            return;
+        }
+
+        const code = response.error?.code ? `[${response.error.code}] ` : "";
+        pending.reject(new Error(`${code}${response.error?.message || `${pending.method} 执行失败。`}`));
+    }
+
+    private loginFailure(reason: string): IGoogleLoginRes {
+        return {
+            accessToken: "",
+            isCancel: false,
+            status: "error",
+            reason,
+        };
+    }
+
+    private errorMessage(error: unknown): string {
+        return error instanceof Error ? error.message : String(error);
+    }
+}

+ 12 - 8
assets/scripts/platform/Google/GoogleH5Impl.ts

@@ -1,6 +1,6 @@
-import AppConfig from "@scripts/app/AppConfig";
 import { Camera, Canvas, Node, screen, UITransform, Vec3 } from "cc";
 import { GoogleImpl, IGoogleBtnLogin, IGoogleLoginRes } from "./GoogleImpl";
+import AppConfig from "@scripts/app/AppConfig";
 
 interface GoogleCredentialResponse {
     credential?: string;
@@ -35,6 +35,7 @@ interface GoogleSdkWindow extends Window {
 
 const GOOGLE_SDK_ID = "google-identity-services";
 const GOOGLE_SDK_URL = "https://accounts.google.com/gsi/client";
+// const GOOGLE_CLIENTID = "984811369314-r66m16sppu58oej4lji56bgnernamhut.apps.googleusercontent.com";
 
 export class GoogleH5Impl extends GoogleImpl {
     private _initPromise: Promise<void> | null = null;
@@ -49,7 +50,8 @@ export class GoogleH5Impl extends GoogleImpl {
             .then(() => {
                 const googleIdentity = this.getGoogleIdentity();
                 if (!googleIdentity) {
-                    throw new Error("Google Identity Services 初始化失败");
+                    console.error("Google Identity Services 初始化失败");
+                    return;
                 }
 
                 googleIdentity.initialize({
@@ -63,7 +65,7 @@ export class GoogleH5Impl extends GoogleImpl {
             })
             .catch(error => {
                 this._initPromise = null;
-                throw error;
+                console.error(error);
             });
 
         return this._initPromise;
@@ -192,19 +194,21 @@ export class GoogleH5Impl extends GoogleImpl {
             };
             const onLoad = () => {
                 clearListeners();
-                this.getGoogleIdentity()
-                    ? resolve()
-                    : reject(new Error("Google Identity Services SDK 不可用"));
+                const id = this.getGoogleIdentity();
+                id ? resolve() : reject();
+                if (!id) console.error("Google Identity Services SDK 不可用");
             };
             const onError = () => {
                 clearListeners();
                 script?.remove();
-                reject(new Error("Google Identity Services SDK 下载失败"));
+                reject();
+                console.error("Google Identity Services SDK 下载失败");
             };
             const timeoutId = window.setTimeout(() => {
                 clearListeners();
                 script?.remove();
-                reject(new Error("Google Identity Services SDK 加载超时"));
+                reject();
+                console.error("Google Identity Services SDK 加载超时");
             }, 15000);
 
             script.addEventListener("load", onLoad, { once: true });

+ 31 - 13
assets/scripts/request/HttpClient.ts

@@ -1,17 +1,20 @@
 import { API_BASE_URL } from '../api';
-import { LOCAL_SIGN } from '../constants/Localkey';
-import { localStorageGet } from '../utils/Helper';
 import { getComParams } from './CommonParams';
 import {
   ApiEndpoint,
   ApiEnvelope,
   ApiParams,
+  HttpAbortedError,
   HttpError,
+  HttpFailedError,
   HttpMethod,
+  HttpParseError,
+  HttpTimeoutError,
 } from './RequestTypes';
+import LZString from 'lz-string';
 
 export interface ApiCodec {
-  encode(json: string): string;
+  encode(params: ApiParams): string;
   decode(payload: string): string;
 }
 
@@ -54,8 +57,10 @@ export class HttpClient {
       } catch (error) {
         console.error(error);
         if (attempt >= this.retryCount) {
-          console.log(`retry ${attempt}/${this.retryCount} url:${path}`);
-          return null;
+          return {
+            code: -1,
+            data: null,
+          };
         }
         await this.delay(this.retryDelayMs);
       }
@@ -69,8 +74,7 @@ export class HttpClient {
     Object.assign(params, comParams);
     const url = this.resolveUrl(path);
     const useCompression = Boolean(this.options.compress && this.options.codec);
-    const rawJson = JSON.stringify(params);
-    const payload = useCompression ? { lz: this.options.codec!.encode(rawJson) } : params;
+    const payload = useCompression ? { lz: this.options.codec!.encode(params) } : params;
     const requestUrl = method === 'GET' ? this.appendQuery(url, payload) : url;
 
     return new Promise((resolve, reject) => {
@@ -89,21 +93,21 @@ export class HttpClient {
         }
         try {
           let text = xhr.responseText;
-          if (useCompression && text && !text.replace(/^\s+/, '').startsWith('{')) {
-            text = this.options.codec!.decode(text);
+          if (text && this.options.codec && !text.replace(/^\s+/, '').startsWith('{')) {
+            text = this.options.codec.decode(text);
           }
           resolve(JSON.parse(text) as ApiEnvelope<T>);
         } catch (error) {
-          reject(new HttpError(
+          reject(new HttpParseError(
             `Invalid API response: ${error instanceof Error ? error.message : String(error)}`,
             xhr.status,
             requestUrl,
           ));
         }
       };
-      xhr.onerror = () => reject(new HttpError('Network request failed', xhr.status, requestUrl));
-      xhr.ontimeout = () => reject(new HttpError('Network request timed out', 0, requestUrl));
-      xhr.onabort = () => reject(new HttpError('Network request aborted', 0, requestUrl));
+      xhr.onerror = () => reject(new HttpFailedError('Network request failed', xhr.status, requestUrl));
+      xhr.ontimeout = () => reject(new HttpTimeoutError('Network request timed out', 0, requestUrl));
+      xhr.onabort = () => reject(new HttpAbortedError('Network request aborted', 0, requestUrl));
       xhr.send(method === 'POST' ? JSON.stringify(payload) : null);
     });
   }
@@ -136,4 +140,18 @@ export const Http = new HttpClient({
   timeoutMs: 10_000,
   retryCount: 2,
   retryDelayMs: 1_000,
+  compress: false,
+  codec: {
+    encode: function (params: ApiParams): string {
+      return LZString.compressToBase64(JSON.stringify(params));
+    },
+    decode: function (payload: string): string {
+      try {
+        return LZString.decompressFromBase64(payload);
+      } catch (error) {
+        console.error(`api response decompress error: `, error);
+        return payload;
+      }
+    }
+  }
 });

+ 28 - 0
assets/scripts/request/RequestTypes.ts

@@ -35,3 +35,31 @@ export class HttpError extends Error {
     this.name = 'HttpError';
   }
 }
+
+export class HttpFailedError extends Error {
+  constructor(message: string, readonly status: number, readonly url: string) {
+    super(message);
+    this.name = 'HttpFailedError';
+  }
+}
+
+export class HttpTimeoutError extends Error {
+  constructor(message: string, readonly status: number, readonly url: string) {
+    super(message);
+    this.name = 'HttpTimeoutError';
+  }
+}
+
+export class HttpAbortedError extends Error {
+  constructor(message: string, readonly status: number, readonly url: string) {
+    super(message);
+    this.name = 'HttpAbortedError';
+  }
+}
+
+export class HttpParseError extends Error {
+  constructor(message: string, readonly status: number, readonly url: string) {
+    super(message);
+    this.name = 'HttpParseError';
+  }
+}

+ 81 - 8
assets/scripts/services/AppLobby.ts

@@ -5,12 +5,16 @@ import { localStorageGet, localStorageRemove } from "@scripts/utils/Helper";
 import { CURRENCY_TYPE } from "@scripts/define/Define";
 import { IGameInfo } from "@scripts/store/gameStore/data";
 
+let firstLaunch = true;
+
 class AppLobby {
+    private _tempOnConnected;
+
     async launchLobby() {
         const mainStore = AppKernel.stores.mainStore;
-        const firstLaunch = mainStore.firstLaunch;
 
         if (firstLaunch) {
+            firstLaunch = false;
             AppKernel.sound.playBgm();
             // 自动登录
             await mainStore.reqGetRoutes()
@@ -65,15 +69,17 @@ class AppLobby {
 
     async openGame(game: IGameInfo) {
         AppKernel.ui.loading(true);
-        const hallClient = AppKernel.services.hallClient;
         const mainStore = AppKernel.stores.mainStore;
         const gameStore = AppKernel.stores.gameStore;
-        gameStore.gameID = game.gameID;
-        hallClient.sendState();
 
         const url = await gameStore.reqLaunchGame(game.gameCode, mainStore.currencyKind);
         AppKernel.ui.loading(false);
         console.log('game url: ', url);
+        if (!!url) {
+            const currency = mainStore.currencyKind;
+            gameStore.onEnterGame(game, currency);
+            AppKernel.services.hallClient.sendState();
+        }
 
         // const gameInfo = await mainStore.getGameCardInfo(gameID);
         // if (!gameInfo) {
@@ -112,9 +118,15 @@ class AppLobby {
     async onPaymentFinished() {
         AppKernel.events.emit('hall:pay-finished');
         const mainStore = AppKernel.stores.mainStore;
-        const payStore = AppKernel.stores.payStore;
-        payStore.reqWithdrawBaseInfo();
-        mainStore.reqUserInfo();
+        AppKernel.services.paymentServices.onPaySuccess();
+
+        AppKernel.ui.toast("充值成功!!");
+        const updater = mainStore.reqUserInfo();
+        await Promise.all([
+            updater,
+        ])
+
+        AppKernel.events.emit('user:update')
     }
 
     async logOut() {
@@ -122,17 +134,78 @@ class AppLobby {
         const mainStore = AppKernel.stores.mainStore;
         if (!await mainStore.reqLogout()) {
             console.error('log out failed!!!');
-
             AppKernel.ui.loading(false);
             return;
         }
 
+        await this.openLogin();
+    }
+
+    async openLogin() {
+        if (!AppKernel.ui.isLoading) AppKernel.ui.loading(true);
         localStorageRemove(LOCAL_SIGN);
         AppKernel.fb?.logOut();
+        AppKernel.services.hallClient.disconnect();
         await AppKernel.stores.onUserLogout();
         await AppKernel.ui.open(UIViewID.LoginView);
         AppKernel.ui.loading(false);
     }
+
+    onHallClientConnected() {
+
+    }
+
+    hallClientConnected() {
+        console.log('[HallClient] connected!!!');
+        const onConnected = this._tempOnConnected;
+        onConnected && onConnected();
+
+        AppKernel.stores.mainStore.setServerKick(false);
+        AppKernel.events.emit('hall:connected');
+    }
+
+    hallClientTimeout() {
+        console.log('[HallClient] timeout!!!');
+        AppKernel.events.emit('hall:timeout');
+        AppKernel.ui.dialog({
+            message: AppKernel.i18n.t("home.disconnected"),
+            lblConfirm: AppKernel.i18n.t("home.reconnect"),
+            callBack: (res) => {
+                if (res) {
+                    this.hallClientReconnect();
+                }
+                return true;
+            }
+        })
+    }
+
+    handleKickout() {
+        console.log('[HallClient] kuckout!!!');
+        delete this._tempOnConnected;
+        const mainStore = AppKernel.stores.mainStore;
+        mainStore.setServerKick(true);
+        AppKernel.events.emit('hall:kicked');
+        AppKernel.ui.dialog({
+            message: AppKernel.i18n.t("home.offline"),
+            lblConfirm: AppKernel.i18n.t("home.reconnect"),
+            callBack: (res) => {
+                if (res) {
+                    this.hallClientReconnect();
+                }
+                return true;
+            }
+        })
+    }
+
+    private hallClientReconnect() {
+        this._tempOnConnected = () => {
+            delete this._tempOnConnected;
+            AppKernel.ui.loading(false);
+        }
+        AppKernel.ui.loading(true);
+        const hallClient = AppKernel.services.hallClient;
+        hallClient.connect();
+    }
 }
 
 export const appLobby = new AppLobby();

+ 8 - 7
assets/scripts/services/hall/HallClient.ts

@@ -81,12 +81,11 @@ class HallClient {
   private createClient(serverUrl: string): ColyseusRoomClient {
     return new ColyseusRoomClient(APP_SERVER.ROOM_HALL, serverUrl, {
       onConnected: () => {
-        AppKernel.stores.mainStore.setServerKick(false);
-        AppKernel.events.emit('hall:connected');
+        AppKernel.appLobby.hallClientConnected();
         this.sendRegistration();
       },
       onDisconnected: (code) => AppKernel.events.emit('hall:disconnected', { code }),
-      onTimeout: () => AppKernel.events.emit('hall:timeout'),
+      onTimeout: () => AppKernel.appLobby.hallClientTimeout(),
       onMessage: (type, message) => this.onMessage(type, message),
     });
   }
@@ -97,8 +96,7 @@ class HallClient {
 
   private onMessage(type: string, rawMessage: unknown): void {
     const message = this.asMessage(rawMessage);
-    AppKernel.events.emit('hall:message', { type, message });
-
+    console.log(`[HallClient] onMessage type:${type}  cmd:${message?.cmd}`);
     switch (type) {
       case 'operate':
         this.handleOperate(message);
@@ -115,8 +113,7 @@ class HallClient {
         this.sendState();
         break;
       case 'kick':
-        AppKernel.stores.mainStore.setServerKick(true);
-        AppKernel.events.emit('hall:kicked', message);
+        AppKernel.appLobby.handleKickout();
         break;
     }
   }
@@ -125,6 +122,10 @@ class HallClient {
     switch (message.cmd) {
       case 'pay_finish': {
         // const payMessage = message as HallPayFinishMessage;
+        const data = message.data;
+        if (!!data && !!data.event) {
+          trackEvent('pay', data.params)
+        }
         AppKernel.appLobby.onPaymentFinished();
         break;
       }

+ 11 - 6
assets/scripts/services/login/LoginService.ts

@@ -1,12 +1,13 @@
 import { Login3rdType, APP_LOGIN_TYPE } from "@scripts/define/Define";
 import { AppKernel } from "../../app/AppKernel";
 import { IGoogleLoginRes } from "@scripts/platform/Google/GoogleImpl";
+import { checkResponse, getHttpMsg } from "@scripts/api/ApiHelper";
 
 class LoginService {
     async guestLogin() {
         const mainStore = AppKernel.stores.mainStore;
-        const loggedIn = await mainStore.reqLogin({ type: APP_LOGIN_TYPE.GUEST });
-        if (!!loggedIn) {
+        const res = await mainStore.reqLogin({ type: APP_LOGIN_TYPE.GUEST });
+        if (checkResponse(res)) {
             await AppKernel.appLobby.onLoginSuccess();
             return true;
         }
@@ -35,21 +36,24 @@ class LoginService {
 
     async login3rd(type: Login3rdType, token: string) {
         const mainStore = AppKernel.stores.mainStore;
-        const loggedIn = await mainStore.reqLogin3rd(type, token);
-        if (!!loggedIn) {
+        const res = await mainStore.reqLogin3rd(type, token);
+        if (checkResponse(res)) {
             await AppKernel.appLobby.onLoginSuccess();
             return true;
         }
+
         return false;
     }
 
     async loginAccount(account: string, password: string) {
         const mainStore = AppKernel.stores.mainStore;
-        mainStore.reqLogin({
+        const res = await mainStore.reqLogin({
             type: APP_LOGIN_TYPE.ID,
             password,
             account,
         })
+
+        return checkResponse(res);
     }
 
     async logOut() {
@@ -57,7 +61,8 @@ class LoginService {
 
     async register({ account, code, password, act }) {
         const mainStore = AppKernel.stores.mainStore;
-        await mainStore.reqRegister({ account, code, password, act })
+        const res = await mainStore.reqRegister({ account, code, password, act })
+        return checkResponse(res);
     }
 }
 

+ 13 - 0
assets/scripts/services/pay/PaymentServices.ts

@@ -29,6 +29,19 @@ class PaymentServices {
 
         AppKernel.ui.loading(false);
     }
+
+    onPaySuccess() {
+        this.updateFirstPackState();
+        const payStore = AppKernel.stores.payStore;
+        payStore.reqWithdrawBaseInfo();
+    }
+
+    private async updateFirstPackState() {
+        const payStore = AppKernel.stores.payStore;
+        await payStore.reqFirstPackVisible();
+        AppKernel.events.emit('firstPack:update');
+        AppKernel.ui.close(UIViewID.UIFirstPack);
+    }
 }
 
 export const paymentServices = new PaymentServices();

+ 1 - 1
assets/scripts/store/checkInStore/CheckInStore.ts

@@ -1,8 +1,8 @@
 import {
     APP_CHECKIN_CHECK,
     APP_CHECKIN_GET,
-    success,
 } from '../../api';
+import { success } from '@scripts/api/ApiHelper';
 import { ApiEnvelope, Http } from '../../request';
 import { ICheckInData, ICheckInResponseData } from './data';
 

+ 30 - 21
assets/scripts/store/gameStore/GameStore.ts

@@ -1,5 +1,6 @@
-import { APP_FAVO_ADD, APP_FAVO_REMOVE, APP_GAME_LAUNCH, APP_GAME_LIST, success } from "@scripts/api";
+import { APP_FAVO_ADD, APP_FAVO_REMOVE, APP_GAME_LAUNCH, APP_GAME_LIST } from "@scripts/api";
 import { Http } from "@scripts/request";
+import { checkResponse, success } from '@scripts/api/ApiHelper';
 import { IGameInfo, IGameLaunch, IGameListParams } from "./data";
 import { CURRENCY_TYPE, CurrencyType, GAME_TABS, GameTabType } from "@scripts/define/Define";
 
@@ -12,9 +13,8 @@ interface IGameCaches {
 
 class GameStore {
 
-    private _gameListMap: { [key: string]: IGameCaches } = {};
     private _allGameList: IGameCaches;
-    private _currencyNow: CurrencyType;
+    private _recentMap: { [key: string]: IGameInfo[] } = {};
     private _favoriting: boolean = false;
 
     public get favoriting() { return this._favoriting; }
@@ -34,7 +34,6 @@ class GameStore {
     async reqAllGames() {
         const params: IGameListParams = {
             currency: "",
-            favorite: 0
         }
 
         const res = await Http.call<IGameInfo[]>(APP_GAME_LIST, params as any);
@@ -48,31 +47,24 @@ class GameStore {
         return false;
     }
 
-    async reqGameList(currency: CurrencyType, cache = true) {
-        this._currencyNow = currency;
-        const gameCache = this._gameListMap[currency];
-        if (gameCache && cache) {
-            if (Date.now() - gameCache.update_at < UPDATE_LIST_CD) {
-                return true;
-            }
-        }
-
+    async reqRecentGames(currency: CurrencyType) {
         const params: IGameListParams = {
-            currency: currency,
-            favorite: 0
+            currency,
+            recent: 1
         }
 
         const res = await Http.call<IGameInfo[]>(APP_GAME_LIST, params as any);
         if (success(res)) {
-            this._gameListMap[currency] = {
-                list: res.data,
-                update_at: Date.now(),
-            };
+            this._recentMap[currency] = res.data;
             return true;
         }
         return false;
     }
 
+    getRecentGames(currency: CurrencyType) {
+        return this._recentMap[currency];
+    }
+
     async reqFavoAdd(id: number) {
         if (this._favoriting) return;
         this._favoriting = true;
@@ -116,18 +108,35 @@ class GameStore {
             case GAME_TABS.Favs:
                 gameList = allGames.filter(v => v.isFavorite === 1);
                 break;
+            case GAME_TABS.Recent:
+                return this._recentMap[currency] || [];
         }
         return gameList.filter(v => v.currencies.includes(currency));
     }
 
     async reqLaunchGame(gameCode: string, currency: string) {
         const res = await Http.call<IGameLaunch>(APP_GAME_LAUNCH, { gameCode, currency });
-        if (res && res.data && res.data.launchUrl) {
-            return res.data.launchUrl;
+        if (checkResponse(res)) {
+            return res.data?.launchUrl;
         }
 
         return null;
     }
+
+    onUserLogout() {
+        this._recentMap = {};
+    }
+
+    onEnterGame(game: IGameInfo, currency = '') {
+        this.gameID = game.gameID;
+        if (this._recentMap[currency]) {
+            const recents = this._recentMap[currency];
+            const item = recents.find(item => item.gameID === game.gameID);
+            if (!item) recents.push(game);
+
+            this._recentMap[currency] = recents;
+        }
+    }
 }
 
 export const gameStore = new GameStore();

+ 2 - 1
assets/scripts/store/gameStore/data.ts

@@ -2,7 +2,8 @@ export interface IGameListParams {
     tagIds?: string | string[],
     providerIds?: number | number[],
     currency: string,
-    favorite: 0 | 1,
+    favorite?: 0 | 1,
+    recent?: 0 | 1,
 }
 
 export interface IGameTag {

+ 1 - 1
assets/scripts/store/mailStore/MailStore.ts

@@ -4,9 +4,9 @@ import {
     APP_MAIL_LIST,
     APP_MAIL_READ,
     APP_MAIL_TAKE,
-    success,
 } from '../../api';
 import { Http } from '../../request';
+import { success } from '@scripts/api/ApiHelper';
 import { IMailActionData, IMailCheckData, IMailItem, IMailPageData } from './data';
 
 class MailStore {

+ 6 - 13
assets/scripts/store/mainStore/MainStore.ts

@@ -5,12 +5,12 @@ import {
     APP_PROTECT_CHECK,
     APP_PROTECT_GET,
     APP_ROUTES,
-    success,
     APP_SEND_CODE,
     APP_GAME_INFO,
     APP_FORGET_PASS,
     APP_LOGOUT,
 } from "../../api";
+import { success } from '@scripts/api/ApiHelper';
 import { AppKernel } from "../../app/AppKernel";
 import { Login3rdType, APP_LOGIN_TYPE, COUNTRY_CODE, CurrencyType, CURRENCY_TYPE } from "../../define/Define";
 import { LOCAL_CURRENCY_MODE, LOCAL_SIGN } from "../../constants/Localkey";
@@ -28,8 +28,6 @@ import {
 const APP_OTP_INTERVAL = 60;
 
 class MainStore {
-    private _firstLaunch = true;
-
     private _hallServerUrl: string;
     private _isGuest: boolean;
     private _userInfo: IUserData;
@@ -53,8 +51,6 @@ class MainStore {
 
     public get currencyKind() { return this._currencyKind };
 
-    public get firstLaunch() { return this._firstLaunch; }
-
     public get hallServerUrl() { return this._hallServerUrl; }
 
     public get isGuest() { return this._isGuest; }
@@ -140,8 +136,6 @@ class MainStore {
             return false;
         }
 
-        this._firstLaunch = false;
-
         let hallServerUrl = '';
         let guest = 0;
         let upgradeBonus = 0;
@@ -264,9 +258,8 @@ class MainStore {
         const res = await Http.call<IUserData>(APP_REGISTER, params);
         if (success(res)) {
             this.parseLogin(res.data);
-            return true;
         }
-        return false;
+        return res;
     }
 
     async reqLogin(arg) {
@@ -298,9 +291,8 @@ class MainStore {
         const res = await Http.call<IUserData>(APP_LOGIN, params)
         if (success(res)) {
             this.parseLogin(res.data);
-            return true;
         }
-        return false;
+        return res;
     }
 
     async reqLogin3rd(type: Login3rdType, token: string) {
@@ -311,9 +303,8 @@ class MainStore {
         const res = await Http.call<IUserData>(APP_LOGIN_3RD, params);
         if (success(res)) {
             this.parseLogin(res.data);
-            return true;
         }
-        return false;
+        return res;
     }
 
     async reqForgetPass(args: IForgetPassReq) {
@@ -408,6 +399,8 @@ class MainStore {
     }
 
     onUserLogout() {
+        delete this._userInfo;
+        this._isLogin = false;
         this._noticesList = [];
         console.log('mainStore onLogout!!!');
     }

+ 22 - 15
assets/scripts/store/payStore/PayStore.ts

@@ -8,8 +8,8 @@ import {
     APP_WITHDRAW_PAYOUT,
     APP_WITHDRAW_RECORD,
     APP_WITHDRAW_SET_INFO,
-    success,
 } from '@scripts/api';
+import { success } from '@scripts/api/ApiHelper';
 import { ApiEnvelope, Http } from '@scripts/request';
 import {
     IFirstPack,
@@ -29,6 +29,23 @@ import {
     IWithdrawSetInfoRequest,
 } from './data';
 
+function formatGoods(list: IGoodsConfig[], callbackfn?: (item: IGoodsConfig, index: number) => void) {
+    list.forEach((goods, index) => {
+        goods.gears = JSON.parse(goods.gear);
+        goods.cp = Number(goods.cp);
+        goods.cp_reward = Number(goods.cp_reward);
+        goods.cp_reward_display_percent = Number(goods.cp_reward_display_percent);
+        goods.gc = Number(goods.gc);
+        goods.gc_reward = Number(goods.gc_reward);
+        goods.gc_reward_display_percent = Number(goods.gc_reward_display_percent);
+        goods.sc = Number(goods.sc);
+        goods.sc_reward = Number(goods.sc_reward);
+        goods.sc_reward_display_percent = Number(goods.sc_reward_display_percent);
+        callbackfn && callbackfn(goods, index);
+    })
+    return list;
+}
+
 class PayStore {
     private _config: IGoodsConfig[] = [];
     private _payOrderList: IPayOrderItem[] = [];
@@ -51,21 +68,10 @@ class PayStore {
         if (success(res) && res.data) {
             let icon_id = 1;
             const goods_list = Array.isArray(res.data.list) ? res.data.list : [];
-            this._config = [...goods_list, ...goods_list].map(goods => {
-                goods.gears = JSON.parse(goods.gear);
-                goods.cp = Number(goods.cp);
-                goods.cp_reward = Number(goods.cp_reward);
-                goods.cp_reward_display_percent = Number(goods.cp_reward_display_percent);
-                goods.gc = Number(goods.gc);
-                goods.gc_reward = Number(goods.gc_reward);
-                goods.gc_reward_display_percent = Number(goods.gc_reward_display_percent);
-                goods.sc = Number(goods.sc);
-                goods.sc_reward = Number(goods.sc_reward);
-                goods.sc_reward_display_percent = Number(goods.sc_reward_display_percent);
+            this._config = formatGoods([...goods_list, ...goods_list], goods => {
                 goods.icon_id = icon_id;
                 icon_id++;
-                return goods;
-            });
+            })
             return true
         }
         return false;
@@ -101,8 +107,9 @@ class PayStore {
     async reqFirstPackInfo() {
         const res = await Http.call<IFirstPack>(APP_FIRST_PACK);
         if (success(res)) {
-            return res.data;
+            formatGoods(res.data.list);
         }
+        return res;
     }
 
     async reqWithdrawBaseInfo(arg: IWithdrawBaseInfoRequest = {}): Promise<boolean> {

+ 9 - 9
assets/scripts/store/payStore/data.ts

@@ -2,21 +2,21 @@ export interface IGoodsConfig {
     /** 展示标签,如 GC ONLY */
     badge?: string;
     /** 基础 Club Points 奖励 */
-    cp: number | string;
+    cp: number;
     /** 额外 Club Points 奖励 */
-    cp_reward: number | string;
+    cp_reward: number;
     /** Club Points 额外奖励客户端展示百分比 */
-    cp_reward_display_percent: number | string;
+    cp_reward_display_percent: number;
     /** 结束时间(Unix 时间戳,秒) */
     end_time?: number;
     /** 档位类型:0 普通充值,1 首充档位 */
     first_pay: number;
     /** 基础 GC 奖励 */
-    gc: number | string;
+    gc: number;
     /** 额外 GC 奖励 */
-    gc_reward: number | string;
+    gc_reward: number;
     /** GC 额外奖励客户端展示百分比 */
-    gc_reward_display_percent: number | string;
+    gc_reward_display_percent: number;
     gear: string;
     /** 支付方式列表 */
     gears: IGoodsGear[],
@@ -33,11 +33,11 @@ export interface IGoodsConfig {
     /** 0 下架,1 上架,2 售罄(接口仅返回 1 或 2) */
     sale_status: number,
     /** 基础 SC 奖励 */
-    sc: number | string,
+    sc: number,
     /** 额外 SC 奖励 */
-    sc_reward: number | string,
+    sc_reward: number,
     /** SC 额外奖励客户端展示百分比 */
-    sc_reward_display_percent: number | string,
+    sc_reward_display_percent: number,
     /** 服务端当前时间 */
     server_time: number,
     /** 生效时间 */

+ 2 - 0
assets/scripts/utils/Helper.ts

@@ -38,7 +38,9 @@ export function formatGoodsNum(value: number): string {
     if (!Number.isFinite(value)) value = 0;
 
     if (value < 10_000) {
+        let decimal = value % 1 > 0 ? 2 : 0;
         return value.toLocaleString('en-US', {
+            minimumFractionDigits: decimal,
             maximumFractionDigits: 20,
             useGrouping: true,
         });

+ 5 - 1
assets/scripts/utils/VipHelper.ts

@@ -1,3 +1,7 @@
-export function formatVip(vip: number) {
+export function formatVipLv(vip: number) {
     return `LV.${vip}`
+}
+
+export function formatCP(cp: number) {
+    return cp.toString();
 }

+ 3 - 0
package.json

@@ -3,5 +3,8 @@
   "uuid": "ab738cbd-e923-4133-b5ba-decd80d554c4",
   "creator": {
     "version": "3.8.8"
+  },
+  "dependencies": {
+    "lz-string": "^1.5.0"
   }
 }

+ 2 - 2
settings/v2/packages/project.json

@@ -5,8 +5,8 @@
   },
   "general": {
     "designResolution": {
-      "fitWidth": false,
-      "fitHeight": true,
+      "fitWidth": true,
+      "fitHeight": false,
       "height": 750,
       "width": 1624
     }