Ver Fonte

no message

tjt há 5 meses atrás
pai
commit
3c5c25b0a4
100 ficheiros alterados com 7985 adições e 0 exclusões
  1. 2 0
      idiom/.creator/asset-template/typescript/Custom Script Template Help Documentation.url
  2. BIN
      idiom/assets/Circle.003.glb
  3. 181 0
      idiom/assets/Circle.003.glb.meta
  4. 9 0
      idiom/assets/Scripts.meta
  5. 287 0
      idiom/assets/Scripts/Container_Manager.ts
  6. 9 0
      idiom/assets/Scripts/Container_Manager.ts.meta
  7. 72 0
      idiom/assets/Scripts/CreateIdiom.ts
  8. 9 0
      idiom/assets/Scripts/CreateIdiom.ts.meta
  9. 39 0
      idiom/assets/Scripts/Cube_Infor.ts
  10. 9 0
      idiom/assets/Scripts/Cube_Infor.ts.meta
  11. 362 0
      idiom/assets/Scripts/GameCtl.ts
  12. 9 0
      idiom/assets/Scripts/GameCtl.ts.meta
  13. 13 0
      idiom/assets/Scripts/Hall.ts
  14. 9 0
      idiom/assets/Scripts/Hall.ts.meta
  15. 5 0
      idiom/assets/Scripts/ModuleDef.ts
  16. 9 0
      idiom/assets/Scripts/ModuleDef.ts.meta
  17. 6 0
      idiom/assets/Scripts/SceneDef.ts
  18. 9 0
      idiom/assets/Scripts/SceneDef.ts.meta
  19. 9 0
      idiom/assets/ch.meta
  20. 9 0
      idiom/assets/ch/Sign.meta
  21. 9 0
      idiom/assets/ch/audio.meta
  22. 338 0
      idiom/assets/ch/audio/audio.ts
  23. 9 0
      idiom/assets/ch/audio/audio.ts.meta
  24. 9 0
      idiom/assets/ch/ch-sdk.meta
  25. 0 0
      idiom/assets/ch/ch-sdk/ch-sdk.umd.js
  26. 17 0
      idiom/assets/ch/ch-sdk/ch-sdk.umd.js.meta
  27. 284 0
      idiom/assets/ch/ch-sdk/chsdk.d.ts
  28. 9 0
      idiom/assets/ch/ch-sdk/chsdk.d.ts.meta
  29. 45 0
      idiom/assets/ch/ch-sdk/提示说明.txt
  30. 11 0
      idiom/assets/ch/ch-sdk/提示说明.txt.meta
  31. 32 0
      idiom/assets/ch/ch.ts
  32. 9 0
      idiom/assets/ch/ch.ts.meta
  33. 348 0
      idiom/assets/ch/ch_util.ts
  34. 9 0
      idiom/assets/ch/ch_util.ts.meta
  35. 381 0
      idiom/assets/ch/chsdk_inside.d.ts
  36. 9 0
      idiom/assets/ch/chsdk_inside.d.ts.meta
  37. 9 0
      idiom/assets/ch/net.meta
  38. 36 0
      idiom/assets/ch/net/NetBase.ts
  39. 1 0
      idiom/assets/ch/net/NetBase.ts.meta
  40. 135 0
      idiom/assets/ch/net/NetPlayer.ts
  41. 1 0
      idiom/assets/ch/net/NetPlayer.ts.meta
  42. 374 0
      idiom/assets/ch/net/NetRoom.ts
  43. 1 0
      idiom/assets/ch/net/NetRoom.ts.meta
  44. 243 0
      idiom/assets/ch/net/NetTeam.ts
  45. 1 0
      idiom/assets/ch/net/NetTeam.ts.meta
  46. 99 0
      idiom/assets/ch/net/WsClient.ts
  47. 9 0
      idiom/assets/ch/net/WsClient.ts.meta
  48. 9 0
      idiom/assets/ch/net/modules.meta
  49. 4 0
      idiom/assets/ch/net/modules/msgpack.min.d.ts
  50. 9 0
      idiom/assets/ch/net/modules/msgpack.min.d.ts.meta
  51. 0 0
      idiom/assets/ch/net/modules/msgpack.min.js
  52. 17 0
      idiom/assets/ch/net/modules/msgpack.min.js.meta
  53. 582 0
      idiom/assets/ch/net/net.ts
  54. 1 0
      idiom/assets/ch/net/net.ts.meta
  55. 9 0
      idiom/assets/ch/pvp.meta
  56. 241 0
      idiom/assets/ch/pvp/ch_pvp.ts
  57. 1 0
      idiom/assets/ch/pvp/ch_pvp.ts.meta
  58. 147 0
      idiom/assets/ch/sign/sign.ts
  59. 9 0
      idiom/assets/ch/sign/sign.ts.meta
  60. 9 0
      idiom/assets/ch/start.meta
  61. 22 0
      idiom/assets/ch/start/ch_sdk_comp.ts
  62. 9 0
      idiom/assets/ch/start/ch_sdk_comp.ts.meta
  63. 169 0
      idiom/assets/ch/start/ch_start_pack.ts
  64. 1 0
      idiom/assets/ch/start/ch_start_pack.ts.meta
  65. 9 0
      idiom/assets/core.meta
  66. 9 0
      idiom/assets/core/sdk.meta
  67. 9 0
      idiom/assets/core/ui.meta
  68. 279 0
      idiom/assets/core/ui/UICanvas.prefab
  69. 13 0
      idiom/assets/core/ui/UICanvas.prefab.meta
  70. 320 0
      idiom/assets/core/ui/ui.ts
  71. 1 0
      idiom/assets/core/ui/ui.ts.meta
  72. 41 0
      idiom/assets/core/ui/ui_ResolutionAutoFit.ts
  73. 9 0
      idiom/assets/core/ui/ui_ResolutionAutoFit.ts.meta
  74. 358 0
      idiom/assets/core/ui/ui_base.ts
  75. 9 0
      idiom/assets/core/ui/ui_base.ts.meta
  76. 9 0
      idiom/assets/core/util.meta
  77. 209 0
      idiom/assets/core/util/ArrayUtil.ts
  78. 9 0
      idiom/assets/core/util/ArrayUtil.ts.meta
  79. 420 0
      idiom/assets/core/util/DataTimeUtil.ts
  80. 9 0
      idiom/assets/core/util/DataTimeUtil.ts.meta
  81. 172 0
      idiom/assets/core/util/DirectorUtil.ts
  82. 9 0
      idiom/assets/core/util/DirectorUtil.ts.meta
  83. 46 0
      idiom/assets/core/util/Instance.ts
  84. 9 0
      idiom/assets/core/util/Instance.ts.meta
  85. 158 0
      idiom/assets/core/util/LocalStorageUtil.ts
  86. 9 0
      idiom/assets/core/util/LocalStorageUtil.ts.meta
  87. 496 0
      idiom/assets/core/util/MathUtil.ts
  88. 9 0
      idiom/assets/core/util/MathUtil.ts.meta
  89. 40 0
      idiom/assets/core/util/PathUtil.ts
  90. 9 0
      idiom/assets/core/util/PathUtil.ts.meta
  91. 189 0
      idiom/assets/core/util/ProjectileMathUtil.ts
  92. 1 0
      idiom/assets/core/util/ProjectileMathUtil.ts.meta
  93. 190 0
      idiom/assets/core/util/ResUtil.ts
  94. 9 0
      idiom/assets/core/util/ResUtil.ts.meta
  95. 99 0
      idiom/assets/core/util/StringUtil.ts
  96. 9 0
      idiom/assets/core/util/StringUtil.ts.meta
  97. 23 0
      idiom/assets/core/util/TableLoadUtil.ts
  98. 9 0
      idiom/assets/core/util/TableLoadUtil.ts.meta
  99. 53 0
      idiom/assets/core/util/UrlUtil.ts
  100. 9 0
      idiom/assets/core/util/UrlUtil.ts.meta

+ 2 - 0
idiom/.creator/asset-template/typescript/Custom Script Template Help Documentation.url

@@ -0,0 +1,2 @@
+[InternetShortcut]
+URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template

BIN
idiom/assets/Circle.003.glb


+ 181 - 0
idiom/assets/Circle.003.glb.meta

@@ -0,0 +1,181 @@
+{
+  "ver": "2.3.13",
+  "importer": "gltf",
+  "imported": true,
+  "uuid": "1938524c-76dc-428f-9205-03dd62d52eb7",
+  "files": [],
+  "subMetas": {
+    "bcca5": {
+      "importer": "gltf-mesh",
+      "uuid": "1938524c-76dc-428f-9205-03dd62d52eb7@bcca5",
+      "displayName": "",
+      "id": "bcca5",
+      "name": "UnnamedMesh.mesh",
+      "userData": {
+        "gltfIndex": 0,
+        "triangleCount": 600
+      },
+      "ver": "1.1.1",
+      "imported": true,
+      "files": [
+        ".bin",
+        ".json"
+      ],
+      "subMetas": {}
+    },
+    "ea6e2": {
+      "importer": "gltf-material",
+      "uuid": "1938524c-76dc-428f-9205-03dd62d52eb7@ea6e2",
+      "displayName": "",
+      "id": "ea6e2",
+      "name": "DefaultMaterial.material",
+      "userData": {
+        "gltfIndex": 0
+      },
+      "ver": "1.0.14",
+      "imported": true,
+      "files": [
+        ".json"
+      ],
+      "subMetas": {}
+    },
+    "453f2": {
+      "importer": "gltf-scene",
+      "uuid": "1938524c-76dc-428f-9205-03dd62d52eb7@453f2",
+      "displayName": "",
+      "id": "453f2",
+      "name": "Circle.003.prefab",
+      "userData": {
+        "gltfIndex": 0
+      },
+      "ver": "1.0.14",
+      "imported": true,
+      "files": [
+        ".json"
+      ],
+      "subMetas": {}
+    }
+  },
+  "userData": {
+    "imageMetas": [],
+    "lods": {
+      "enable": false,
+      "hasBuiltinLOD": false,
+      "options": [
+        {
+          "screenRatio": 0.25,
+          "faceCount": 1
+        },
+        {
+          "screenRatio": 0.125,
+          "faceCount": 0.25
+        },
+        {
+          "screenRatio": 0.01,
+          "faceCount": 0.1
+        }
+      ]
+    },
+    "assetFinder": {
+      "meshes": [
+        "1938524c-76dc-428f-9205-03dd62d52eb7@bcca5"
+      ],
+      "skeletons": [],
+      "textures": [],
+      "materials": [
+        "1938524c-76dc-428f-9205-03dd62d52eb7@ea6e2"
+      ],
+      "scenes": [
+        "1938524c-76dc-428f-9205-03dd62d52eb7@453f2"
+      ]
+    },
+    "materials": {
+      "1938524c-76dc-428f-9205-03dd62d52eb7@ea6e2": {
+        "__type__": "cc.Material",
+        "_name": "",
+        "_objFlags": 0,
+        "__editorExtras__": {},
+        "_native": "",
+        "_effectAsset": {
+          "__uuid__": "c8f66d17-351a-48da-a12c-0212d28575c4",
+          "__expectedType__": "cc.EffectAsset"
+        },
+        "_techIdx": 0,
+        "_defines": [
+          {
+            "USE_INSTANCING": true
+          },
+          {},
+          {},
+          {},
+          {},
+          {}
+        ],
+        "_states": [
+          {
+            "rasterizerState": {},
+            "depthStencilState": {},
+            "blendState": {
+              "targets": [
+                {}
+              ]
+            }
+          },
+          {
+            "rasterizerState": {},
+            "depthStencilState": {},
+            "blendState": {
+              "targets": [
+                {}
+              ]
+            }
+          },
+          {
+            "rasterizerState": {},
+            "depthStencilState": {},
+            "blendState": {
+              "targets": [
+                {}
+              ]
+            }
+          },
+          {
+            "rasterizerState": {},
+            "depthStencilState": {},
+            "blendState": {
+              "targets": [
+                {}
+              ]
+            }
+          },
+          {
+            "rasterizerState": {},
+            "depthStencilState": {},
+            "blendState": {
+              "targets": [
+                {}
+              ]
+            }
+          },
+          {
+            "rasterizerState": {},
+            "depthStencilState": {},
+            "blendState": {
+              "targets": [
+                {}
+              ]
+            }
+          }
+        ],
+        "_props": [
+          {},
+          {},
+          {},
+          {},
+          {},
+          {}
+        ]
+      }
+    }
+  }
+}

+ 9 - 0
idiom/assets/Scripts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "079ac908-f199-41fc-8cc5-fe3f92e46383",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 287 - 0
idiom/assets/Scripts/Container_Manager.ts

@@ -0,0 +1,287 @@
+import { _decorator, BoxCollider, Component, instantiate, Node, NodePool, Prefab, Quat, random, RigidBody } from 'cc';
+import { TableUtil } from '../module_extra/table_ts/TableUtil';
+import { table_idiom } from '../module_extra/table_ts/table_idiom';
+import TableLoadUtil from '../core/util/TableLoadUtil';
+import { ModuleDef } from './ModuleDef';
+import ch_util from '../ch/ch_util';
+
+import { CreateIdiom } from './CreateIdiom';
+import { Cube_Infor, Cube_State } from './Cube_Infor';
+import { UI_Idioms } from '../module_game/ui/UI_Idioms/UI_Idioms';
+import { gui } from '../core/ui/ui';
+import { GameCtl } from './GameCtl';
+const { ccclass, property } = _decorator;
+
+@ccclass('Container_Manager')
+export class Container_Manager extends Component {
+    static instance: Container_Manager = null;
+
+    @property(Prefab)
+    prefab: Prefab = null;
+
+    config: any = null;//成语库
+    idioms: any[] = [];//生成的成语
+    index: number[] = [];//生成的成语角标,避免生成重复成语
+
+    @property(CreateIdiom)
+    create_node: CreateIdiom = null;
+
+    @property([Node])
+    nodes: Node[] = [];//位置节点 用于成语放入
+
+    node_isIdiom: boolean[] = new Array(10).fill(false);
+
+    idiom_combine: Map<Cube_Infor, number> = new Map();
+
+    Cube_Pool: NodePool = new NodePool();
+    nodeReferences: Node[] = []; // 额外维护的节点引用数组
+
+    private instantiateCube() {
+        for (let i = 0; i < 10; i++) {
+            const newCube = instantiate(this.prefab);
+            newCube.active = true; // 初始时设置为非激活状态
+
+            // 按顺序为节点赋值文字内容
+            let idiomIndex = Math.floor(i / 2);
+            let isPiece1 = i % 2 === 0;
+            newCube.getComponent(Cube_Infor).Text = isPiece1
+                ? this.idioms[idiomIndex].piece_1_word
+                : this.idioms[idiomIndex].piece_2_word;
+
+            console.log("生成第" + i + "个节点:" + newCube.getComponent(Cube_Infor).Text);
+
+            this.Cube_Pool.put(newCube);
+        }
+    }
+
+    public getCube(): Node {
+        const cube = this.Cube_Pool.get();
+        if (cube)
+            cube.active = true;
+        return cube;
+    }
+
+
+    // 将方块回收到对象池
+    public recycleCube(cube: Node): void {
+        cube.active = false; // 将方块设置为非激活状态
+        this.Cube_Pool.put(cube);
+    }
+
+
+    //合成规则导入
+    start() {
+        if (!Container_Manager.instance) {
+            Container_Manager.instance = this;
+        }
+        this.config = table_idiom.getList();
+        if (this.config.length === 95) {
+            console.log("合成规则导入成功");
+        }
+
+        this.level_idioms();
+        
+    }
+
+    update(deltaTime: number) {
+
+    }
+
+    checkIdiom_Combine(matchedcube2: Cube_Infor, outMatchedCubes: Cube_Infor[]): boolean {
+        if (this.idiom_combine.size < 2) {
+            return false; // 至少需要两个方块
+        }
+
+        for (let cube of this.idiom_combine.keys()) {
+            // 遍历 idioms 列表,检查是否匹配成语
+            const matchedIdiom = this.idioms.find(
+                idiom =>
+                    idiom.piece_1_word === cube.Text && idiom.piece_2_word === matchedcube2.Text
+            );
+
+            if (matchedIdiom) {
+                // 匹配成功
+                outMatchedCubes.push(cube, matchedcube2);
+                this.nodeReferences = this.nodeReferences.filter((el) => el !== matchedcube2.node && el !== cube.node);
+                console.log("成功拼成成语: " + matchedIdiom.piece_1_word + matchedIdiom.piece_2_word);
+                return true;
+            }
+
+            // 再检查逆序组合是否匹配
+            const reverseMatchedIdiom = this.idioms.find(
+                idiom =>
+                    idiom.piece_1_word === matchedcube2.Text && idiom.piece_2_word === cube.Text
+            );
+
+            if (reverseMatchedIdiom) {
+                // 匹配成功
+                outMatchedCubes.push(matchedcube2, cube);
+                this.nodeReferences = this.nodeReferences.filter((el) => el !== matchedcube2.node && el !== cube.node);
+                console.log(
+                    "成功拼成成语: " +
+                    reverseMatchedIdiom.piece_1_word +
+                    reverseMatchedIdiom.piece_2_word
+                );
+                return true;
+            }
+        }
+
+        return false; // 没有匹配到成语
+    }
+
+
+
+
+    async level_idioms() {
+        // 筛选满足条件的元素
+        let validConfig = this.config.filter(item =>
+            (item.idiom_type === "一" || item.idiom_type === "二") && // 可以加多个条件
+            (item.piece_1_word === "一" || item.piece_2_word === "二") // 可以加多个筛选条件
+        );
+
+        if (validConfig.length < 5) {
+            console.error("满足条件的成语数量不足 5 个");
+        } else {
+            // 从筛选结果中随机选择
+            for (let i = 0; i < 5;) {
+                let rand = ch_util.getRandomInt(0, validConfig.length - 1);
+                // 使用 indexOf 检查是否已存在
+                if (this.index.indexOf(rand) === -1) {
+                    this.index[i] = rand;
+                    this.idioms[i] = validConfig[rand];
+                    i++;
+                }
+            }
+        }
+        
+        await gui.show(UI_Idioms);
+        await this.instantiateCube();
+        await this.create_node.nodeMoving();
+    }
+
+    //消除一组
+    eliminate() {
+        //先判断槽内是否有方块,如果有,匹配槽内最前面一个与散落方块中的
+        let cube;
+        for (const [key, value] of this.idiom_combine) {
+            if (value === 0) {
+                cube = key;
+                break;
+            }
+        }
+
+        //槽中有方块
+        if (cube) {
+            const originalReferences = [...this.nodeReferences];
+            for (const element of originalReferences) {
+                if (this.idioms.find(c => c.idiom == element.getComponent(Cube_Infor).Text + cube.Text) && element.getComponent(Cube_Infor) !== cube) {
+                    console.log(element.getComponent(Cube_Infor).Text);
+                    element.getComponent(Cube_Infor).state = Cube_State.wait;
+                    element.getComponent(Cube_Infor).rigidbody.type = RigidBody.Type.STATIC;
+                    element.getComponent(BoxCollider).enabled = false;
+                    let targetRotation = new Quat();
+                    Quat.fromEuler(targetRotation, -90, 0, 0);
+                    element.rotation = targetRotation;
+                    // 使用filter过滤掉当前元素
+                    GameCtl.instance.combine_ani(element.getComponent(Cube_Infor), cube);
+                    this.nodeReferences = this.nodeReferences.filter((el) => el !== element && el !== cube.node);
+                    break;
+                }
+                else if (this.idioms.find(c => c.idiom == cube.Text + element.getComponent(Cube_Infor).Text) && element.getComponent(Cube_Infor) !== cube) {
+                    console.log(element.getComponent(Cube_Infor).Text);
+                    element.getComponent(Cube_Infor).state = Cube_State.wait;
+                    element.getComponent(Cube_Infor).rigidbody.type = RigidBody.Type.STATIC;
+                    element.getComponent(BoxCollider).enabled = false;
+                    let targetRotation = new Quat();
+                    Quat.fromEuler(targetRotation, -90, 0, 0);
+                    element.rotation = targetRotation;
+                    GameCtl.instance.combine_ani(cube, element.getComponent(Cube_Infor));
+                    this.nodeReferences = this.nodeReferences.filter((el) => el !== element && el !== cube.node);
+                    break;
+                }
+            }
+        }
+
+        //槽中没有方块
+        else{
+            let flag:boolean=false;
+            const originalReferences1 = [...this.nodeReferences];
+            const originalReferences2 = [...this.nodeReferences];
+            for (const element1 of originalReferences1)
+            {
+                for (const element2 of originalReferences2) {
+                    if (this.idioms.find(c => c.idiom == element1.getComponent(Cube_Infor).Text + element2.getComponent(Cube_Infor).Text)) {
+                        element1.getComponent(Cube_Infor).state = Cube_State.wait;
+                        element1.getComponent(Cube_Infor).rigidbody.type = RigidBody.Type.STATIC;
+                        let targetRotation = new Quat();
+                        Quat.fromEuler(targetRotation, -90, 0, 0);
+                        element1.rotation = targetRotation;
+
+                        element2.getComponent(Cube_Infor).state = Cube_State.wait;
+                        element2.getComponent(Cube_Infor).rigidbody.type = RigidBody.Type.STATIC;
+                        element2.getComponent(BoxCollider).enabled = false;
+                        element2.rotation = targetRotation;
+                        // 使用filter过滤掉当前元素
+                        GameCtl.instance.combine_ani(element1.getComponent(Cube_Infor), element2.getComponent(Cube_Infor));
+                        this.nodeReferences = this.nodeReferences.filter((el) => el !== element1 && el !== element2);
+                        flag=true;
+                        break;
+                    }
+                    if (this.idioms.find(c => c.idiom == element2.getComponent(Cube_Infor).Text + element1.getComponent(Cube_Infor).Text)) {
+                        element1.getComponent(Cube_Infor).state = Cube_State.wait;
+                        element1.getComponent(Cube_Infor).rigidbody.type = RigidBody.Type.STATIC;
+                        let targetRotation = new Quat();
+                        Quat.fromEuler(targetRotation, -90, 0, 0);
+                        element1.rotation = targetRotation;
+
+                        element2.getComponent(Cube_Infor).state = Cube_State.wait;
+                        element2.getComponent(Cube_Infor).rigidbody.type = RigidBody.Type.STATIC;
+                        element2.getComponent(BoxCollider).enabled = false;
+                        element2.rotation = targetRotation;
+                        // 使用filter过滤掉当前元素
+                        GameCtl.instance.combine_ani(element2.getComponent(Cube_Infor), element1.getComponent(Cube_Infor));
+                        this.nodeReferences = this.nodeReferences.filter((el) => el !== element1 && el !== element2);
+                        flag=true;
+                        break;
+                    }
+                }
+                if(flag===true)
+                {
+                    break;
+                }
+            }
+
+        }
+    }
+
+
+    async shuffle() {
+        // 回收所有非槽内的活跃节点
+        this.nodeReferences.forEach(node => {
+            const cubeInfo = node.getComponent(Cube_Infor);
+            if (cubeInfo.state === Cube_State.live) {
+                this.recycleCube(node); // 回收到池中
+            }
+        });
+
+        // 等待节点移动完成后执行后续逻辑
+        await this.create_node.nodeMoving();
+    }
+
+
+
+    //清空槽子
+    Empty() {
+        for (let idiom of this.idiom_combine.keys()) {
+            idiom.rigidbody.type = RigidBody.Type.DYNAMIC;
+            idiom.state = Cube_State.live;
+            idiom.node.position = this.create_node.node.position;
+        }
+        this.idiom_combine.clear();
+        this.node_isIdiom.fill(false);
+
+        gui.get(UI_Idioms).all_light_Hide();
+    }
+}
+
+

+ 9 - 0
idiom/assets/Scripts/Container_Manager.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "e84def08-5db6-438d-96d8-95b4297afb88",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 72 - 0
idiom/assets/Scripts/CreateIdiom.ts

@@ -0,0 +1,72 @@
+import { _decorator, Component, debug, director, instantiate, Layers, Node, NodePool, Prefab, tween, Vec3 } from 'cc';
+import { Cube_Infor, Cube_State } from './Cube_Infor';
+import { Container } from '../core/util_class/Container';
+import { Container_Manager } from './Container_Manager';
+import { UI_Main } from '../module_game/ui/UI_Main/UI_Main';
+import { gui } from '../core/ui/ui';
+import { UI_Idioms } from '../module_game/ui/UI_Idioms/UI_Idioms';
+const { ccclass, property } = _decorator;
+
+@ccclass('CreateIdiom')
+export class CreateIdiom extends Component {
+    count: number = 0;
+
+    onLoad() {
+        gui.show(UI_Main);
+    }
+
+    update(deltaTime: number) {
+
+    }
+
+    nodeMoving() {
+        this.node.setPosition(new Vec3(0,1.6,0));
+
+        let startPos = this.node.position; // 起点,抛物线开始的坐标
+        let middlePos = new Vec3(this.node.position.x, this.node.position.y + 2, 0); // 中间控制点
+        let destPos = new Vec3(this.node.position.x, this.node.position.y + 4, 0); // 终点,抛物线上升顶点
+
+        let twoBezier = (t: number, p1: Vec3, cp: Vec3, p2: Vec3) => {
+            // 贝塞尔曲线计算
+            let x = (1 - t) * (1 - t) * p1.x + 2 * t * (1 - t) * cp.x + t * t * p2.x;
+            let y = (1 - t) * (1 - t) * p1.y + 2 * t * (1 - t) * cp.y + t * t * p2.y;
+
+            // 螺旋运动计算
+            let angle = t * Math.PI * 6; // 螺旋角度变化,增加圈数
+            let radius = 0.5 + t * 1.5; // 螺旋半径逐渐增大
+            let xOffset = Math.cos(angle) * radius; // x 轴偏移
+            let zOffset = Math.sin(angle) * radius; // z 轴偏移
+
+            // 返回最终点,带有螺旋效果
+            return new Vec3(x + xOffset, y, zOffset);
+        };
+
+
+        let tweenDuration: number = 3.0; // 动画时长
+        let createdNodes = 0; // 已生成的节点计数
+
+        tween(this.node.position)
+            .to(tweenDuration, destPos, {
+                onUpdate: (target: Vec3, ratio: number) => {
+                    // 计算新的位置,带螺旋上升效果
+                    this.node.position = twoBezier(ratio, startPos, middlePos, destPos);
+
+                    // 根据进度生成新节点
+                    let cube = Container_Manager.instance.getCube();
+                    if (cube != null&&cube.getComponent(Cube_Infor).state === Cube_State.live) {
+                        let newNode = cube;
+                        newNode.parent = director.getScene();
+                        newNode.setPosition(this.node.position);
+                        Container_Manager.instance.nodeReferences.push(newNode);
+                        console.log(cube.getComponent(Cube_Infor).Text);
+                    }
+                }
+            }).start();
+    }
+
+
+
+
+}
+
+

+ 9 - 0
idiom/assets/Scripts/CreateIdiom.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "3b901a14-e592-479e-9f13-b498988d9599",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 39 - 0
idiom/assets/Scripts/Cube_Infor.ts

@@ -0,0 +1,39 @@
+import { _decorator, Component, Label, Node, RigidBody } from 'cc';
+const { ccclass, property } = _decorator;
+
+export enum Cube_State{
+    live,
+    wait,
+    dead
+}
+
+@ccclass('Cube_Infor')
+export class Cube_Infor extends Component {
+    @property(Label)
+    txt:Label;
+    @property(RigidBody)
+    rigidbody:RigidBody;
+
+    text:string;
+
+    // lock:boolean = false;
+
+    state:Cube_State = Cube_State.live;
+    start() {
+
+    }
+
+    update(deltaTime: number) {
+        
+    }
+
+    set Text(value:string){
+        this.txt.string = value;
+    }
+
+    get Text(){
+        return this.txt.string;
+    }
+}
+
+

+ 9 - 0
idiom/assets/Scripts/Cube_Infor.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "0fe67764-56ea-4565-8511-dee46ed6c032",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 362 - 0
idiom/assets/Scripts/GameCtl.ts

@@ -0,0 +1,362 @@
+import { _decorator, BoxCollider, Camera, Component, director, EventTouch, find, Game, geometry, Layers, Node, PhysicsSystem, Quat, RigidBody, Size, tween, UITransform, v3, Vec3 } from 'cc';
+import { Cube_Infor, Cube_State } from './Cube_Infor';
+import { Container_Manager } from './Container_Manager';
+import { UI_Idioms } from '../module_game/ui/UI_Idioms/UI_Idioms';
+import { gui } from '../core/ui/ui';
+
+const { ccclass, property } = _decorator;
+
+@ccclass('GameCtl')
+export class GameCtl extends Component {
+    static instance: GameCtl = null;
+    @property(Camera)
+    camera: Camera = null;
+
+    @property([Node])
+    Ani: Node[] = [];
+
+    canTouch: boolean = true;
+    onLoad() {
+        GameCtl.instance = this;
+        console.log(find('Ani'));
+
+        console.log('Camera value in onLoad:', this.camera);
+        this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
+        this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
+        this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
+
+    }
+
+    update(deltaTime: number) {
+
+    }
+
+    onTouchStart(event: EventTouch) {
+
+    }
+
+
+    onTouchMove(event: EventTouch) {
+
+    }
+
+    onTouchEnd(event: EventTouch) {
+        if (this.canTouch) {
+            this.shootRay(event);
+        }
+    }
+
+    //发射射线检测判断物体是否可消除
+    shootRay(event: EventTouch) {
+        if (!this.camera) return;
+
+        let ray = new geometry.Ray();
+        this.camera.screenPointToRay(event.getLocationX(), event.getLocationY(), ray);
+
+        const index = Layers.nameToLayer('Cube');
+
+        const cubeMask = 2 << index;
+        const maxDistance = 100000;
+        const queryTrigger = false;
+
+        if (PhysicsSystem.instance.raycastClosest(ray, cubeMask, maxDistance, queryTrigger)) {
+            const raycastClosestResult = PhysicsSystem.instance.raycastClosestResult;
+            const hitPoint = raycastClosestResult.hitPoint;
+            const hitNormal = raycastClosestResult.hitNormal;
+            const collider = raycastClosestResult.collider;
+            const distance = raycastClosestResult.distance;
+            console.log(collider.node.name);
+
+            //当前其余点击无效
+            if (collider.node.getComponent(Cube_Infor).state === Cube_State.live) {
+                this.entryContainer(collider.node);
+            }
+
+        }
+        console.log('发射了射线');
+    }
+
+
+    entryContainer(node: Node) {
+        // 判断容器剩余容量
+
+        let startIndex = -1;
+        let targetPos = new Vec3();
+
+        let txt_length = node.getComponent(Cube_Infor).Text.length;
+        console.log(txt_length);
+
+        // 判断字长 并 判断是否还有空间可放置
+        switch (txt_length) {
+            case 1:
+                for (let i = 0; i < Container_Manager.instance.node_isIdiom.length; i++) {
+                    if (Container_Manager.instance.node_isIdiom[i] === false) {
+                        startIndex = i;  // 找到连续的三个 false,记录起始位置
+                        break;  // 找到第一个符合条件的位置后停止
+                    }
+                }
+                if (startIndex !== -1) {  // 如果找到了一个值为 false 的元素
+                    targetPos = Container_Manager.instance.nodes[startIndex].getWorldPosition().clone();
+                    Container_Manager.instance.node_isIdiom[startIndex] = true;
+                    console.log(targetPos);
+                } else {
+                    console.log("没有空间了");
+                }
+                break;
+            case 2: {
+                for (let i = 0; i < Container_Manager.instance.node_isIdiom.length - 2; i++) {
+                    if (Container_Manager.instance.node_isIdiom[i] === false &&
+                        Container_Manager.instance.node_isIdiom[i + 1] === false) {
+                        startIndex = i;  // 找到连续的两个 false,记录起始位置
+                        break;  // 找到第一个符合条件的位置后停止
+                    }
+                }
+
+                if (startIndex !== -1) {
+                    console.log("找到连续的两个 false,起始索引是:", startIndex);
+                    // 可以在此使用 startIndex 进行后续操作,例如:
+                    let pos1 = Container_Manager.instance.nodes[startIndex].getWorldPosition();
+                    let pos2 = Container_Manager.instance.nodes[startIndex + 1].getWorldPosition();
+
+                    Container_Manager.instance.node_isIdiom[startIndex] = true;
+                    Container_Manager.instance.node_isIdiom[startIndex + 1] = true;
+                    targetPos.set(
+                        (pos1.x + pos2.x) / 2,
+                        (pos1.y + pos2.y) / 2,
+                        (pos1.z + pos2.z) / 2
+                    );
+                    console.log(targetPos);
+                }
+
+                break;
+            }
+            case 3: {
+
+                for (let i = 0; i < Container_Manager.instance.node_isIdiom.length - 2; i++) {
+                    if (Container_Manager.instance.node_isIdiom[i] === false &&
+                        Container_Manager.instance.node_isIdiom[i + 1] === false &&
+                        Container_Manager.instance.node_isIdiom[i + 2] === false) {
+                        startIndex = i;  // 找到连续的三个 false,记录起始位置
+                        break;  // 找到第一个符合条件的位置后停止
+                    }
+                }
+
+                if (startIndex !== -1) {
+                    // 找到连续三个 false,startIndex 即为最前面的索引
+                    console.log("找到连续的三个 false,起始索引是:", startIndex);
+                    // 可以在此使用 startIndex 进行后续操作,例如:
+                    let pos1 = Container_Manager.instance.nodes[startIndex].getWorldPosition();
+                    let pos2 = Container_Manager.instance.nodes[startIndex + 1].getWorldPosition();
+                    let pos3 = Container_Manager.instance.nodes[startIndex + 2].getWorldPosition();
+
+                    Container_Manager.instance.node_isIdiom[startIndex] = true;
+                    Container_Manager.instance.node_isIdiom[startIndex + 1] = true;
+                    Container_Manager.instance.node_isIdiom[startIndex + 2] = true;
+                    targetPos.set(
+                        (pos1.x + pos2.x + pos3.x) / 3,
+                        (pos1.y + pos2.y + pos3.y) / 3,
+                        (pos1.z + pos2.z + pos3.z) / 3
+                    );
+                    console.log(targetPos);
+                } else {
+                    console.log("没有找到连续的三个 false");
+                }
+                break;
+            }
+            default:
+                return;
+        }
+
+        if (startIndex !== -1) {
+            // node.getComponent(Cube_Infor).lock = true;
+            node.getComponent(Cube_Infor).state = Cube_State.wait;
+            node.getComponent(Cube_Infor).rigidbody.type = RigidBody.Type.STATIC; // 禁用重力
+            let targetRotation = new Quat();
+            Quat.fromEuler(targetRotation, -90, 0, 0);
+            this.canTouch = false;
+            tween(node)
+                .to(0.5, { position: new Vec3(targetPos.x, targetPos.y, targetPos.z + 0.4), rotation: targetRotation })
+                .call(() => {
+                    Container_Manager.instance.idiom_combine.set(node.getComponent(Cube_Infor), startIndex);
+                    // 执行判断成语合成逻辑
+                    let matchedCubes: Cube_Infor[] = [];
+                    let flag = Container_Manager.instance.checkIdiom_Combine(node.getComponent(Cube_Infor), matchedCubes);
+                    if (flag) {
+                        // 执行合成动画
+                        console.log("匹配的成语方块:", matchedCubes);
+                        this.combine_ani(matchedCubes[0], matchedCubes[1]);
+                    } else {
+                        //高亮
+                        gui.get(UI_Idioms).light_Show(node.getComponent(Cube_Infor));
+                        this.canTouch = true;
+                    }
+                })
+                .start();
+        }
+    }
+
+    //合成动画
+    combine_ani(cube1: Cube_Infor, cube2: Cube_Infor) {
+        // 创建第一个 tween 动画
+        const tween1 = tween(cube1.node)
+            .to(0.5, { position: new Vec3(this.Ani[0].position.x, this.Ani[0].position.y, this.Ani[0].position.z + 0.4) })
+            .call(() => {
+                for (let i = Container_Manager.instance.idiom_combine.get(cube1); i < Container_Manager.instance.idiom_combine.get(cube1) + cube1.Text.length; i++) {
+                    Container_Manager.instance.node_isIdiom[i] = false;
+                }
+                Container_Manager.instance.idiom_combine.delete(cube1);
+                setTimeout(() => {
+                    cube1.state=Cube_State.dead;
+                    Container_Manager.instance.recycleCube(cube1.node);
+                }, 1000.0);
+            });
+
+        // 创建第二个 tween 动画
+        const tween2 = tween(cube2.node)
+            .to(0.5, { position: new Vec3(this.Ani[1].position.x, this.Ani[1].position.y, this.Ani[1].position.z + 0.4) })
+            .call(() => {
+                for (let i = Container_Manager.instance.idiom_combine.get(cube2); i < Container_Manager.instance.idiom_combine.get(cube2) + cube2.Text.length; i++) {
+                    Container_Manager.instance.node_isIdiom[i] = false;
+                }
+                Container_Manager.instance.idiom_combine.delete(cube2);
+                setTimeout(() => {
+                    cube2.state=Cube_State.dead;
+                    //在nodeReferences中修改该节点属性
+                    // Container_Manager.instance.nodeReferences.forEach((node, index) => {
+                    //     if (node === cube2.node) {
+                    //         Container_Manager.instance.nodeReferences[index].getComponent(Cube_Infor).state = Cube_State.dead;
+                    //     }
+                    // });
+                    Container_Manager.instance.recycleCube(cube2.node);
+                }, 1000.0);
+            });
+
+        // 使用 tween 的并行组合功能
+        tween(cube1.node)
+            .parallel(tween1, tween2) // 并行执行 tween1 和 tween2
+            .call(() => {
+                // 取消相关字的高亮显示
+                gui.get(UI_Idioms).light_Hide(cube1, cube2);
+                this.adjustContainer();
+
+                // 检测容器中哪些字需要高亮显示
+                console.log(Container_Manager.instance.node_isIdiom); // 在所有动画结束后执行
+                Container_Manager.instance.idioms = Container_Manager.instance.idioms.filter(c => c.idiom !== cube1.Text + cube2.Text);
+                this.canTouch = true;
+            })
+            .start();
+    }
+
+
+    adjustContainer() {
+        const container = Container_Manager.instance;
+
+        // 新的 node_isIdiom 状态数组
+        const newNodeIsIdiom = Array(container.node_isIdiom.length).fill(false);
+
+        // 新的 idiom_combine 映射表
+        const newIdiomCombine = new Map<Cube_Infor, number>();
+        // 遍历 idiom_combine,重新计算位置
+        for (const [cube, startIndex] of container.idiom_combine.entries()) {
+            const txtLength = cube.Text.length; // 获取字长
+            let targetIndex = -1; // 目标位置起始索引
+            let targetPos = new Vec3();
+
+            switch (txtLength) {
+                case 1: {
+                    // 找到一个空位
+                    for (let i = 0; i < container.node_isIdiom.length; i++) {
+                        if (!newNodeIsIdiom[i]) {
+                            targetIndex = i;
+                            break;
+                        }
+                    }
+                    if (targetIndex !== -1) {
+                        targetPos = container.nodes[targetIndex].getWorldPosition().clone();
+                        newNodeIsIdiom[targetIndex] = true; // 占用位置
+                    } else {
+                        console.log("没有空位容纳单字 Cube_Infor");
+                    }
+                    break;
+                }
+
+                case 2: {
+                    // 找到连续两个空位
+                    for (let i = 0; i < container.node_isIdiom.length - 1; i++) {
+                        if (!newNodeIsIdiom[i] && !newNodeIsIdiom[i + 1]) {
+                            targetIndex = i;
+                            break;
+                        }
+                    }
+
+                    if (targetIndex !== -1) {
+                        const pos1 = container.nodes[targetIndex].getWorldPosition();
+                        const pos2 = container.nodes[targetIndex + 1].getWorldPosition();
+                        targetPos.set(
+                            (pos1.x + pos2.x) / 2,
+                            (pos1.y + pos2.y) / 2,
+                            (pos1.z + pos2.z) / 2
+                        );
+                        newNodeIsIdiom[targetIndex] = true;
+                        newNodeIsIdiom[targetIndex + 1] = true;
+                    } else {
+                        console.log("没有连续两个空位容纳双字 Cube_Infor");
+                    }
+                    break;
+                }
+
+                case 3: {
+                    // 找到连续三个空位
+                    for (let i = 0; i < container.node_isIdiom.length - 2; i++) {
+                        if (
+                            !newNodeIsIdiom[i] &&
+                            !newNodeIsIdiom[i + 1] &&
+                            !newNodeIsIdiom[i + 2]
+                        ) {
+                            targetIndex = i;
+                            break;
+                        }
+                    }
+
+                    if (targetIndex !== -1) {
+                        const pos1 = container.nodes[targetIndex].getWorldPosition();
+                        const pos2 = container.nodes[targetIndex + 1].getWorldPosition();
+                        const pos3 = container.nodes[targetIndex + 2].getWorldPosition();
+                        targetPos.set(
+                            (pos1.x + pos2.x + pos3.x) / 3,
+                            (pos1.y + pos2.y + pos3.y) / 3,
+                            (pos1.z + pos2.z + pos3.z) / 3
+                        );
+                        newNodeIsIdiom[targetIndex] = true;
+                        newNodeIsIdiom[targetIndex + 1] = true;
+                        newNodeIsIdiom[targetIndex + 2] = true;
+                    } else {
+                        console.log("没有连续三个空位容纳三字 Cube_Infor");
+                    }
+                    break;
+                }
+
+                default:
+                    return;
+            }
+
+            // 移动 Cube_Infor 到目标位置
+            if (targetIndex !== -1) {
+                tween(cube.node)
+                    .to(0.3, { position: new Vec3(targetPos.x, targetPos.y, targetPos.z + 0.4) })
+                    .start();
+
+                newIdiomCombine.set(cube, targetIndex); // 更新新映射
+            }
+        }
+
+        // 更新 container 状态
+        container.node_isIdiom = newNodeIsIdiom;
+        container.idiom_combine = newIdiomCombine;
+
+        console.log("调整后的容器状态:", container.node_isIdiom);
+        console.log("更新后的 idiom_combine:", [...container.idiom_combine.entries()]);
+    }
+}
+
+

+ 9 - 0
idiom/assets/Scripts/GameCtl.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "5237ffb8-fdfb-4894-a5be-ac2d3ada5f02",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 13 - 0
idiom/assets/Scripts/Hall.ts

@@ -0,0 +1,13 @@
+import { _decorator, Component, Node } from 'cc';
+import { UI_Hall } from '../module_game/ui/UI_Hall/UI_Hall';
+import { gui } from '../core/ui/ui';
+const { ccclass, property } = _decorator;
+
+@ccclass('Hall')
+export class Hall extends Component {
+    start() {
+        gui.show(UI_Hall);
+    }
+}
+
+

+ 9 - 0
idiom/assets/Scripts/Hall.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "7cb62609-5dee-4b3c-a9ca-17484c97e45c",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 5 - 0
idiom/assets/Scripts/ModuleDef.ts

@@ -0,0 +1,5 @@
+export class ModuleDef{
+    public static BASIC='module_basic';
+    public static EXTRA='module_extra';
+    public static GAME='module_game';
+}

+ 9 - 0
idiom/assets/Scripts/ModuleDef.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "755bda5f-ea54-487c-b1e9-98196d3757e7",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 6 - 0
idiom/assets/Scripts/SceneDef.ts

@@ -0,0 +1,6 @@
+export class SceneDef {
+    public static START='start';
+    public static GAME='game';
+    public static Hall='hall';
+}
+

+ 9 - 0
idiom/assets/Scripts/SceneDef.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "01aa741e-3f0a-4c17-83c4-f95b02f10561",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/ch.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "4f161d9f-2073-43bc-a9a2-3ce9f4a1d385",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/ch/Sign.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "b8872c79-0efd-4c7e-a793-8bbad5aca643",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/ch/audio.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "ef867cd6-6777-4779-ab30-581043020c10",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 338 - 0
idiom/assets/ch/audio/audio.ts

@@ -0,0 +1,338 @@
+import { assetManager, AudioClip, AudioSource, director, Node } from "cc";
+const ch_log = chsdk.log;
+const ch_storage = chsdk.storage;
+/**音频等资源加载方式*/
+export enum loadType {
+    /**未知*/
+    none = 0,
+    /**bundle*/
+    bundle = 1,
+    /**远程*/
+    remote = 2,
+}
+/**音频播放控制
+ * 需要初始化资源加载方式
+ * loadType.bundle 时需要设置 bundle名
+ * loadType.remote 测试使用远程的音频需要设置远程地址
+*/
+export default class ch_audio {
+    private static _instance: ch_audio;
+    public static getInstance(): ch_audio {
+        if (!this._instance) this._instance = new ch_audio();
+        return this._instance;
+    }
+    private _volume_music: number = 1;
+    private _volume_effect: number = 1;
+    private _switch_music: boolean = true;
+    private _switch_effect: boolean = true;
+    private readonly _effect_max: number = 5;
+    private _effect_index: number = 0;
+    private _effect_source_pool: AudioSource[] = [];
+    private _music_source: AudioSource;
+    private _load_type: loadType = loadType.none;
+    private _bundle_name: string;
+    private _remote_url: string;
+    private _playing_sound: Set<string> = new Set();
+    constructor() {
+        const audio = new Node();
+        audio.name = '__ch_audio__';
+        director.getScene().addChild(audio);
+        director.addPersistRootNode(audio);
+        this._music_source = this._create(audio);
+        for (let i = 0; i < this._effect_max; i++) {
+            this._effect_source_pool.push(this._create(audio));
+        }
+        this.load();
+    }
+    /**
+     * 创建音频源
+     * @param node 节点
+     * @param volume 音量
+     * @returns AudioSource 音频源组件
+     */
+    private _create(node: Node): AudioSource {
+        const source = node.addComponent(AudioSource);
+        source.loop = false;
+        source.playOnAwake = false;
+        source.volume = 0.5;
+        return source;
+    }
+    /**初始化*/
+    init(load_type: loadType, bundle_name: string, remote_url: string): void {
+        this._load_type = load_type;
+        this._bundle_name = bundle_name;
+        this._remote_url = remote_url;
+    }
+    /**切换bundle*/
+    set_bundle_name(bundle_name: string): void {
+        this._bundle_name = bundle_name;
+    }
+    /**
+     * 释放通过 [[load]] 或者 [[loadDir]] 加载的声音资源。
+     * @param sound 声音资源路径
+     */
+    release(sound?: string): void {
+        if (this._load_type == loadType.none) {
+            ch_log.warn('音频模块未初始化');
+        } else if (this._load_type == loadType.bundle) {
+            const bundle = assetManager.getBundle(this._bundle_name);
+            if (!sound) {
+                bundle.releaseAll();
+            } else {
+                bundle.release(sound, AudioClip);
+            }
+        }
+    }
+    /** 保存音乐音效的音量、开关配置数据到本地 */
+    save() {
+        const local_data: any = {};
+        local_data.volume_music = this._volume_music;
+        local_data.volume_effect = this._volume_effect;
+        local_data.switch_music = this._switch_music;
+        local_data.switch_effect = this._switch_effect;
+        ch_storage.set("ch_audio", local_data);
+    }
+    /** 本地加载音乐音效的音量、开关配置数据并设置到游戏中 */
+    load() {
+        const local_data = ch_storage.getObject("ch_audio");
+        if (local_data) {
+            try {
+                this.setState(local_data);
+            }
+            catch (e) {
+                this.setStateDefault();
+            }
+        }
+        else {
+            this.setStateDefault();
+        }
+    }
+    private setState(local_data: { volume_music: number, volume_effect: number, switch_music: boolean, switch_effect: boolean }) {
+        this.volumeMusic = local_data.volume_music;
+        this.volumeEffect = local_data.volume_effect;
+        this.switchMusic = local_data.switch_music;
+        this.switchEffect = local_data.switch_effect;
+    }
+    private setStateDefault() {
+        this.volumeMusic = 0.8;
+        this.volumeEffect = 0.8;
+        this.switchMusic = true;
+        this.switchEffect = true;
+    }
+    /**
+     * 获取背景音乐音量
+     */
+    get volumeMusic(): number {
+        return this._volume_music;
+    }
+    /** 
+     * 设置背景音乐音量
+     * @param value     音乐音量值
+     */
+    set volumeMusic(value: number) {
+        this._volume_music = value;
+        this._music_source.volume = value;
+    }
+    /** 
+     * 获取背景音乐开关值 
+     */
+    get switchMusic(): boolean {
+        return this._switch_music;
+    }
+    /** 
+     * 设置背景音乐开关值
+     * @param value     开关值
+     */
+    set switchMusic(value: boolean) {
+        this._switch_music = value;
+        if (value == false) this._music_source.stop();
+    }
+    /** 
+     * 获取音效音量 
+     */
+    get volumeEffect(): number {
+        return this._volume_effect;
+    }
+    /**
+     * 设置获取音效音量
+     * @param value     音效音量值
+     */
+    set volumeEffect(value: number) {
+        this._volume_effect = value;
+        for (let i = 0; i < this._effect_source_pool.length; i++) {
+            this._effect_source_pool[i].volume = this._volume_effect;
+        }
+    }
+
+    /** 
+     * 获取音效开关值 
+     */
+    get switchEffect(): boolean {
+        return this._switch_effect;
+    }
+    /**
+     * 设置音效开关值
+     * @param value     音效开关值
+     */
+    set switchEffect(value: boolean) {
+        this._switch_effect = value;
+        if (value == false) {
+            for (let i = 0; i < this._effect_source_pool.length; i++) {
+                this._effect_source_pool[i].stop();
+            }
+        }
+    }
+    /**
+     * @en
+     * play short audio, such as strikes,explosions
+     * @zh
+     * 播放短音频,比如 打击音效,爆炸音效等
+     * @param sound clip or url for the audio
+     * @param interval 同名字音频限制播放间隔(毫秒)   (默认:0不限制  特殊系数:>0 <=1 使用音频时间X此系数)
+     */
+    playOneShot(sound: AudioClip | string, interval: number = 0, remote_ext: string = '.mp3') {
+        if (!this._switch_effect) return;
+        if (sound instanceof AudioClip) {
+            this.doPlayOneShot(sound, interval);
+        }
+        else {
+            if (this._load_type == loadType.none) {
+                ch_log.warn('音频模块未初始化');
+            } else if (this._load_type == loadType.bundle) {
+                const bundle = assetManager.getBundle(this._bundle_name);
+                if (!bundle) {
+                    ch_log.warn(`请确保 bundle${this._bundle_name} 已加载`);
+                } else {
+                    bundle.load(sound, (err, clip: AudioClip) => {
+                        if (err) {
+                            ch_log.error(err);
+                        }
+                        else {
+                            this.doPlayOneShot(clip, interval);
+                        }
+                    });
+                }
+            } else if (this._load_type == loadType.remote) {
+                assetManager.loadRemote(this._remote_url + sound + remote_ext, (err: Error | null, clip: AudioClip) => {
+                    if (err) {
+                        ch_log.error(err);
+                    }
+                    else {
+                        this.doPlayOneShot(clip, interval);
+                    }
+                });
+            }
+        }
+    }
+    private doPlayOneShot(clip: AudioClip, interval: number): void {
+        const name: string = clip.name;
+        if (interval > 0) {
+            if (this._playing_sound.has(name)) return;
+            this._playing_sound.add(name);
+            const time = interval <= 1 ? clip.getDuration() * interval * 1000 : interval;
+            setTimeout(() => { this._playing_sound.delete(name); }, time);
+            this.getNextEffectSource().playOneShot(clip, this._volume_effect);
+        } else {
+            this.getNextEffectSource().playOneShot(clip, this._volume_effect);
+        }
+    }
+    private getNextEffectSource(): AudioSource {
+        const source = this._effect_source_pool[this._effect_index];
+        this._effect_index = (this._effect_index + 1) % this._effect_max;
+        return source;
+    }
+    /**
+     * @en
+     * play long audio, such as the bg music
+     * @zh
+     * 播放长音频,比如 背景音乐
+     * @param sound clip or url for the sound
+     */
+    play(sound: AudioClip | string, remote_ext: string = '.mp3') {
+        if (!this._switch_music) return;
+        if (sound instanceof AudioClip) {
+            this._music_source.loop = true;
+            this._music_source.stop();
+            this._music_source.clip = sound;
+            this._music_source.play();
+            this._music_source.volume = this._volume_music;
+        }
+        else {
+            if (this._load_type == loadType.none) {
+                ch_log.warn('音频模块未初始化');
+            } else if (this._load_type == loadType.bundle) {
+                const bundle = assetManager.getBundle(this._bundle_name);
+                if (!bundle) {
+                    ch_log.warn(`请确保 bundle${this._bundle_name} 已加载`);
+                } else {
+                    bundle.load(sound, (err, clip: AudioClip) => {
+                        if (err) {
+                            ch_log.error(err);
+                        }
+                        else {
+                            this._music_source.loop = true;
+                            this._music_source.stop();
+                            this._music_source.clip = clip;
+                            this._music_source.play();
+                            this._music_source.volume = this._volume_music;
+                        }
+                    });
+                }
+            } else if (this._load_type == loadType.remote) {
+                assetManager.loadRemote(this._remote_url + sound + remote_ext, (err: Error | null, clip: AudioClip) => {
+                    if (err) {
+                        ch_log.error(err);
+                    }
+                    else {
+                        this._music_source.loop = true;
+                        this._music_source.stop();
+                        this._music_source.clip = clip;
+                        this._music_source.play();
+                        this._music_source.volume = this._volume_music;
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * stop the audio play
+     */
+    stop() {
+        this._music_source.stop();
+        for (let i = 0; i < this._effect_source_pool.length; i++) {
+            this._effect_source_pool[i].stop();
+
+        }
+    }
+    /**stop and clean */
+    clean() {
+        this._music_source.stop();
+        this._music_source.clip?.name
+        this._music_source.clip = null;
+        for (let i = 0; i < this._effect_source_pool.length; i++) {
+            this._effect_source_pool[i].stop();
+            this._effect_source_pool[i].clip = null;
+        }
+    }
+    /**
+     * pause the audio play
+     */
+    pause() {
+        this._music_source.pause();
+    }
+
+    /**
+     * resume the audio play
+     */
+    resume() {
+        if (!this._switch_music) return;
+        this._music_source.play();
+    }
+
+    /** 重播当前音乐 */
+    public replay_music(): void {
+        this._music_source.stop();
+        this._music_source.play();
+    }
+}

+ 9 - 0
idiom/assets/ch/audio/audio.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "e18034b6-02ea-4a07-92d0-bc9ef0df5fc4",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/ch/ch-sdk.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "c77415ec-dc2b-41a4-8aca-b4fdbd6acb3b",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
idiom/assets/ch/ch-sdk/ch-sdk.umd.js


+ 17 - 0
idiom/assets/ch/ch-sdk/ch-sdk.umd.js.meta

@@ -0,0 +1,17 @@
+{
+  "ver": "4.0.24",
+  "importer": "javascript",
+  "imported": true,
+  "uuid": "1165cb67-475d-486e-91f1-55d85d3dd6eb",
+  "files": [
+    ".js"
+  ],
+  "subMetas": {},
+  "userData": {
+    "loadPluginInEditor": true,
+    "loadPluginInWeb": true,
+    "loadPluginInNative": true,
+    "loadPluginInMiniGame": true,
+    "isPlugin": true
+  }
+}

+ 284 - 0
idiom/assets/ch/ch-sdk/chsdk.d.ts

@@ -0,0 +1,284 @@
+declare namespace chsdk {
+    /**服务器类型对应地址*/
+    enum serverType {
+        /**本地服*/
+        test = 1,
+        /**测试服*/
+        dev = 2,
+        /**正线服*/
+        online = 3
+    }
+    /**广告类型*/
+    enum ad_type {
+        /**激励视频*/
+        rewarded = "Rewarded",
+        /**插屏广告*/
+        interstitial = "Interstitial",
+        /**banner 广告*/
+        banner = "Banner",
+        /**自定义广告*/
+        custom = "Custom"
+    }
+    /**广告状态*/
+    enum ad_state {
+        /**展示失败*/
+        fail = 0,
+        /**展示成功*/
+        show = 1,
+        /**完整展示或点击*/
+        rewarded = 2
+    }
+    /**日志等级*/
+    enum loglevel {
+        OFF = 0,
+        ERROR = 1,
+        WARN = 2,
+        DEBUG = 3,
+        INFO = 4,
+        ALL = 5
+    }
+    /**请求码*/
+    enum code {
+        fail = 999,
+        /**请求成功*/
+        success = 0,
+        /**token失效 */
+        token_err = -1,
+        /**请求超时*/
+        time_out = 1001,
+        /**同一接口请求次数超出限制*/
+        too_many_requests = 1002
+    }
+    /**更新周期
+      1永久,
+      2日更新,
+      3周更新,
+      4月更新
+    */
+    enum updateType {
+        none = 1,
+        day = 2,
+        week = 3,
+        month = 4
+    }
+    /**初始化sdk,并登录对应平台
+     * @param gid 游戏ID(开发后台配置)
+     * @param loglevel 日志级别 关闭=0, ERROR = 1, WARN = 2, DEBUG = 3, INFO = 4, ALL = 5,
+     * @param serverIP 服务器类型  测试服 =2, 正线服 = 3,
+     * @returns code === 0 成功 err?=错误信息
+     */
+    function init(gid: string, loglevel: loglevel, serverIP: serverType): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**当前平台*/
+    function get_pf(): string;
+    /**玩家平台信息*/
+    function get_player_info(): {
+        nickName: string;
+        avatarUrl: string;
+        gender: number;
+        hid: number;
+        province: string;
+        ip: string;
+        loginTime: number;
+        registerTime: number;
+    };
+    /**游戏id*/
+    function get_gid(): string;
+    /**游戏是否初始完毕*/
+    function get_inited(): boolean;
+    /**玩家id*/
+    function get_uid(): number;
+    /**玩家openid*/
+    function get_openid(): string;
+    /**获取平台上用户的呢称和头像,获取成功后自动同步到服务器,一般在进排行榜前调用*/
+    function getUserInfo(): Promise<{
+        nickName: string;
+        avatarUrl: string;
+        gender: number;
+    }>;
+    /**上报自定义埋点事件
+    * @param evt 后台定义的事件名
+    * @param data 后台定义的事件参数
+    */
+    function reportEvent(evt: string, data: {
+        [key: string]: any;
+    }): void;
+    /**上报广告
+    * @param adsId 广告id
+    * @param adsState 广告状态 展示失败=0,展示成功=1,完整展示获得奖励=2
+    * @param adsScene 广告场景目的
+    * @param adsType 广告类型  (默认)激励视频 ='Rewarded',插屏广告='Interstitial', banner 广告 ='Banner',自定义广告 ='Custom'
+    * @param adStartTime 开始播放时间戳ms
+    * @returns code = 0 上报成功 err 上报错误提示
+    */
+    function reportSeeAds(adsId: string, adsType: ad_type, adsState: ad_state, adsScene: string, adStartTime: number): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**
+    * @description 上传游戏远程存储数据
+    * @param key 要保存的key
+    * @param save_data 游戏数据
+    * @returns code 错误码 errMsg 错误信息
+    */
+    function saveGameData(key: string, save_data: {
+        [key: string]: any;
+    }): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**
+    * @description 获取游戏远程存储数据
+    * @param key 数据key
+    * @returns code 错误码 errMsg 错误信息 data 游戏数据
+    */
+    function loadGameData(key: string): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**
+    * @description 更改用户名称头像等信息
+    * @param nickName 名称
+    * @param avatar 头像
+    */
+    function changeUserData(nickName?: string, avatar?: string): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**
+  * @description 上传排行榜数据 
+  * @param rankField 排行榜类型标识
+  * @param rankValue 分数(如果分数和缓存中的数据一致将不上传)
+  * @param update 更新周期(不同周期为不同的排行榜)  1永久, 2每日,3每星期,4每月
+  * @param valueType 数值类 0覆盖(默认) 1:累加
+  * @param extend 扩展数据(非必须)
+  */
+    function saveRankData(rankField: string, rankValue: number, update?: chsdk.updateType, valueType?: 0 | 1, extend?: {
+        [key: string]: any;
+    }): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**
+    * @description 获取排行榜信息
+    * @param rankField 排行榜类型标识
+    * @param update 更新周期(不同周期为不同的排行榜)  1永久, 2每日,3每星期,4每月
+    * @param count 拉取个数
+    * @param isOwn 是否拉取自己的排行 默认为false
+    * @param cache 默认为true,1分钟内优先从缓存中拿排行数据,否则请求服务器
+    * @returns data:{errCode 错误码 errMsg 错误信息 data 排行榜数据(list) own 自己的排名}
+    */
+    function loadRankData(rankField: string, update?: updateType, count?: number, isOwn?: boolean, cache?: boolean): Promise<{
+        code: number;
+        err?: string;
+        data?: {
+            list?: {
+                head: string;
+                nickName: string;
+                rank: number;
+                score: number;
+                userId: number;
+                [key: string]: any;
+            }[];
+            own?: {
+                head: string;
+                nickName: string;
+                rank: number;
+                score: number;
+                userId: number;
+                [key: string]: any;
+            };
+        };
+    }>;
+    /**
+    * @description 上传地区排行榜数据
+    * @param rankField 排行榜类型标识
+    * @param rankValue 分数(如果分数和缓存中的数据一致将不上传)
+    * @param update 更新周期(不同周期为不同的排行榜)  1永久, 2每日,3每星期,4每月
+    * @param extend 扩展数据(非必须)
+    */
+    function saveProvinceRankData(rankField: string, rankValue: number, update?: updateType, extend?: {
+        [key: string]: any;
+    }): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**
+    * @description 获取具体某地区排行榜信息
+    * @param hid 省份代码
+    * @param rankField 排行榜类型标识
+    * @param update 更新周期(不同周期为不同的排行榜)  1永久, 2每日,3每星期,4每月
+    * @param count 拉取个数
+    * @param isOwn 是否显示自己的排行 默认为false不显示
+    * @param cache 默认为true,1分钟内优先从缓存中拿排行数据,否则请求服务器
+    * @returns data:{errCode 错误码 errMsg 错误信息 data 排行榜数据数组}
+    */
+    function loadProvinceRankData(hid: number, rankField: string, update?: updateType, count?: number, isOwn?: boolean, cache?: boolean): Promise<{
+        code: number;
+        err?: string;
+        data?: {
+            list?: {
+                head: string;
+                nickName: string;
+                rank: number;
+                score: number;
+                userId: number;
+                [key: string]: any;
+            }[];
+            own?: {
+                head: string;
+                nickName: string;
+                rank: number;
+                score: number;
+                userId: number;
+                [key: string]: any;
+            };
+        };
+    }>;
+    /**
+    * @description 获取全国省份地区排行榜信息
+    * @param rankField 排行榜类型
+    * @param cache 默认为true,1分钟内优先从缓存中拿排行数据,否则请求服务器
+    * @returns data:{errCode 错误码 errMsg 错误信息 data 排行榜数据数组{hid 省份代码,province省份名...}}
+    */
+    function loadProvinceRankInfo(rankField: string, cache?: boolean): Promise<{
+        code: number;
+        err?: string;
+        data?: {
+            hid: number;
+            province: string;
+            rank: number;
+            score: number;
+        }[];
+    }>;
+    /**玩家改变玩家默认省份
+    * @param hid 省份代码
+    * @returns errCode 错误码 errMsg 错误信息 time服务器时间戳(秒)
+    */
+    function setUserLocation(hid: number): Promise<{
+        code: number;
+        err?: string;
+        data?: number;
+    }>;
+    /**获取服务器时间戳(秒)
+    * @returns errCode 错误码 errMsg 错误信息 time服务器时间戳(秒)
+    */
+    function getServerTime(): Promise<{
+        code: number;
+        err?: string;
+        time?: number;
+    }>;
+    /**获取城市代码对应地名 */
+    function provinceCode2Name(hid: number): string;
+    /**获取所有城市代码名字*/
+    function getAllProvince(): [number, string][];
+}

+ 9 - 0
idiom/assets/ch/ch-sdk/chsdk.d.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "0677b62c-5ed1-4c63-b79b-8aa69e6cfb88",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 45 - 0
idiom/assets/ch/ch-sdk/提示说明.txt

@@ -0,0 +1,45 @@
+1.
+chsdk.d.ts
+ch-sdk.umd.js
+直接将上面的文件复制到cocosreator assets里
+
+2.
+尽早调用chsdk.init 初始化后成功后可以调用其它接口
+具体接口参看接口chsdk.d.ts提示说明
+3.
+注意 chsdk.init 初始化的参数
+上线测试阶段初始化可以使用测试服
+正式上线后改为正式服关闭调试输出
+游戏 id和自定义上报事件需要在后台设置,需要和运营人员沟通
+
+下面的地址需要微信或抖音后台添加到白名单
+测试服务器地址
+https://dev.ichunhao.cn
+https://receivetest.ichunhao.cn
+正式服务器地址
+https://receive.ichunhao.cn
+https://app.ichunhao.cn
+
+
+cocoscreator 测试脚本
+@ccclass('test')
+export class test extends Component {
+    async start() {
+        const ret = await chsdk.init('1001',chsdk.loglevel.ALL,chsdk.serverType.dev);
+        console.log('初始化',ret); 
+        let r = await chsdk.saveRankData('test',99,chsdk.updateType.day);
+        console.log('上传排行傍数据',r); 
+        r = await chsdk.loadRankData('test',chsdk.updateType.day,50,true);
+        console.log('拉取排行傍数据',r); 
+        r = await chsdk.saveGameData('test',{test_data:"测试数据"});
+        console.log('保存游戏数据',r);
+        r = await chsdk.loadGameData('test');
+        console.log('加载游戏数据',r);
+        r = await chsdk.getServerTime();
+        console.log('获取服务器时间',r);
+        r=await chsdk.loadProvinceRankInfo('level');
+        console.log('获取全国榜',r);
+    }
+    update(deltaTime: number) {
+    }
+}

+ 11 - 0
idiom/assets/ch/ch-sdk/提示说明.txt.meta

@@ -0,0 +1,11 @@
+{
+  "ver": "1.0.1",
+  "importer": "text",
+  "imported": true,
+  "uuid": "0151a2ff-cb6e-4173-94db-c63348e8b154",
+  "files": [
+    ".json"
+  ],
+  "subMetas": {},
+  "userData": {}
+}

+ 32 - 0
idiom/assets/ch/ch.ts

@@ -0,0 +1,32 @@
+import ch_audio from "./audio/audio";
+import pvp from "./pvp/ch_pvp";
+import util from "./ch_util";
+import sign from "./sign/sign";
+import { ch_net, game_protocol } from "./net/net";
+import { NetPlayer } from "./net/NetPlayer";
+import { NetRoom } from "./net/NetRoom";
+export { NetPlayer, NetRoom };
+export type { game_protocol };
+export const ch = {
+    /**主sdk(需要初始化)*/
+    sdk: chsdk,
+    /**日志*/
+    log: chsdk.log,
+    /**本地缓存*/
+    storage: chsdk.storage,
+    /**日期*/
+    date: chsdk.date,
+    /**创建一个模块事件*/
+    get_new_event<CT>() { return chsdk.get_new_event<CT>() },
+    //---------------------------------------------
+    /**交互*/
+    pvp: pvp,
+    /**创建一个新的网络连接管理*/
+    get_new_net<gd extends game_protocol>() { return new ch_net<gd>(); },
+    /**工具*/
+    util: util,
+    /**登录签到模块*/
+    get sign(): sign { return sign.getInstance(); },
+    /**音频播放模块(需要初始化)*/
+    get audio(): ch_audio { return ch_audio.getInstance(); },
+}

+ 9 - 0
idiom/assets/ch/ch.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "168012b2-7345-4efb-b543-bcd5b60a2689",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 348 - 0
idiom/assets/ch/ch_util.ts

@@ -0,0 +1,348 @@
+import { size, view, Node, UITransform, screen, ImageAsset, SpriteFrame, Texture2D, sys, assetManager } from "cc";
+/**工具*/
+class ch_util {
+    private static _instance: ch_util;
+    public static getInstance(): ch_util {
+        if (!this._instance) this._instance = new ch_util();
+        return this._instance;
+    }
+
+    /**
+    * 随机数 (包含min,不包含max)
+    * @param min
+    * @param max
+    * @param isInt
+    * @return {*}
+    */
+    public getRandom(min: number = 0, max: number = 1): number {
+        if (min == null) min = 0;
+        if (max == null) max = 1;
+        if (min === max) return min;
+        return min + (Math.random() * (max - min));
+    }
+    /**
+    * 随机整数-不包含最大值
+    * @param min
+    * @param max
+    * @return {*}
+    */
+    public getRandomInt(min: number, max: number): number {
+        min = Math.ceil(min); max = Math.ceil(max);
+        return Math.floor(Math.random() * (max - min)) + min;
+    }
+    /** 生成随机整数 -1 或 1*/
+    public getRandomDir(): -1 | 1 {
+        return Math.floor(Math.random() * 2) === 0 ? -1 : 1;
+    }
+    /**
+    * 在指定数组中随机取出N个不重复的数据
+    * @param resArr
+    * @param ranNum
+    * @returns {Array}
+    */
+    public getRandomDiffValueFromArr<T>(resArr: Array<T>, ranNum: number): Array<T> {
+        let arr = new Array<T>();
+        let result = new Array<T>();
+        if (!resArr || resArr.length <= 0 || ranNum <= 0) {
+            return result;
+        }
+        for (let i = 0; i < resArr.length; i++) {
+            arr.push(resArr[i]);
+        }
+        if (ranNum >= arr.length) return arr;
+        ranNum = Math.min(ranNum, arr.length - 1);
+        for (let i = 0; i < ranNum; i++) {
+            let ran = this.getRandomInt(0, arr.length - 1);
+            result.push(arr.splice(ran, 1)[0]);
+        }
+        return result;
+    }
+    /**
+     * 取小数位
+     * @param decimal 小数
+     * @param places 位数
+     * @return {number}
+     */
+    public numberToDecimal(decimal: number, places: number): number {
+        let round: number = Math.pow(10, places);
+        return Math.round(decimal * round) / round;
+    }
+
+    public parse(text: string, reciver?: (key: any, value: any) => any): any {
+        try {
+            return JSON.parse(text, reciver);
+        } catch (error) {
+            //ch_log.error(error);
+            return null;
+        }
+    }
+
+    public stringify(value: any, replacer?: (key: string, value: any) => any, space?: string | number) {
+        try {
+            return JSON.stringify(value, replacer, space);
+        } catch (error) {
+            return null;
+        }
+    }
+    /**
+     * 判断字符是否为双字节字符(如中文字符)
+     * @param string 原字符串
+     */
+    public str_isDoubleWord(string: string): boolean {
+        return /[^\x00-\xff]/.test(string);
+    }
+    /**
+    * 是否为空
+    * @param str 
+    */
+    public str_isEmpty(str: string): boolean {
+        if (str == null || str == undefined || str.length == 0) {
+            return true;
+        }
+        return false;
+    }
+    /**
+     * 转美式计数字符串
+     * @param value 数字
+     * @example
+     * 123456789 = 123,456,789
+     */
+    public numberTotPermil(value: number): string {
+        return value.toLocaleString();
+    }
+    private readonly _k: number = 1000;
+    private readonly _k_sizes_en: string[] = ['', 'K', 'M', 'G', 'T', 'P', 'E'];
+    private readonly _k_sizes_cn: string[] = ['', '千', '百万', '十亿', '万亿', '拍(千万亿)', '艾(十亿亿)'];
+    private readonly _w: number = 10000;
+    private readonly _w_sizes_en: string[] = ['', 'W', 'M', 'B', 'T'];
+    private readonly _w_sizes_cn: string[] = ['', '万', '亿', '万亿'];
+    /**  
+    * 通用单位转换方法  
+    */
+    private convertNumber(value: number, base: number, sizes: string[], fixed: number): string {
+        if (value < base) return value.toString();
+        const i = Math.floor(Math.log(value) / Math.log(base));
+        const r = value / Math.pow(base, i);
+        if (i >= sizes.length) return value.toString();
+        return `${r.toFixed(fixed)}${sizes[i]}`;
+    }
+    /**   
+     * 转单位计数(默认英文)  
+     * @param value 数字  
+     * @param fixed 保留小数位数  
+     * @param isEn 是否英文  
+     * @example  
+     * 12345 = 12.35K  
+     */
+    public numberToThousand(value: number, fixed: number = 2, isEn: boolean = true): string {
+        const sizes = isEn ? this._k_sizes_en : this._k_sizes_cn;
+        return this.convertNumber(value, this._k, sizes, fixed);
+    }
+    /**   
+     * 转单位计数(默认中文)  
+     * @param value 数字  
+     * @param fixed 保留小数位数  
+     * @param isEn 是否英文  
+     * @example  
+     * 12345 = 1.23万  
+     */
+    public numberToTenThousand(value: number, fixed: number = 2, isEn: boolean = false): string {
+        const sizes = isEn ? this._w_sizes_en : this._w_sizes_cn;
+        return this.convertNumber(value, this._w, sizes, fixed);
+    }
+    /**获取一个唯一标识的字符串 */
+    public guid() {
+        let guid: string = Math.random().toString(36).substring(2);
+        return guid;
+    }
+    /**排序一个json Object*/
+    public obj_sort(obj: any): any {
+        let sorted_keys: string[] = Object.keys(obj).sort();
+        let new_obj = {};
+        for (let i = 0; i < sorted_keys.length; i++) {
+            const key = sorted_keys[i];
+            const value = obj[key];
+            const t = this.obj_get_value_type(value);
+            new_obj[key] = t == 'object' ? this.obj_sort(value) : value;
+        }
+        return new_obj;
+    }
+    /**获取一个josn值的类型*/
+    public obj_get_value_type(value: any): string {
+        let type: string;
+        if (value === null) {
+            type = 'null';
+        } else {
+            type = typeof value;
+        }
+        // 如果是对象或数组,还可以进一步判断
+        if (type === 'object') {
+            if (Array.isArray(value)) { type = 'array'; }
+            //else if (value instanceof Date) {  type = 'date';   }  
+        }
+        return type;
+    }
+    /**
+     * 判断指定的值是否为对象
+     * @param value 值
+     */
+    public valueIsObject(value: any): boolean {
+        return Object.prototype.toString.call(value) === '[object Object]';
+    }
+    /**
+    * 深拷贝
+    * @param target 目标
+    */
+    /** 克隆对象 */
+    public obj_clone<T = any>(target_: T, record_set = new Set()): T {
+        let result: any;
+
+        switch (typeof target_) {
+            case "object": {
+                // 数组:遍历拷贝
+                if (Array.isArray(target_)) {
+                    if (record_set.has(target_)) {
+                        return target_;
+                    }
+
+                    record_set.add(target_);
+                    result = [];
+                    for (let k_n = 0; k_n < target_.length; ++k_n) {
+                        // 递归克隆数组中的每一项
+                        result.push(this.obj_clone(target_[k_n], record_set));
+                    }
+                }
+                // null:直接赋值
+                else if (target_ === null) {
+                    result = null;
+                }
+                // RegExp:直接赋值
+                else if ((target_ as any).constructor === RegExp) {
+                    result = target_;
+                }
+                // 普通对象:循环递归赋值对象的所有值
+                else {
+                    if (record_set.has(target_)) {
+                        return target_;
+                    }
+
+                    record_set.add(target_);
+                    result = {};
+                    for (const k_s in target_) {
+                        result[k_s] = this.obj_clone(target_[k_s], record_set);
+                    }
+                }
+
+                break;
+            }
+
+            case "function": {
+                result = target_.bind({});
+                break;
+            }
+
+            default: {
+                result = target_;
+            }
+        }
+
+        return result;
+    }
+    /**
+    * 拷贝对象
+    * @param target 目标
+    */
+    public copy(target: object): object {
+        return this.parse(this.stringify(target));
+    }
+    //请求相关--------------------------------------------
+    public getReqId(): string {
+        return Date.now() + "_" + Math.ceil(1e3 * Math.random());
+    }
+    private _url_data: Record<string, string> | null = null;
+    /**获取链接中传入的某个值*/
+    public getUrlData<T extends (string | number)>(key: string): T | null {
+        if (!this._url_data) {
+            this._url_data = this.parseUrl();
+        }
+        const value = this._url_data[key];
+        if (value === undefined) return null;
+        if (typeof value === 'string') {
+            const numberValue = parseFloat(value);
+            if (!isNaN(numberValue)) return (numberValue as unknown) as T;
+            return value as T;
+        }
+        return null;
+    }
+    private parseUrl(): Record<string, string> {
+        if (!sys.isBrowser || typeof window !== "object" || !window.document) return {};
+        const url = window.document.location.href;
+        const queryString = url.split("?")[1];
+        if (!queryString) return {};
+        return queryString.split("&").reduce<Record<string, string>>((acc, param) => {
+            const [key, value] = param.split("=");
+            if (key) acc[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
+            return acc;
+        }, {});
+    }
+    /**获到node的坐标和范围用于平台在对应位置创建按纽*/
+    public getBtnOp(btnNode: Node): { left: number, top: number, width: number, height: number } {
+        let btnNodeUiTransform = btnNode.getComponent(UITransform);
+        const btnSize = size(btnNodeUiTransform.width + 0, btnNodeUiTransform.height + 0);
+        //let frameWidth = screen.windowSize.width / screen.devicePixelRatio
+        let frameHeight = screen.windowSize.height / screen.devicePixelRatio
+        //const winSize = screen.windowSize;
+        //const designSize = view.getDesignResolutionSize();
+        //console.log('designSize', designSize);
+        //console.log('winSize:', winSize);
+        //console.log('frameSize:', frameWidth,frameHeight);
+        //适配不同机型来创建微信授权按钮
+        let rect = btnNodeUiTransform.getBoundingBoxToWorld();
+        let ratio = screen.devicePixelRatio;
+        let scale = view.getScaleX();
+        let factor = scale / ratio;
+        let offsetX = 0;
+        let offsetY = 0;
+        let top = frameHeight - (rect.y + rect.height) * factor - offsetY;
+        let left = rect.x * factor + offsetX;
+        const width = btnSize.width * factor;
+        const height = btnSize.height * factor;
+        return { left: left, top: top, width: width, height: height }
+    }
+    /**远程加载图片*/
+    public async loadImage(url: string): Promise<SpriteFrame> {
+        if (chsdk.get_pf() == chsdk.pf.web) {
+            return await this.loadRemoteSprite(url);
+        } else {
+            const idata = await chsdk.loadImage(url);
+            return this.getSpriteFrame(idata);
+        }
+    }
+    /**cocos加载远程图片*/
+    public loadRemoteSprite(remoteUrl: string, ext: '.png' | '.jpg' = '.png'): Promise<SpriteFrame> {
+        return new Promise((resolve) => {
+            assetManager.loadRemote<ImageAsset>(remoteUrl, { ext: ext }, function (err, imageAsset) {
+                const spriteFrame = new SpriteFrame();
+                const texture = new Texture2D();
+                texture.image = imageAsset;
+                spriteFrame.texture = texture;
+                resolve(spriteFrame);
+            });
+        });
+    }
+    /**远程加载的图片数据转成spriteFrame*/
+    private getSpriteFrame(img: any): SpriteFrame {
+        if (!img) return null;
+        try {
+            const spriteFrame = new SpriteFrame();
+            const texture = new Texture2D();
+            texture.image = img instanceof ImageAsset ? img : new ImageAsset(img);
+            spriteFrame.texture = texture;
+            return spriteFrame;
+        } catch (error) {
+            //ch_log.warn(error);
+            return null;
+        }
+    }
+}
+export default ch_util.getInstance();

+ 9 - 0
idiom/assets/ch/ch_util.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "1badfb4c-c407-40ee-9e7f-6f1aef485f28",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 381 - 0
idiom/assets/ch/chsdk_inside.d.ts

@@ -0,0 +1,381 @@
+declare namespace chsdk {
+    /**日志等级*/
+    enum loglevel {
+        OFF = 0,
+        ERROR = 1,
+        WARN = 2,
+        DEBUG = 3,
+        INFO = 4,
+        ALL = 5
+    }
+    /**上报类型*/
+    export enum reportType {
+        /**默认不上报*/
+        off = 0,
+        /**使用ch服务器*/
+        ch = 1,
+        /**上报到应用平台(需在各平台后台设置上报事件,微信,抖音)*/
+        platform = 2,
+        /**上面两个都选*/
+        ch__platform = 3
+    }
+    /**平台*/
+    export enum pf {
+        /**测试*/
+        web = "web",
+        /**微信*/
+        wx = "wx",
+        /**抖音*/
+        tt = "tt"
+    }
+    /**获取token*/
+    export function getToken(): string;
+    export function init_inside(gid: string, loglevel: number, serverIP: number | string, isLocal?: boolean, report?: reportType): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**
+     * 获取基本功能完成接口地址
+     * @param action 接口字符串如 '/user/shareList'
+     * @returns
+     */
+    export function getUrl(action: string): string;
+    /**
+    * 获取上报功能完成接口地址
+    * @param action 接口字符串如 '/user/shareList'
+    * @returns
+    */
+    export function getReportUrl(action: string): string;
+    export function check_req_time(req_type: string): {
+        code: number;
+        err: string;
+    } | null;
+    export function makePostTokenRequest(url: string, body?: any): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    export function base64_encode(input: string): string;
+    export function base64_decode(input: string): string;
+    /**
+     * md5加密
+     */
+    export function md5HashStr(str: string): string;
+    /**
+     * 一次http请求
+     * @param url 请求地址
+     * @param method 请求方式
+     * @param data
+     * @param timeout
+     * @param responseType
+     * @param headers
+     * @returns
+     */
+    export function do_request(url: string, method: 'POST' | 'GET' | 'PUT', data?: any, timeout?: number, responseType?: XMLHttpRequestResponseType, headers?: {
+        [key in string]: string;
+    }): Promise<any>;
+    /**验证权限功能
+    * key 某个功能权限
+    * 返回 是否验证成功
+    */
+    export function verify_option(key: string): boolean;
+    /**获取一个唯一标识的字符串 */
+    export function guid(): string;
+    /**判断是否有名字,没有返回玩家*/
+    export function getDefNickName(info: any): string;
+    /**
+    * 主动记录分数到缓存
+    * @param rankType 排行榜类型
+    * @param update 更新类型
+    * @param type  0 个人排行 1地区排行
+    * @param score 分数
+    */
+    export function recordCacheScore(rankField: string, update: number, type: 0 | 1, score: number): void;
+    /**
+     * 获取缓存中的分数
+     * @param rankField
+     * @param update
+     * @param type
+     * @returns
+     */
+    export function getCacheScore(rankField: string, update: number, type: 0 | 1): number;
+    /**清空所有缓存分数*/
+    export function cleanCacheScore(): void;
+    /**清空所有缓存排行榜数据*/
+    export function cleanCacheRank(): void;
+    /**当前平台是否有分享功能*/
+    export function canShareAppMessage(): boolean;
+    /**
+    * 主动拉起分享转发
+    */
+    export function shareAppMessage(title?: string, imageUrlId?: string, imageUrl?: string, message?: string): void;
+    /**
+    * 主动拉起分享转发并等待是错分享成功
+    */
+    export function shareAppMessageAsync(title?: string, imageUrlId?: string, imageUrl?: string, message?: string): Promise<boolean>;
+    /**设置被动分享参数,具体参看平台的参数设置*/
+    export function setOnShareAppMessage(res: {}): void;
+    export function getQuery(): any;
+    /**
+    * @description 上传从分享进入(无需主动调用,除非需要在message里加入游戏特别数据)
+    * @param openid 服务器拿到的玩家oid 如果为空从平台拿
+    * @param message 分享的自定义数据
+    */
+    export function sendShare(openid?: string | null, message?: string | null): Promise<{
+        code: number;
+        err?: string;
+        data?: any;
+    }>;
+    /**
+    * @description 获取从自己分享进入游戏的玩家列表
+    */
+    export function getShareList(): Promise<{
+        code: number;
+        err?: string;
+        data?: {
+            gid: string;
+            head: string;
+            hid: number;
+            ip: string;
+            loginTime: number;
+            nickName: string;
+            openId: string;
+            option: string;
+            pf: string;
+            registerTime: number;
+            userId: number;
+            msg?: string;
+        }[];
+    }>;
+    /**播放奖励广告
+        * @param scene 奖励目的场景
+       playRewardAd("复活")
+       .then(success => {
+         if (success) {
+             console.log("广告播放成功,用户获得奖励");
+             // 在这里执行成功后的操作
+         } else {
+             console.log("广告播放失败,用户未获得奖励");
+             // 处理广告播放失败的逻辑
+         }
+       })
+       .catch(error => {
+           console.error("发生未处理的错误: ", error);
+        });
+        */
+    export function playRewardAd(scene: string): Promise<boolean>;
+    /**播放插屏广告*/
+    export function playInsterAd(scene: string): Promise<boolean>;
+    /**设置广告配置*/
+    export function setConf(pf: pf, conf: {
+        adUnitId: string;
+        multiton: boolean;
+        inster_unitId?: string;
+        tmplIds?: string[];
+    }): void;
+    /**当前平台是否有侧边栏功能*/
+    export function checkHasSidebar(): boolean;
+    /**抖音是否从侧边栏进入游戏,微信是否从我的小程序进入游戏*/
+    export function checkFromSidebar(): boolean;
+    /**抖音进入侧边栏*/
+    export function goToSidebar(): boolean;
+    /**开始录屏*/
+    export function recorderStart(duration?: number): void;
+    /**结束录屏*/
+    export function recorderStop(): void;
+    /**分享录屏*/
+    export function shareRecord(title?: string, desc?: string, path?: string, topics?: string[]): void;
+    /**创建反馈按钮
+    * @param op 按纽坐标大小
+    * 例:feed_node.active = ch.sdk.createFeedbackButton(ch.util.getBtnOp(feed_node));
+    * 如果对应平台有此功能(目前只有wx)返回true,没有返回false
+    * 成功记得 destoryFeedbackButton();*/
+    export function createFeedbackButton(op: {
+        left: number;
+        top: number;
+        width: number;
+        height: number;
+    }): boolean;
+    /**销毁反馈按钮*/
+    export function destoryFeedbackButton(): void;
+    /**复制到剪切板*/
+    export function setClipboardData(content: string): void;
+    /**短震动(15ms)*/
+    export function vibrateShort(): void;
+    /**长震动(400ms)*/
+    export function vibrateLong(): void;
+    /**界面提示信息*/
+    export function showToast(title: string, duration?: number, icon?: 'success' | 'error' | 'fail' | 'loading' | 'none'): boolean;
+    /**获取菜单坐标范围*/
+    export function getMenuButtonBoundingClientRect(): any;
+    export function showLoading(title?: string): boolean;
+    export function hideLoading(): void;
+    export function showModal(title?: string, content?: string, confirmText?: string, showCancel?: boolean, cancelText?: string): Promise<{
+        confirm: boolean;
+        cancel: boolean;
+    }>;
+    export function openSetting(scope: string | 'scope.userInfo' | 'scope.userLocation'): Promise<boolean>;
+    /**加载图片
+    * cocos里不要直接调用,使用 ch.util.loadImage(url);
+    */
+    export function loadImage(imgURL: string): Promise<any>;
+    /**当前平台是否有关注功能*/
+    export function canAwemeUserProfile(): boolean;
+    /**检查是否已经关注*/
+    export function checkFollowAwemeState(): Promise<boolean>;
+    /**打开关注返回关注结果*/
+    export function openAwemeUserProfile(): Promise<boolean>;
+    /**当前平台是否有订阅功能*/
+    export function canSubscribeMessage(): boolean;
+    /**订阅消息 参数为空的话使用配置里的模板id*/
+    export function requestSubscribeMessage(tmplIds?: string[]): Promise<boolean>;
+    class ch_log {
+        private static _instance;
+        static getInstance(): ch_log;
+        private _log_level;
+        set_log_level(loglevel: loglevel): void;
+        private _no;
+        private _log;
+        private _info;
+        private _debug;
+        private _trace;
+        private _warn;
+        private _error;
+        private _log_start;
+        private _log_end;
+        get log(): (message?: any, ...optionalParams: any[]) => void;
+        get info(): (message?: any, ...optionalParams: any[]) => void;
+        get debug(): (message?: any, ...optionalParams: any[]) => void;
+        get trace(): (message?: any, ...optionalParams: any[]) => void;
+        get warn(): (message?: any, ...optionalParams: any[]) => void;
+        get error(): (message?: any, ...optionalParams: any[]) => void;
+        /**记录开始计时*/
+        get log_start(): (label?: string) => void;
+        /** 打印范围内时间消耗*/
+        get log_end(): (label?: string) => void;
+    }
+    /**日志打印*/
+    export const log: ch_log;
+    /**事件*/
+    export interface EventsMap {
+        [event: string]: (...args: any[]) => void;
+    }
+    export type EventNames<Map extends EventsMap> = keyof Map & string;
+    export type OmitIndex<T> = {
+        [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
+    };
+    export type EventParams<Map extends EventsMap, Ev extends EventNames<Map>> = Parameters<Map[Ev]>;
+    class EventModel<CT extends OmitIndex<EventsMap>> {
+        private _handlersMap;
+        /** 事件键 */
+        key: {
+            [k in EventNames<CT>]: k;
+        };
+        /**
+        * 监听事件
+        * @param type_ 事件类型
+        * @param callback_ 触发回调
+        * @param target_ 事件目标对象
+        * @param once_b_ 是否触发单次
+        * @returns 触发回调
+        */
+        on<T extends EventNames<CT>, T2 extends (...event_: EventParams<CT, T>) => void>(type_: T, callback_: T2, target_?: any, once_?: boolean): typeof callback_ | null;
+        once<T extends EventNames<CT>, T2 extends (...event_: EventParams<CT, T>) => void>(type_: T, callback_: T2, target_?: any): void;
+        /**
+         * 取消监听事件
+         * @param type_ 事件类型
+         * @param callback_ 触发回调
+         * @param target_ 事件目标对象
+         * @returns 触发回调
+         */
+        off<T extends EventNames<CT>, T2 extends (...event_: EventParams<CT, T>) => void>(type_: T, callback_?: T2, target_?: any): void;
+        /**
+         * 清除某个事件队列
+         * @param type_ 事件名,为空的话清除所有
+         */
+        clearAll<T extends EventNames<CT>>(type_?: T): void;
+        /**
+        * 派发事件
+        * @param type_ 事件类型
+        * @param args_ 事件参数
+        */
+        emit<T extends EventNames<CT>, T2 extends EventParams<CT, T>>(type_: T, ...args_: T2): void;
+        private _emit;
+    }
+    /**sdk事件*/
+    export interface event_sdk {
+        show(): void;
+        hide(): void;
+        onShare(success: boolean): void;
+    }
+    export const sdk_event: EventModel<event_sdk>;
+    export function get_new_event<CT>(): EventModel<CT>;
+    /**日期时间*/
+    class ch_date {
+        private static _instance;
+        static getInstance(): ch_date;
+        private _day_s;
+        private _diff;
+        updateServerTime(s: number): void;
+        now(): number;
+        getTime(): number;
+        getDayStartTime(s: number): number;
+        getDayEndTime(e: number): number;
+        getWeekEndTime(e: number): number;
+        getMonthEndTime(e: number): number;
+        /**两个时间相隔天数*/
+        getDiffDayNum(e: number, t: number): number;
+        /**判定两个时间是否是同一天*/
+        isSameDate(timestamp1: number, timestamp2: number): boolean;
+        /**
+         * 单位毫秒格式化
+         * - $H: 替换为小时,补全空位(02:00:00)
+         * - $h: 替换为小时,不补全(2:00:00)
+         * - $M: 替换为分钟,补全空位(00:02:00)
+         * - $m: 替换为分钟,不补全(00:2:00)
+         * - $S: 替换为秒,补全空位(00:00:02)
+         * - $s: 替换为秒,不补全(0:00:2)
+         */
+        ms_format(ms_n_: number, format_s_?: string): string;
+    }
+    export const date: ch_date;
+    /**本地存储*/
+    class ch_storage {
+        private static _instance;
+        static getInstance(): ch_storage;
+        /**
+         * 缓存变量存储
+         * @param  key
+         * @param  value
+         * @param  user_id 区别用户
+         */
+        set(key: string, value: any, user_id?: string | null): any;
+        /**
+         * 获取缓存变量区别用户
+         * @param {*} key
+         * @returns
+         */
+        private get;
+        /** 获取指定关键字的string*/
+        getString(key: string, user_id?: string | null): string | null;
+        /** 获取指定关键字的数值 */
+        getNumber(key: string, user_id?: string | null, defaultValue?: number): number;
+        /** 获取指定关键字的布尔值*/
+        getBoolean(key: string, user_id?: string | null): boolean;
+        /** 获取指定关键字的JSON对象*/
+        getJson(key: string, user_id?: string | null): any;
+        /** 获取指定关键字的JSON对象*/
+        getObject(key: string, user_id?: string | null): any;
+        /**
+         * 删除缓存变量
+         * @param {*} key
+        */
+        remove(key: string, user_id?: string | null): any;
+        /** 清空整个本地存储 */
+        clear(): void;
+    }
+    /**本地存储*/
+    export const storage: ch_storage;
+    export { };
+}

+ 9 - 0
idiom/assets/ch/chsdk_inside.d.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "21d5e548-813e-465e-8ac1-1429b03fc0b5",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/ch/net.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "4826a8c5-6a15-4bb8-a347-85f281595aa1",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 36 - 0
idiom/assets/ch/net/NetBase.ts

@@ -0,0 +1,36 @@
+/**网络变量基类*/
+export class NetBase {
+    private _id: string;
+    public get Id(): string { return this._id };
+    private dataMap: { [key: string]: any } = {};
+    private dirtyMap: { [key: string]: any } = {};
+    constructor(id: string) {
+        this._id = id;
+        this.dataMap = {};
+    }
+    /**初始变量值*/
+    public initValue(data: { [key: string]: any }): void {
+        this.dataMap = data ?? {};
+    }
+    /**修改某个键的值*/
+    protected setValue(key: string, value: any): void {
+        this.dataMap[key] = value;
+    }
+    protected setValueDirty(key: string, value: any): void {
+        this.dirtyMap[key] = value;
+    }
+    /**获取数据的值*/
+    protected getValue(key: string): any {
+        return this.dataMap[key];
+    }
+    /**处理脏数据*/
+    private doDirtyData(f: (data: { [key: string]: any }) => void): void {
+        if (Object.keys(this.dirtyMap).length === 0) return null;
+        f(this.dirtyMap);
+        this.dirtyMap = {};
+    }
+    protected dispose(): void {
+        this.dataMap = {};
+        this.dirtyMap = {};
+    }
+}

+ 1 - 0
idiom/assets/ch/net/NetBase.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"ee4ce3fc-63ab-4e6f-badd-226a50749568","files":[],"subMetas":{},"userData":{}}

+ 135 - 0
idiom/assets/ch/net/NetPlayer.ts

@@ -0,0 +1,135 @@
+import { NetBase } from "./NetBase";
+type EventNames<Map extends chsdk.OmitIndex<chsdk.EventsMap>> = Extract<keyof Map, string>;
+type StartEventNames<Map extends chsdk.OmitIndex<chsdk.EventsMap>, Prefix extends string> = Extract<keyof Map, string> extends infer K ? K extends `${Prefix}${string}` ? K : never : never;
+type EventParamFrist<Map extends chsdk.OmitIndex<chsdk.EventsMap>, Ev extends EventNames<Map>> = Ev extends keyof Map
+    ? Map[Ev] extends (...args: infer Params) => any ? Params extends [infer First, ...any[]] ? First : never : never : never;
+type PickEvent<T, Prefix1 extends string, Prefix2 extends string> = Pick<T, Extract<keyof T, `${Prefix1}${string}` | `${Prefix2}${string}`>>;
+/**网络玩家*/
+export class NetPlayer<GD> extends NetBase {
+    public readonly evt = chsdk.get_new_event<PickEvent<GD, 'p_', 'online' | 'finish' | 'ready'>>();
+    private _location: number = 0;
+    private _online: boolean = true;
+    private _ready: boolean = false;
+    private _score: number = 0;
+    public get score(): number { return this._score };
+    private _rank: number = 0;
+    public get rank(): number { return this._rank };
+    private _totalRank: number = 0;
+    public get totalRank(): number { return this._totalRank };
+    private _ishost: boolean = true;
+    /**房间位置0开头*/
+    public get location(): number { return this._location };
+    private _userData: { gid: string, head: string, hid: number, ip: string, loginTime: number, nickName: string, openId: string, option: string, pf: string, registerTime: number, userId: number };
+    public get gid(): string { return this._userData.gid };
+    public get head(): string { return this._userData.head };
+    public get hid(): number { return this._userData.hid };
+    public get province(): string { return chsdk.provinceCode2Name(this._userData.hid) ?? '其它' };
+    public get ip(): string { return this._userData.ip };
+    public get loginTime(): number { return this._userData.loginTime };
+    public get nickName(): string { return this._userData.nickName || '玩家' + this._userData.userId };
+    public get openId(): string { return this._userData.openId };
+    public get option(): string { return this._userData.option };
+    public get pf(): string { return this._userData.pf };
+    public get userId(): number { return this._userData.userId };
+    public get registerTime(): number { return this._userData.registerTime };
+    /**是否是主机*/
+    public get isHost(): boolean { return this._ishost };
+    /**是否准备*/
+    public get ready(): boolean { return this._ready };
+    /**是否在线*/
+    public get online(): boolean { return this._online };
+    /**是否能开启匹配*/
+    public get canMatch(): boolean { return this.online && this.ready };
+    private _rankInfo: { LowerRank: number, Rank: number, Star: number } | null;
+    /**段位信息*/
+    public get level(): { LowerRank: number, Rank: number, Star: number } | null { return this._rankInfo };
+    /**是否是当前玩家自己 */
+    public get isOwn(): boolean { return chsdk.get_uid() === this.userId; }
+    /**是否是AI */
+    public get isAI(): boolean { return this._userData.userId < 8888; };
+    private _canAI: boolean = false;
+    /**是否有控制当前AI权限*/
+    public get canAI(): boolean { return this._canAI; };
+    /**是否有此玩家数据权限*/
+    public get isPermission(): boolean { return this.isOwn || this._canAI; };
+    public init(pd: { location: number, status: boolean, userData: any, rank: number, TotalRank: number, score: number, teamReady?: boolean; gameData?: any, userRank?: any }): void {
+        this._location = pd.location;
+        this._online = pd.status ?? true;
+        this._ready = pd.teamReady;
+        this._score = pd.score;
+        this._rank = pd.rank;
+        this._totalRank = pd.TotalRank;
+        this._userData = pd.userData;
+        this._userData.hid = Number.parseInt(pd.userData.hid);
+        this._userData.loginTime = Number.parseInt(pd.userData.loginTime);
+        this._userData.registerTime = Number.parseInt(pd.userData.registerTime);
+        this._userData.userId = Number.parseInt(pd.userData.userId);
+        this._rankInfo = pd.userRank;
+        if (pd.gameData) this.initValue(pd.gameData);
+    }
+    private set_level(level: { LowerRank: number, Rank: number, Star: number }, rank: number, score: number, totalRank: number): void {
+        this._rankInfo = level;
+        this._rank = rank;
+        this._totalRank = totalRank;
+        this._score = score;
+    }
+    private set_host(isHost: boolean, ownHost: boolean): void {
+        this._ishost = isHost;
+        this._canAI = this.isAI && ownHost;
+    }
+
+    private change_online(ol: boolean): void {
+        this._online = ol;
+        (this.evt as any)._emit('online', this._online);
+    }
+    private change_ready(ready: boolean): void {
+        this._ready = ready;
+        (this.evt as any)._emit('ready', this._ready);
+    }
+    /**修改某个键的值*/
+    public setValue<T extends StartEventNames<GD, 'p_'>, T2 extends EventParamFrist<GD, T>>(key: T, data: T2): void {
+        if (!this.isPermission) return;
+        let old = super.getValue(key);
+        if (old) {
+            if (typeof old === "object") {
+                //old = JSON.parse(JSON.stringify(old));
+            } else if (data === old) {
+                return;
+            }
+        }
+        super.setValue(key, data);
+        super.setValueDirty(key, data);
+        (this.evt as any)._emit(key, data, old);
+    }
+    /**获取数据的值*/
+    public getValue<T extends StartEventNames<GD, 'p_'>, T2 extends EventParamFrist<GD, T>>(key: T): T2 {
+        return super.getValue(key);
+    }
+    //服务器发送过来的数据
+    private server_change(data: { [key: string]: any }): void {
+        if (this.isPermission) return;
+        Object.keys(data).forEach(key => {
+            const old = super.getValue(key);
+            super.setValue(key, data[key]);
+            (this.evt as any)._emit(key, data[key], old);
+        });
+    }
+    private setFinish(rank: number): void {
+        this._rank = rank;
+        (this.evt as any)._emit('finish', this._rank);
+    }
+    private _finsh_tag: boolean = false;
+    /**玩家完成游戏 不是自己或主机没有权限*/
+    public finishGame(): void {
+        if (!this.isPermission) return;
+        this._finsh_tag = true;
+    }
+    private doFinishGame(f: (id: string) => void): void {
+        if (this._finsh_tag) f(this.Id);
+        this._finsh_tag = false;
+    }
+    public dispose(): void {
+        super.dispose();
+        this.evt.clearAll();
+    }
+}

+ 1 - 0
idiom/assets/ch/net/NetPlayer.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"6fc05173-7c63-429c-9b1a-dde11cb30f78","files":[],"subMetas":{},"userData":{}}

+ 374 - 0
idiom/assets/ch/net/NetRoom.ts

@@ -0,0 +1,374 @@
+import { NetBase } from "./NetBase";
+import { NetPlayer } from "./NetPlayer";
+type EventNames<Map extends chsdk.OmitIndex<chsdk.EventsMap>> = Extract<keyof Map, string>;
+type EventParams<Map extends chsdk.OmitIndex<chsdk.EventsMap>, Ev extends EventNames<Map>> = Ev extends keyof Map ? Map[Ev] extends (...args: any[]) => any ? Parameters<Map[Ev]> : never : never;
+type StartEventNames<Map extends chsdk.OmitIndex<chsdk.EventsMap>, Prefix extends string> = Extract<keyof Map, string> extends infer K ? K extends `${Prefix}${string}` ? K : never : never;
+type EventParamFrist<Map extends chsdk.OmitIndex<chsdk.EventsMap>, Ev extends EventNames<Map>> = Ev extends keyof Map
+    ? Map[Ev] extends (...args: infer Params) => any ? Params extends [infer First, ...any[]] ? First : never : never : never;
+type PickEvent<T, Prefix1 extends string, Prefix2 extends string, Prefix3 extends string> = Pick<T, Extract<keyof T, `${Prefix1}${string}` | `${Prefix2}${string}` | `${Prefix3}${string}`>>;
+/**网络房间*/
+export class NetRoom<GD> extends NetBase {
+    public readonly evt = chsdk.get_new_event<PickEvent<GD, 'r_', 'r_obj', 'revt_'>>();
+    private _hostId: string;
+    /**游戏主机ID*/public get HostId(): string { return this._hostId };
+    private _status: boolean = true;
+    /**游戏是否关闭*/public get cloosed(): boolean { return !this._status };
+    public _own_id: string;
+    private _players: Map<string, NetPlayer<GD>> = new Map();
+    //
+    private _waitSends_mode0: Array<{ type: string, data: any }> = [];
+    private _waitSends_mode1: Array<{ type: string, data: any }> = [];
+    private _waitSends_mode2: Array<{ type: string, data: any }> = [];
+    //
+    constructor(roomData: any, playerData: any) {
+        super(roomData.roomId);
+        this._status = roomData.status;
+        this._hostId = roomData.hostInfo;
+        let ps = Object.keys(playerData).map(key => ({ key, data: playerData[key] }));
+        for (let i = 0; i < ps.length; i++) this.addPlayer(ps[i]);
+        this._players.forEach((v, _) => { (v as any).set_host(v.Id === this._hostId, this.isHost); });
+        this.initValue(roomData.gameData);
+    }
+    private addPlayer(p: any): void {
+        const id = p.key;
+        const pd = p.data;
+        let player = this._players.get(id);
+        if (!player) {
+            player = new NetPlayer<GD>(id);
+            this._players.set(id, player);
+        }
+        player.init(pd);
+        if (player.isOwn) this._own_id = id;
+    }
+    private updatePlayerStatus(id: string, online: boolean): void {
+        const p = this.getPlayer(id);
+        if (p) {
+            (p as any).change_online(online);
+            const location = p.location;
+            const nickName = p.nickName;
+            (this.evt as any)._emit('r_online', id, online, location, nickName);
+        }
+    }
+    private changeHost(id: string): void {
+        this._hostId = id;
+        this._players.forEach((v, _) => { (v as any).set_host(v.Id === this._hostId, this.isHost); });
+        const p = this.getPlayer(this._hostId);
+        (this.evt as any)._emit('r_host', id, p.location, p.nickName);
+    }
+    /**自己是否是主机*/
+    public get isHost(): boolean {
+        return this._own_id === this._hostId;
+    }
+    /**自己的所有信息*/
+    public get own(): NetPlayer<GD> {
+        return this._players.get(this._own_id);
+    }
+    /**其它玩家信息*/
+    public get others(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => !player.isOwn);
+    }
+    /**所有ai信息*/
+    public get ais(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => player.isAI);
+    }
+    /**所有玩家*/
+    public get all(): NetPlayer<GD>[] {
+        return this.getAllPlayer();
+    }
+    /**在线玩家信息*/
+    public get onlines(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => player.online);
+    }
+    /**不在线玩家信息*/
+    public get outlines(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => !player.online);
+    }
+    /**所有玩家*/
+    public getAllPlayer(): NetPlayer<GD>[] {
+        return Array.from(this._players.values());
+    }
+    /**将玩家按 location 放到一个数组中,并自定义数组长度*/
+    public getAllPlayersAtLocations(customLength: number): (NetPlayer<GD> | null)[] {
+        const locationArray: (NetPlayer<GD> | null)[] = Array(customLength).fill(null);
+        this._players.forEach((player) => {
+            if (player.location >= 0 && player.location < customLength) locationArray[player.location] = player;
+        });
+        return locationArray;
+    }
+    /**中某个玩家的所有信息*/
+    public getPlayer(id: string): NetPlayer<GD> {
+        return this._players.get(id);
+    }
+    /**获取除了某个id的所有玩家*/
+    public getExPlayer(id: string): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => player.Id != id);
+    }
+    /**获在线或不在线的所有玩家*/
+    public getOnlinePlayer(isOnline: boolean): NetPlayer<GD>[] {
+        return isOnline ? this.onlines : this.outlines;
+    }
+    /**是否有AI权限*/
+    public canAiPlayer(player: NetPlayer<GD>): boolean {
+        if (!this._status) return false;
+        if (!this.isHost) return false;
+        if (!player || !player.isAI) return false;
+        return true;
+    }
+    /**是否有AI权限Id,返回可以控制的player*/
+    public canAiPlayerId(id: string): NetPlayer<GD> | null {
+        if (!this._status) return null;
+        if (!this.isHost) return null;
+        const player = this._players.get(id);
+        if (!player) return null;
+        if (!player.isAI) return null;
+        return player;
+    }
+    /**是否拥有权限(包括,自己和自己是主机时的ai)*/
+    public hasPermission(player: NetPlayer<GD>): boolean {
+        return player.isOwn || this.canAiPlayer(player);
+    }
+    /**创建obj数据*/
+    public creatObj<T extends { [key: string]: any }>(data: T): string {
+        if (!this._status) return;
+        if (!this.isHost) return;
+        let oid = super.getValue('oid') ?? 0;
+        oid++;
+        const key = 'obj_' + oid;
+        super.setValue('oid', oid);
+        super.setValueDirty('oid', oid);
+        super.setValue(key, data);
+        super.setValueDirty(key, data);
+        (this.evt as any)._emit('r_obj', key, data);
+        return key;
+    }
+    /**删除obj数据*/
+    public getObj<T extends { [key: string]: any }>(key: string): T {
+        return super.getValue(key);
+    }
+    /**不建议经常更改obj */
+    public changeObj<T extends { [key: string]: any }>(key: string, data: T): void {
+        if (!this.isHost) return;
+        let old = super.getValue(key);
+        if (old) {
+            super.setValue(key, data);
+            super.setValueDirty(key, data);
+            (this.evt as any)._emit('r_obj', key, data, old);
+        }
+    }
+    //
+    public deleteObj(key: string) {
+        if (!this.isHost) return;
+        super.setValue(key, null);
+        super.setValueDirty(key, null);
+    }
+    /**修改某个键的值*/
+    public setValue<T extends StartEventNames<GD, 'r_'>, T2 extends EventParamFrist<GD, T>>(key: T, data: T2): void {
+        if (!this._status) return;
+        if (!this.isHost) return;//不是主机没有权限
+        let old = super.getValue(key);
+        if (old) {
+            if (typeof data === "object") {
+                //old = JSON.parse(JSON.stringify(old));
+            } else if (data === old) {
+                return;
+            }
+        }
+        super.setValue(key, data);
+        super.setValueDirty(key, data);
+        (this.evt as any)._emit(key, data, old);
+    }
+    /**获取数据的值*/
+    public getValue<T extends StartEventNames<GD, 'r_'>, T2 extends EventParamFrist<GD, T>>(key: T): T2 {
+        return super.getValue(key);
+    }
+    private server_change(data: { [key: string]: any }): void {
+        if (!this._status) return;
+        if (this.isHost) return;
+        Object.keys(data).forEach(key => {
+            this.change(key, data[key]);
+        });
+    }
+    private change(key: string, data: any) {
+        const old = super.getValue(key);
+        super.setValue(key, data);
+        if (typeof data !== "object" && data === old) return;
+        if (key === 'oid') {
+            return;
+        } else if (key.startsWith('obj_')) {
+            (this.evt as any)._emit('r_obj', key, data, old);
+        } else {
+            (this.evt as any)._emit(key, data, old);
+        }
+    }
+    private _results: any[] = [];
+    /**   
+ * 游戏结算数据  
+ *   
+ * @returns {Array} results - 包含游戏结算信息的数组。  
+ * 每个元素表示一个玩家的结算数据,结构如下:  
+ *   
+ * - Id: {string} 玩家唯一标识符  
+ * - AddScore: {number} 本局游戏中增加的星星数  
+ * - IsFinish: {boolean} 游戏是否已完成  
+ * - Rank: {number} 玩家在当前游戏中的排名  
+ * - Score: {number} 玩家在最后星星数 
+ * - TotalRank: {number} 玩家的总排名  
+ * - Level: {Object} 玩家当前等级的信息  
+ *   - LowerRank: {number} 当前段位的等级  
+ *   - Rank: {number} 段位 
+ *   - Star: {number} 当前段位的等级的星级  
+ * - UserData: {Object} 玩家个人信息  
+ *   - gid: {string} 游戏全局唯一标识符  
+ *   - head: {string} 玩家头像的 URL  
+ *   - hid: {number} 玩家省份id
+ *   - ip: {string} 玩家 IP 地址  
+ *   - loginTime: {number} 玩家登录时间的时间戳  
+ *   - nickName: {string} 玩家昵称  
+ *   - openId: {string} 玩家在平台上的唯一标识符  
+ *   - option: {string} 玩家选择的选项或设置
+ *   - pf: {string} 平台信息  
+ *   - registerTime: {number} 玩家注册时间的时间戳  
+ *   - userId: {number} 玩家在系统中的用户 ID
+ *   - province: {string} 玩家所在的省份
+ * - Elements: {Array} 包含其他玩家的结算数据  
+ *   - Rank: {number} 其他玩家的排名 
+ *   - Level: {Object} 其他玩家的等级信息  
+ *     - LowerRank: {number} 其他玩家当前段位的等级  
+ *     - Rank: {number} 其他玩家的段位 
+ *     - Star: {number} 其他玩家段位的等级的星级    
+ *   - UserData: {Object} 其他玩家的个人信息(与上面的 UserData 结构相同)  
+ */
+    public get results(): {
+        Id: string, AddScore: number, IsFinish: boolean, Rank: number, Score: number, TotalRank: number,
+        Level: { LowerRank: number, Rank: number, Star: number },
+        UserData: {
+            gid: string, head: string, hid: number, ip: string, loginTime: number,
+            nickName: string, openId: string, option: string, pf: string, registerTime: number, userId: number, province: string
+        },
+        Elements: {
+            Rank: number,
+            Level: { LowerRank: number, Rank: number, Star: number },
+            UserData: {
+                gid: string, head: string, hid: number, ip: string, loginTime: number,
+                nickName: string, openId: string, option: string, pf: string, registerTime: number, userId: number, province: string
+            }
+        }[],
+    }[] {
+        return this._results;
+    }
+    /**获取自己本局获得星星*/
+    public get own_results_addScore(): number {
+        const rs = this.results;
+        if (!rs) return 0;
+        for (let i = 0; i < rs.length; i++) {
+            if (rs[i].Id === this._own_id) {
+                return rs[i].AddScore;
+            }
+        }
+        return 0;
+    }
+    private closed(data: any): void {
+        this._status = false;
+        this._results.length = 0;
+        const ps = Object.keys(data).map(key => ({ key, data: data[key] }));
+        for (let i = 0; i < ps.length; i++) {
+            const key = ps[i].key;
+            const user = ps[i].data;
+            user.Id = key;
+            const player = this.getPlayer(key);
+            (player as any).set_level(user.Level, user.Rank, user.Score, user.TotalRank);
+            user.UserData = {
+                gid: player.gid, head: player.head, hid: player.hid, ip: player.ip, loginTime: player.loginTime,
+                nickName: player.nickName, openId: player.openId, option: player.option, pf: player.pf, registerTime: player.registerTime, userId: player.userId, province: player.province
+            };
+            const elements = user.Elements;
+            if (elements) {
+                for (let i = 0; i < elements.length; i++) {
+                    const o = elements[i];
+                    o.nickName = o.nickName || '玩家' + o.userId
+                    o.hid = Number.parseInt(o.hid);
+                    o.loginTime = Number.parseInt(o.loginTime);
+                    o.registerTime = Number.parseInt(o.registerTime);
+                    o.userId = Number.parseInt(o.userId);
+                    o.province = chsdk.provinceCode2Name(o.hid) ?? '其它';
+                }
+            }
+            this._results.push(user);
+        };
+        if (this._results.length > 1) {
+            this._results.sort((a, b) => {
+                if (a.Rank === 0 && b.Rank === 0) {
+                    return 0;
+                } else if (a.Rank === 0) {
+                    return 1;
+                } else if (b.Rank === 0) {
+                    return -1;
+                } else {
+                    return a.Rank - b.Rank;
+                }
+            });
+        }
+        //
+        (this.evt as any)._emit('r_closed');
+    }
+    private finish(pid: string, rank: number): void {
+        const player = this.getPlayer(pid);
+        if (!player) return;
+        (player as any).setFinish(rank);
+        (this.evt as any)._emit('r_finish', pid, rank);
+    }
+    /**向房间所有玩家发消息*/
+    public sendEvt<T extends StartEventNames<GD, 'revt_'>, T2 extends EventParams<GD, T>>(key: T, ...data: T2): void {
+        if (!this._status) return;
+        this._waitSends_mode0.push({ type: key, data: data });
+    }
+    /**向房间主机发消息*/
+    public sendToHost<T extends StartEventNames<GD, 'revt_'>, T2 extends EventParams<GD, T>>(key: T, ...data: T2): void {
+        if (!this._status) return;
+        this._waitSends_mode1.push({ type: key, data: data });
+    }
+    /**向房间其它玩家*/
+    public sendToOther<T extends StartEventNames<GD, 'revt_'>, T2 extends EventParams<GD, T>>(key: T, ...data: T2): void {
+        if (!this._status) return;
+        this._waitSends_mode2.push({ type: key, data: data });
+    }
+    /**处理发送事件*/
+    private doSendMode(f: (mode0: { type: string, data: any }[], mode1: { type: string, data: any }[], mode2: { type: string, data: any }[]) => void): void {
+        f(this._waitSends_mode0, this._waitSends_mode1, this._waitSends_mode2);
+        this._waitSends_mode0.length = 0;
+        this._waitSends_mode1.length = 0;
+        this._waitSends_mode2.length = 0;
+    }
+    private _chat_msg: string | null = null;
+    private _last_chat_time: number = 0;
+    /**房间里聊天 ,成功返回true,发送频率过快返回false*/
+    public chat(msg: string): boolean {
+        const now = chsdk.date.now();
+        if (now - this._last_chat_time < 1000) return false;
+        this._last_chat_time = now;
+        this._chat_msg = msg;
+        return true;
+    }
+    private onChat(id: string, msg: string): void {
+        const p = this.getPlayer(id);
+        (this.evt as any)._emit('r_chat', id, msg, p.location, p.nickName);
+    }
+    private doSendChat(f: (msg: string) => void) {
+        f(this._chat_msg);
+        this._chat_msg = null;
+    }
+    //
+    private _onEvt(key: string, data: any): void {
+        (this.evt as any)._emit(key, ...data);
+    }
+    //
+    public dispose(): void {
+        super.dispose();
+        this._players.forEach((v, _) => {
+            v.dispose();
+        });
+        this._players.clear();
+        this._status = false;
+        this.evt.clearAll();
+    }
+}

+ 1 - 0
idiom/assets/ch/net/NetRoom.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"577dc82a-0c71-4d81-99ac-eb9579469af3","files":[],"subMetas":{},"userData":{}}

+ 243 - 0
idiom/assets/ch/net/NetTeam.ts

@@ -0,0 +1,243 @@
+import { NetPlayer } from "./NetPlayer";
+type PickEvent<T, Prefix extends string> = Pick<T, Extract<keyof T, `${Prefix}${string}`>>;
+const C2S_MATCH = 'c2s_match';
+const C2S_MATCH_CANCEL = 'c2s_match_cancel';
+const C2S_READY = 'c2s_ready';
+const C2S_KICK_OUT = 'c2s_kick_out';
+/**网络队伍*/
+export class NetTeam<GD> {
+    /**队伍事件*/
+    public readonly evt = chsdk.get_new_event<PickEvent<GD, 't_'>>();
+    private _status: boolean = true;
+    /**队伍是否能进*/public get status(): boolean { return this._status };
+    private _hostId: string;
+    /**队伍队长ID*/public get HostId(): string { return this._hostId };
+    private _password: string;
+    /**队伍进入口令*/public get Password(): string { return this._password };
+    private _limit: number;
+    /**队伍上限*/public get Limit(): number { return this._limit };
+    private _id: string;
+    /**队伍标识id*/public get Id(): string { return this._id };
+    private _inCollect: boolean = false;
+    /**是否在匹配中*/public get inCollect(): boolean { return this._inCollect };
+    private _own_id: string;
+    private _players: Map<string, NetPlayer<GD>> = new Map();
+    private _send: (type: string, data?: any) => void;
+    constructor(data: any, send: (type: string, data?: any) => void) {
+        const team = data.teamData;
+        const playerData = data.playerData;
+        this._hostId = team.hostInfo;
+        this._status = team.status;
+        this._inCollect = team.inCollect;
+        this._password = team.password;
+        this._limit = team.limit;
+        this._id = team.roomId;
+        this._players.clear();
+        let ps = Object.keys(playerData).map(key => ({ key, data: playerData[key] }));
+        for (let i = 0; i < ps.length; i++) this.addPlayer(ps[i]);
+        this._send = send;
+    }
+    /**主机开始匹配*/
+    public match(): void {
+        if (!this.isHost) return;
+        this._send(C2S_MATCH);
+    }
+    /**主机退出匹配*/
+    public exit_match(): void {
+        if (!this.isHost) return;
+        this._send(C2S_MATCH_CANCEL);
+    }
+    /**主机踢出某个玩家出小队 */
+    public kick_out(location: number): void;
+    public kick_out(id: string): void;
+    public kick_out(arg: string | number): void {
+        if (!this.isHost) return;
+        if (typeof arg === 'string') {
+            this._send(C2S_KICK_OUT, arg);
+        } else if (typeof arg === 'number') {
+            const playerToKick = this.all.find(player => player.location === arg);
+            if (playerToKick) this._send(C2S_KICK_OUT, playerToKick.Id); // 踢出找到的玩家  
+        }
+    }
+    /**主机踢出所有没有准备的玩家 */
+    public kick_out_no_readys(): void {
+        if (!this.isHost) return;
+        const list = this.notreadys;
+        for (let i = 0; i < list.length; i++) this._send(C2S_KICK_OUT, list[i].Id);
+    }
+    /**主机踢出所有不在线玩家*/
+    public kick_out_outlines(): void {
+        if (!this.isHost) return;
+        const list = this.outlines;
+        for (let i = 0; i < list.length; i++) this._send(C2S_KICK_OUT, list[i].Id);
+    }
+    /**主机踢出所有不在线或没有准备的玩家*/
+    public kick_out_badplayers(): void {
+        if (!this.isHost) return;
+        const list = this.badplayers;
+        for (let i = 0; i < list.length; i++) this._send(C2S_KICK_OUT, list[i].Id);
+    }
+    /**准备*/
+    public ready(): void {
+        this._send(C2S_READY);
+    }
+    private addPlayer(p: any, isevt: boolean = false): void {
+        const id = p.key;
+        const pd = p.data;
+        let player = this._players.get(id);
+        if (!player) {
+            player = new NetPlayer<GD>(id);
+            this._players.set(id, player);
+        }
+        player.init(pd);
+        (player as any).set_host(player.Id === this._hostId);
+        if (player.isOwn) this._own_id = id;
+        if (isevt) (this.evt as any)._emit('t_entry', id, player.location, player.nickName);
+    }
+    /**玩家离开*/
+    private removePlayer(id: string): void {
+        const p = this._players.get(id)
+        if (p) {
+            const location = p.location;
+            const nickName = p.nickName;
+            this._players.delete(id);
+            (this.evt as any)._emit('t_exit', id, location, nickName);
+        }
+    }
+    /**队伍解散*/
+    private closed(): void {
+        (this.evt as any)._emit('t_closed');
+    }
+    private updatePlayerStatus(id: string, online: boolean): void {
+        const p = this.getPlayer(id);
+        if (p) {
+            (p as any).change_online(online);
+            const location = p.location;
+            const nickName = p.nickName;
+            (this.evt as any)._emit('t_online', id, online, location, nickName);
+        }
+    }
+    private updatePlayerReady(id: string, ready: boolean): void {
+        const p = this.getPlayer(id);
+        if (p) {
+            (p as any).change_ready(ready);
+            const location = p.location;
+            const nickName = p.nickName;
+            (this.evt as any)._emit('t_ready', id, ready, location, nickName);
+        }
+    }
+    private match_start(): void {
+        this._inCollect = true;
+        (this.evt as any)._emit('t_matchStart');
+    }
+    private match_cancel(): void {
+        this._inCollect = false;
+        (this.evt as any)._emit('t_matchCancel');
+    }
+    private match_success(): void {
+        this._inCollect = false;
+        (this.evt as any)._emit('t_matchSuccess');
+        const ps = this.getAllPlayer();
+        for (let i = 0; i < ps.length; i++) {
+            (ps[i] as any).change_ready(false);
+        }
+    }
+    private not_ready(): void {
+        (this.evt as any)._emit('t_no_ready');
+    }
+    private changeHost(id: string): void {
+        this._hostId = id;
+        this._players.forEach((v, _) => { (v as any).set_host(v.Id === this._hostId); });
+        const p = this.getPlayer(this._hostId);
+        (this.evt as any)._emit('t_host', id, p.location, p.nickName);
+    }
+    private _chat_msg: string | null = null;
+    private _last_chat_time: number = 0;
+    /*队伍里聊天,成功返回true,发送频率过快返回false*/
+    public chat(msg: string): boolean {
+        const now = chsdk.date.now();
+        if (now - this._last_chat_time < 1000) return false;
+        this._last_chat_time = now;
+        this._chat_msg = msg;
+        return true;
+    }
+    private onChat(id: string, msg: string): void {
+        const p = this.getPlayer(id);
+        (this.evt as any)._emit('t_chat', id, msg, p.location, p.nickName);
+    }
+    private doSendChat(f: (msg: string) => void) {
+        f(this._chat_msg);
+        this._chat_msg = null;
+    }
+    /**自己是否是主机*/
+    get isHost(): boolean {
+        return this._own_id === this._hostId;
+    }
+    /**自己的所有信息*/
+    public get own(): NetPlayer<GD> {
+        return this._players.get(this._own_id);
+    }
+    /**所有玩家*/
+    public get all(): NetPlayer<GD>[] {
+        return Array.from(this._players.values());
+    }
+    /**其它玩家信息*/
+    public get others(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => !player.isOwn);;
+    }
+    /**在线玩家信息*/
+    public get onlines(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => player.online);;
+    }
+    /**不在线玩家信息*/
+    public get outlines(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => !player.online);;
+    }
+    /**没有准备或不在线玩家*/
+    public get badplayers(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => !player.ready || !player.online);;
+    }
+    /**准备好的玩家信息*/
+    public get readys(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => player.ready);;
+    }
+    /**没有准备的玩家信息*/
+    public get notreadys(): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => !player.ready);;
+    }
+    /**某个玩家的所有信息*/
+    public getPlayer(id: string): NetPlayer<GD> {
+        return this._players.get(id);
+    }
+    /**所有玩家*/
+    public getAllPlayer(): NetPlayer<GD>[] {
+        return this.all;
+    }
+    /**将玩家按 location 放到一个数组中,并自定义数组长度*/
+    public getAllPlayersAtLocations(customLength: number): (NetPlayer<GD> | null)[] {
+        const locationArray: (NetPlayer<GD> | null)[] = Array(customLength).fill(null);
+        this._players.forEach((player) => {
+            if (player.location >= 0 && player.location < customLength) locationArray[player.location] = player;
+        });
+        return locationArray;
+    }
+    /**获取除了某个id的所有玩家*/
+    public getExPlayer(id: string): NetPlayer<GD>[] {
+        return Array.from(this._players.values()).filter(player => player.Id != id);;
+    }
+    /**获在线或不在线的所有玩家*/
+    public getOnlinePlayer(isOnline: boolean): NetPlayer<GD>[] {
+        return isOnline ? this.onlines : this.outlines;
+    }
+    /**获准备或没有准备的所有玩家*/
+    public getReadyPlayer(ready: boolean): NetPlayer<GD>[] {
+        return ready ? this.readys : this.notreadys;
+    }
+    public dispose(): void {
+        this._send = null;
+        this._players.forEach((v, _) => {
+            v.dispose();
+        });
+        this._players.clear();
+    }
+}

+ 1 - 0
idiom/assets/ch/net/NetTeam.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"33cae0ae-d426-493d-a083-da5acb9f81af","files":[],"subMetas":{},"userData":{}}

+ 99 - 0
idiom/assets/ch/net/WsClient.ts

@@ -0,0 +1,99 @@
+
+/**web scoket 客户端*/
+export class WsClient {
+    public onConnected(evt: any): void { };
+    public onError(err: any): void { };
+    public onClosing(): void { };
+    public onClosed(event: CloseEvent): void { };
+    public onMessage(msg: MessageEvent): void { };
+    //
+    private _ws: WebSocket | null = null;
+    /** WebSocket对象*/get ws() { return this._ws };
+    /** 连接地址*/
+    private _url: string | null = null;
+    /**证书*/
+    private _ca?: string;
+    constructor(url: string, ca?: string, autoConnect?: boolean) {
+        this._ca = ca;
+        this._url = url;
+        if (autoConnect) this.connect();
+    }
+    /**连接成功时的回调 */
+    private _onConnected(evt: any) {
+        this.onConnected(evt);
+    }
+    /**收到消息时的回调 */
+    private _onMessage(msg: MessageEvent) {
+        this.onMessage(msg);
+    }
+    /** 错误处理回调 */
+    private _onError(err: any) {
+        this.onError(err);
+    }
+    /**连接关闭时的回调 */
+    private _onClosed(event: CloseEvent) {
+        this.onClosed(event);
+    }
+    /**
+     * 获取当前连接状态
+     * @returns 是否处于活动状态
+     */
+    get isActive(): boolean {
+        return this._ws?.readyState === WebSocket.OPEN;
+    }
+    /**
+     * 检查是否正在连接
+     * @returns 是否正在连接
+     */
+    private get isConnecting(): boolean {
+        return this._ws?.readyState === WebSocket.CONNECTING;
+    }
+    /**
+     *是否正在关闭
+     */
+    private get isClosing(): boolean {
+        return this._ws?.readyState === WebSocket.CLOSING;
+    }
+    /**是否已经关闭 */
+    private get isClosed(): boolean {
+        return this._ws?.readyState === WebSocket.CLOSED;
+    }
+    /**手动连接*/
+    connect() {
+        if (this.isConnecting) return false;
+        if (this.isActive) return false;
+        const url = this._url;
+        if (!url) return false;
+        try {
+            //eslint-disable-next-line @typescript-eslint/ban-ts-comment
+            //@ts-ignore
+            //eslint-disable-next-line @typescript-eslint/no-unsafe-call
+            const ws = this._ca && url.startsWith('wss://') ? new WebSocket(url, {}, this._ca) : new WebSocket(url)
+            ws.binaryType = 'arraybuffer';
+            ws.onmessage = this._onMessage.bind(this);
+            ws.onopen = this._onConnected.bind(this);
+            ws.onerror = this._onError.bind(this);
+            ws.onclose = this._onClosed.bind(this);
+            this._ws = ws;
+        } catch (error) {
+            this._onError(error instanceof Error ? error.message : 'Unknown error');
+        }
+        return true;
+    }
+    /**
+     * 主动关闭WebSocket连接
+     */
+    close(code?: number, reason?: string): void {
+        if (this.isClosed || this.isClosing) return;
+        this.onClosing();
+        this._ws.close(code, reason);
+    }
+    /**
+    * 发送数据
+    * @param data 指定格式数据
+    */
+    send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
+        this._ws.send(data);
+    }
+}
+

+ 9 - 0
idiom/assets/ch/net/WsClient.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "d1418e93-2c2a-4321-ada7-99ffcf5a4a7d",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/ch/net/modules.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "d8261c29-65fe-4518-8959-dd668a9b1cc2",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 4 - 0
idiom/assets/ch/net/modules/msgpack.min.d.ts

@@ -0,0 +1,4 @@
+declare namespace msgpack {
+    export function encode(any): ArrayBuffer
+    export function decode(res: ArrayBuffer): Object
+}

+ 9 - 0
idiom/assets/ch/net/modules/msgpack.min.d.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "078ded11-0a9e-4ee8-b292-c033c9eef840",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
idiom/assets/ch/net/modules/msgpack.min.js


+ 17 - 0
idiom/assets/ch/net/modules/msgpack.min.js.meta

@@ -0,0 +1,17 @@
+{
+  "ver": "4.0.24",
+  "importer": "javascript",
+  "imported": true,
+  "uuid": "7014d4ed-21a3-40c7-a08a-21f69b928aa1",
+  "files": [
+    ".js"
+  ],
+  "subMetas": {},
+  "userData": {
+    "loadPluginInEditor": false,
+    "loadPluginInWeb": true,
+    "loadPluginInNative": true,
+    "loadPluginInMiniGame": true,
+    "isPlugin": true
+  }
+}

+ 582 - 0
idiom/assets/ch/net/net.ts

@@ -0,0 +1,582 @@
+import { NetRoom } from "./NetRoom";
+import { NetTeam } from "./NetTeam";
+import { WsClient } from "./WsClient";
+/**自定义同步游戏变量(只能以r_或p_开头,按帧率同步,重新进入会收到最新的数据)
+ * 只有第一个参数有效,可加上第一个参数跟第一个参数同类型,用于接收改变前的数据
+ * t_开头为队伍事件
+ * r_开头 主机游戏数据 主机权限
+ * revt_ 开头房间事件
+ * p_ 开头玩家数据 玩家权限和主机ai权限
+ * online 玩家在线下线状态
+ * 尽量拆分数据类型,不要设置过于复杂的数据类型*/
+export interface game_protocol extends chsdk.OmitIndex<chsdk.EventsMap> {
+    t_entry(id: string, location: number, nickName: string): void;//队伍有人进入
+    t_exit(id: string, location: number, nickName: string): void;//队伍有人退出
+    t_host(id: string, location: number, nickName: string): void;//队长切换 
+    t_chat(id: string, msg: string, location: number, nickName: string): void;//队伍聊天
+    t_online(id: string, online: boolean, location: number, nickName: string): void;//队伍玩家在线情况变化
+    t_ready(id: string, online: boolean, location: number, nickName: string): void;//队伍玩家准备情况变化
+    t_matchStart(): void;//匹配开始
+    t_matchCancel(): void;//匹配取消
+    t_matchSuccess(): void;//匹配成功
+    t_no_ready(): void;//有玩家未准备
+    t_closed(): void;//队伍解散(一般用于被踢出事件)
+    //
+    r_obj(key: string, data: any, old: any): void;
+    r_closed(): void;//房间关闭 关闭后 获取 results 结算信息
+    r_host(id: string, location: number, nickName: string): void;//房主切换
+    r_chat(id: string, msg: string, location: number, nickName: string): void;//房间聊天
+    r_online(id: string, online: boolean, location: number, nickName: string): void;//房间玩家在线情况变化
+    r_finish(id: string, rank: number): void//房间某个玩家完成游戏获得名次
+    r_state(state: number, old_state?: number): void;//房间游戏状态
+    p_state(state: number, old_state?: number): void;//玩家游戏状态
+    finish(rank: number): void;//玩家完成游戏获得名次
+    online(online: boolean): void//玩家断线重连消息
+}
+/**网络状态*/
+interface ws_event_protocol {
+    Connecting(): void;//连接中
+    Reconnecting(reconnectCount: number): void;//重新连接中
+    Connected(): void;//进入大厅连接成功
+    Reconnected(): void;//重连成功
+    Error(err: any | string): void;//错误消息
+    Closing(): void;//关闭连接中
+    Closed(event: CloseEvent): void;//连接已关闭
+}
+function isArrayBuffer(it: unknown): it is ArrayBuffer {
+    try {
+        return it instanceof ArrayBuffer
+    } catch (_) {
+        return false
+    }
+}
+const HEARTBEAT_INTERVAL = 1000; //每秒发送一次 Ping  
+const HEARTBEAT_TIMEOUT = 30000; //30 秒的断线超时
+const DIRTYDATACHECK_INTERVAL = 200;//每200毫秒同步一次自定义游戏变量
+const EVTCHECK_INTERVAL = 66;//每66毫秒同步一次自定义游戏事件
+const RECONNECT_COUNT = 10;//默认重连次数
+const RECONNECT_INTERVAL = 2000;//默认重连间隔
+const PING = 'ping';
+const PONG = 'pong';
+//
+const S2C_LOGIN_SUCCESS = 's2c_login_success';
+const S2C_TEAM_SUCCESS = 's2c_team_success';
+const S2C_JOIN_TEAM_FAIL = 's2c_join_team_fail';
+const S2C_TEAM_USER_JOIN = 's2c_team_user_join';
+const S2C_TEAM_USER_EXIT = 's2c_team_user_exit';
+const S2C_TEAM_CHANGE_HOST = 's2c_team_change_host';
+const S2C_USER_RECONNECT = 's2c_user_reconnect';
+const S2C_USER_DISCONNECT = 's2c_user_disconnect';
+const S2C_MATCH = 's2c_match';
+const S2C_MATCH_SUCCESS = 's2c_match_success';
+const S2C_MATCH_CANCEL = 's2c_match_cancel';
+const S2C_ROOM_GAMEDATA_CHANGE = 's2c_room_gameData_change';
+const S2C_USER_GAMEDATA_CHANGE = 's2c_user_gameData_change';
+const S2C_ROOM_CHANGE_HOST = 's2c_room_change_host';
+const S2C_ROOM_CLOSED = 's2c_room_closed';
+const S2C_TALK = 's2c_talk';
+const S2C_FINISH = 's2c_finish';
+const S2C_SETTLEMENT = 's2c_settlement';
+const S2C_READY = 's2c_ready';
+const S2C_NOT_READY = 's2c_not_ready';
+//
+const C2S_UPDATE_USER_GAMEDATA = 'c2s_update_user_gameData';
+const C2S_UPDATE_ROOM_GAMEDATA = 'c2s_update_room_gameData';
+const C2S_SET_TEAM = 'c2s_set_team';
+const C2S_JOIN_TEAM = 'c2s_join_team';
+const C2S_EXIT_TEAM = 'c2s_exit_team';
+
+const C2S_CLOSE_ROOM = 'c2s_close_room';
+const C2S_ROOM_EXIT = 'c2s_room_exit';
+const C2S_MESSAGE = 'c2s_message';
+const C2S_MESSAGE_TO_HOST = 'c2s_message_to_host';
+const C2S_MESSAGE_TO_OTHER = 'c2s_message_without_self';
+const C2S_TALK = 'c2s_talk';
+const C2S_BACKED_RETURN = 'c2s_backed_return';
+const C2S_BACKED = 'c2s_to_backed';
+const C2S_FINISH = 'c2s_finish';
+//
+const Ver: string = '0.1.8';
+/**客户端*/
+export class ch_net<GD extends game_protocol = game_protocol> {
+    /**连接状态事件*/
+    public state = chsdk.get_new_event<ws_event_protocol>();
+    private _ws: WsClient | null = null;
+    private _url?: string | null;
+    private _ca?: string;
+    private reconnection: boolean;
+    private reconnectCount: number;
+    private reconnectInterval: number;
+    private timeoutInterval: number;
+    private gameDataInterval: number;
+    private gameEvtInterval: number;
+    private timeout: any | null = null;
+    private json: boolean = false;
+    private heartbeatInterval: any | null = null; // 心跳定时器 
+    private dirtyDataCheckInterval: any | null = null; //变量同步定时器 
+    private evtCheckInterval: any | null = null; //事件同步定时器 
+    private _currentReconnectCount: number = 0;
+    private _pingTime: number = 0;
+    private _ping: number = 0;//ping值
+    private _callbacks: { [id: string]: (result: any) => void } = {};
+    private _waitSends: Array<string | ArrayBufferLike | Blob | ArrayBufferView> = [];
+    private _new_resolve<T>(id: string): Promise<T> { return new Promise(resolve => this._callbacks[id] = resolve); }
+    private _lv: { LowerRank: number, Rank: number, Star: number } = { LowerRank: 0, Rank: 0, Star: 0 };
+    public get ownLevel(): { LowerRank: number, Rank: number, Star: number } { return this._lv };
+    private _do_resolve<T>(id: string, data: T): boolean {
+        const resolveCallback = this._callbacks[id];
+        if (!resolveCallback) return false;
+        resolveCallback(data);
+        delete this._callbacks[id];
+        return true;
+    }
+    private _team: NetTeam<GD> | null;
+    private _room: NetRoom<GD> | null;
+    public get dataInterval(): number { return this.gameDataInterval * 0.001 };
+    public get evtInterval(): number { return this.gameEvtInterval * 0.001 };
+    public get isActive(): boolean { return this._ws && this._ws.isActive };
+    /**队伍信息*/
+    public get team(): NetTeam<GD> {
+        return this._team;
+    }
+    /**是否在队伍中*/
+    public get inTeam(): boolean {
+        return this._team != null && this._team.own != null;
+    }
+    /**房间信息*/
+    public get room(): NetRoom<GD> {
+        return this._room;
+    }
+    /**是否在房间中*/
+    public get inRoom(): boolean {
+        return this._room != null && !this._room.cloosed;
+    }
+    /**
+    * 创建一个客户端
+    */
+    constructor() {
+        this._waitSends = [];
+        this._callbacks = {};
+    }
+    /**初始化
+    * @param url - 连接的地址 'ws://localhost:3000'
+    * @param options - 可选的配置选项对象,支持以下属性:
+    *  - **ca**: (可选) CA 证书字符串,用于 SSL 连接的验证。  
+    *  - **reconnection**: (可选) 指示是否启用重连机制,默认为 `true`。  
+    *  - **reconnectCount**: (可选) 在连接失败后重连的最大次数,默认值为 `10`。  
+    *  - **reconnectInterval**: (可选) 每次重连之间的间隔时间,以毫秒为单位,默认值为 `2000` (2 秒)。  
+    *  - **timeoutInterval**: (可选) 心跳断线时间,以毫秒为单位,默认值为 `30000` (30 秒)。
+    * - **gameEvtInterval**:(可选) 游戏事件同步间隔,默认值为 `66` (0.06秒 1秒钟15次)。
+    *  - **gameDataInterval**:(可选) 游戏变量同步间隔,默认值为 `200` (0.2秒 1秒钟5次)。
+    *  - **json**: (可选) 是否用json序列化否则用messagepck 默认为 `false`。
+     * @example
+     * 自定义消息
+       interface message extends message_protocol {
+            useItem(userId: string, otherId: string, itemId: number): void//使用道具
+       }
+       自定义数据 
+       export interface gd extends game_data {
+          r_lv(lv: number);//关卡信息
+          r_time(time: number);//关卡时间
+       }
+       export const net = ch.get_new_net<gd, message>();
+       const url = chsdk.getUrl('/handle').replace(chsdk.getUrl('/handle').split(':')[0], 'ws');
+       const token = chsdk.getToken();
+       net.init(url,{query: `token=${token}`});
+     */
+    public init(url: string, options?: { query?: string, ca?: string, reconnection?: boolean, reconnectCount?: number, reconnectInterval?: number, timeoutInterval?: number, gameEvtInterval?: number, gameDataInterval?: number, json?: boolean }) {
+        this._url = url;
+        this._url = `${this._url}${options?.query?.length ? '?' + options.query : ''}`;
+        this._ca = options?.ca;
+        this.reconnection = options?.reconnection ?? true;
+        this.reconnectCount = options?.reconnectCount ?? RECONNECT_COUNT;
+        this.reconnectInterval = options?.reconnectInterval ?? RECONNECT_INTERVAL;
+        this.timeoutInterval = options?.timeoutInterval ?? HEARTBEAT_TIMEOUT;
+        this.gameDataInterval = options?.gameDataInterval ?? DIRTYDATACHECK_INTERVAL;
+        this.gameEvtInterval = options?.gameEvtInterval ?? EVTCHECK_INTERVAL;
+        this.json = options?.json ?? false;
+        this._ws = new WsClient(this._url, this._ca);
+        this._ws.onConnected = this._onConnected.bind(this);
+        this._ws.onError = this._onError.bind(this);
+        this._ws.onClosing = this._onClosing.bind(this);
+        this._ws.onClosed = this._onClosed.bind(this);
+        this._ws.onMessage = this._onMessage.bind(this);
+        const isbuffer = typeof ArrayBuffer !== 'undefined';
+        console.log('ch_net 初始化 ver:', Ver, isbuffer, this.json, this._url);
+        //
+        chsdk.sdk_event.on('hide', this.on_hide, this);
+        chsdk.sdk_event.on('show', this.on_show, this);
+    }
+    /**主动断开连接*/
+    public dispose(): void {
+        this.state.clearAll();
+        this._clear_team();
+        this._clear_room();
+        this._callbacks = {};
+        this._waitSends.length = 0;
+        this._ws.close(5000, 'client disconnect');
+        chsdk.sdk_event.off('hide', this.on_hide, this);
+        chsdk.sdk_event.off('show', this.on_show, this);
+    }
+    private on_hide(): void {
+        if (!this.inRoom) return;
+        this.send_data(C2S_BACKED);
+    }
+    private on_show(): void {
+        if (!this.inRoom) return;
+        this.send_data(C2S_BACKED_RETURN);
+    }
+    private encode(msg: any): any {
+        return this.json ? JSON.stringify(msg) : msgpack.encode(msg);
+    }
+    private decode(medata: any): any {
+        return isArrayBuffer(medata) ? msgpack.decode(new Uint8Array(medata)) : JSON.parse(medata);
+    }
+    private send_data(type: string, data?: any): void {
+        this._send(this.encode(data ? { type: type, data: data } : { type: type }));
+    }
+    /**开始连接,返回null为成功,否则为错误提示*/
+    public async connect(): Promise<string | null> {
+        if (!this._ws) {
+            chsdk.log.error('not init');
+            return 'not init';
+        }
+        if (!this._ws.connect()) return 'no netconnect';
+        this.state.emit(this.state.key.Connecting);
+        return this._new_resolve('connect');
+    }
+    /**主动重新连接*/
+    public reconnect(): string | null {
+        if (!this._ws) {
+            chsdk.log.error('not init');
+            return 'not init';
+        }
+        this._currentReconnectCount = 1;
+        this.do_reconnect();
+    }
+    private do_reconnect(): void {
+        if (!this._ws.connect()) return;
+        this.state.emit(this.state.key.Reconnecting, this._currentReconnectCount);
+    }
+    /**玩家创建队伍,返回null为成功,否则为错误提示*/
+    public async creat_team(player_count: number = 4): Promise<string | null> {
+        if (!this._ws) return 'no netconnect';
+        if (this.inTeam) return null;
+        this.send_data(C2S_SET_TEAM, player_count);
+        return this._new_resolve('creat_team');
+    }
+    /**玩家进入队伍,返回null为成功,否则为错误提示*/
+    public async join_team(password: string): Promise<string | null> {
+        if (!this._ws) return 'no netconnect';
+        if (this.inTeam) return null;
+        this.send_data(C2S_JOIN_TEAM, { password: password });
+        return this._new_resolve('join_team');
+    }
+    /**玩家退出队伍,返回null为成功,否则为错误提示*/
+    public async exit_team(): Promise<string | null> {
+        if (!this._ws) return 'no netconnect';
+        if (!this.inTeam) return 'not in team';
+        if (this.inRoom) return 'in game';
+        this.send_data(C2S_EXIT_TEAM);
+        return this._new_resolve('exit_team');
+    }
+    /**主机结束游戏关闭房间,成功后收到房间关闭事件*/
+    public close_room(): void {
+        if (!this._ws) return;
+        if (!this.inRoom) return;
+        if (!this.room.isHost) return;
+        this.send_data(C2S_CLOSE_ROOM);
+    }
+    /**玩家退出房间 ,返回null为成功,否则为错误提示,退出成功后收到房间关闭事件*/
+    public async exit_room(): Promise<string | null> {
+        if (!this._ws) return 'no netconnect';
+        if (!this.inRoom) return 'not in room';
+        this.send_data(C2S_ROOM_EXIT);
+        return this._new_resolve('exit_room');
+    }
+    private _clear_team(): void {
+        this._team?.dispose();
+        this._team = null;
+    }
+    private _clear_room(): void {
+        if (!this._room) return;
+        this._room?.dispose();
+        this._room = null;
+    }
+    private _send(data: Parameters<WebSocket['send']>[0]) {
+        if (!this._ws) return;
+        if (this._ws.isActive) {
+            this._ws.send(data);
+        } else {
+            this._waitSends.push(data);
+        }
+    }
+    private _updatePlayerStatus(id: string, online: boolean) {
+        if (this.inTeam) (this._team as any).updatePlayerStatus(id, online);
+        if (this.inRoom) (this._room as any).updatePlayerStatus(id, online);
+    }
+    //
+    private _onMessage(msg: MessageEvent): void {
+        this.resetHeartbeat();
+        try {
+            const { data: medata } = msg;
+            const data = this.decode(medata);
+            if (data.type !== PONG) chsdk.log.log('receive', JSON.stringify(data));
+            switch (data.type) {
+                case PONG:
+                    this._ping = chsdk.date.now() - this._pingTime;
+                    chsdk.date.updateServerTime(data.data);
+                    //chsdk.log.log("pong:", this._ping, data.data, chsdk.date.now());
+                    break;
+                case S2C_LOGIN_SUCCESS:
+                    this._lv = data.data;
+                    if (this._do_resolve('connect', null)) {
+                        this.state.emit(this.state.key.Connected);
+                    } else {
+                        this.state.emit(this.state.key.Reconnected);
+                    }
+                    this._callbacks = {};
+                    this.startHeartbeat();
+                    while (this._waitSends.length) {
+                        if (!this._ws) break;
+                        const data = this._waitSends.shift();
+                        if (data) this._ws.send(data);
+                    }
+                    break;
+                case S2C_TEAM_SUCCESS:
+                    if (this._team) this._clear_team();
+                    this._team = new NetTeam<GD>(data.data, this.send_data.bind(this));
+                    if (!this._do_resolve('creat_team', null)) this._do_resolve('join_team', null);
+                    break;
+                case S2C_JOIN_TEAM_FAIL:
+                    this._do_resolve('join_team', data.data);
+                    break;
+                case S2C_TEAM_USER_JOIN:
+                    const join_user = data.data;
+                    join_user.status = true;
+                    (this._team as any).addPlayer({ key: join_user.userKey, data: join_user }, true);
+                    break;
+                case S2C_TEAM_USER_EXIT:
+                    const esit_id = data.data;
+                    if (esit_id === this._team.own.Id) {
+                        (this._team as any).closed();
+                        this._clear_team();
+                        this._do_resolve('exit_team', null);
+                    } else {
+                        (this._team as any).removePlayer(esit_id);
+                    }
+                    break;
+                case S2C_TEAM_CHANGE_HOST:
+                    (this._team as any).changeHost(data.data);
+                    break;
+                case S2C_ROOM_CHANGE_HOST:
+                    (this._room as any).changeHost(data.data);
+                    break;
+                case S2C_USER_RECONNECT:
+                    this._updatePlayerStatus(data.data, true);
+                    break;
+                case S2C_USER_DISCONNECT:
+                    this._updatePlayerStatus(data.data, false);
+                    break;
+                case S2C_MATCH:
+                    (this._team as any).match_start();
+                    break;
+                case S2C_MATCH_CANCEL:
+                    (this._team as any).match_cancel();
+                    break;
+                case S2C_NOT_READY:
+                    (this._team as any).not_ready();
+                    break;
+                case S2C_READY:
+                    (this._team as any).updatePlayerReady(data.data, true);
+                    break;
+                case S2C_MATCH_SUCCESS:
+                    this._clear_room();
+                    const roomData = data.data.roomData;
+                    const pData = data.data.playerData;
+                    this._room = new NetRoom<GD>(roomData, pData);
+                    if (this._team) (this._team as any).match_success();
+                    break;
+                case S2C_ROOM_GAMEDATA_CHANGE:
+                    (this._room as any).server_change(data.data);
+                    break;
+                case S2C_USER_GAMEDATA_CHANGE:
+                    const p = this._room.getPlayer(data.data.userKey);
+                    if (!p) break;
+                    (p as any).server_change(data.data.data);
+                    break;
+                case S2C_ROOM_CLOSED:
+                    (this._room as any).closed(data.data);
+                    if (this.inTeam) {
+                        const list = this.room.all;
+                        for (let i = 0; i < list.length; i++) {
+                            const rp = list[i];
+                            if (rp.isAI) continue;
+                            const p = this.team.getPlayer(rp.Id);
+                            if (!p) continue;
+                            (p as any).set_level(rp.level, rp.rank, rp.score, rp.totalRank);
+                        }
+                    }
+                    this._do_resolve('exit_room', null);
+                    break;
+                case S2C_TALK:
+                    if (this.inRoom) {
+                        (this._room as any).onChat(data.data.userKey, data.data.msg);
+                    } else if (this.inTeam) {
+                        (this._team as any).onChat(data.data.userKey, data.data.msg);
+                    }
+                    break;
+                case S2C_FINISH:
+                    (this._room as any).finish(data.data.userKey, data.data.rank);
+                    break;
+                case S2C_SETTLEMENT:
+                    (this._room as any).settlement(data.data);
+                    break;
+                case C2S_MESSAGE:
+                    if (Array.isArray(data.data)) {
+                        for (let i = 0; i < data.data.length; i++) {
+                            const d = data.data[i];
+                            (this._room as any)._onEvt(d.type, d.data);
+                        }
+                    } else {
+                        chsdk.log.log('no def type:', data.data);
+                        //(this.receive as any)._emit(data.type, data.data);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        } catch (error) {
+            chsdk.log.warn(error)
+            this.state.emit(this.state.key.Error, error);
+        }
+    }
+    private _onConnected(evt: any): void {
+        if (this.timeout) clearTimeout(this.timeout);
+        this._currentReconnectCount = 0;
+        this.timeout = setTimeout(() => {
+            this._ws.close();
+            chsdk.log.error("Closing connection.");
+        }, 3000);
+    }
+    private _onError(err: any): void {
+        chsdk.log.log('err', err);
+        this.state.emit(this.state.key.Error, err);
+    }
+    private _onClosing(): void {
+        chsdk.log.log('closing');
+        this.state.emit(this.state.key.Connecting);
+    }
+
+    private _onClosed(event: CloseEvent): void {
+        chsdk.log.log("WebSocket connection closed", event);
+        this.stopHeartbeat();
+        if (!this.reconnection || (event.reason && event.code > 4000)) {
+            this.state.emit(this.state.key.Closed, event);
+            this._do_resolve('connect', event);
+            chsdk.log.log("WebSocket connection closed", event);
+            return;
+        }
+        if (this._currentReconnectCount < this.reconnectCount) {
+            this._currentReconnectCount += 1;
+            chsdk.log.log(`Reconnecting... Attempt ${this._currentReconnectCount}`);
+            setTimeout(() => { this.do_reconnect() }, this.reconnectInterval)
+        } else {
+            chsdk.log.error("Max reconnect attempts reached. Giving up.");
+            this.state.emit(this.state.key.Closed, event);
+            this._do_resolve('connect', event);
+        }
+    }
+    private startHeartbeat() {
+        this.heartbeatInterval = setInterval(() => {
+            this.sendHeartbeat();
+        }, HEARTBEAT_INTERVAL);
+        this.dirtyDataCheckInterval = setInterval(() => {
+            this.dirtyDataCheck();
+        }, this.gameDataInterval);
+        this.evtCheckInterval = setInterval(() => {
+            this.evtCheck();
+        }, this.gameEvtInterval);
+    }
+    private resetHeartbeat() {
+        if (this.timeout) clearTimeout(this.timeout);
+        this.timeout = setTimeout(() => {
+            chsdk.log.error("Heartbeat timeout. Closing connection.");
+            this._ws.close();
+        }, this.timeoutInterval);
+    }
+    private sendHeartbeat() {
+        this._pingTime = chsdk.date.now();
+        this.send_data(PING, this._ping);
+    }
+    private stopHeartbeat() {
+        if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
+        if (this.timeout) clearTimeout(this.timeout);
+        if (this.dirtyDataCheckInterval) clearInterval(this.dirtyDataCheckInterval);
+        if (this.evtCheckInterval) clearInterval(this.evtCheckInterval);
+    }
+    private evtCheck() {
+        if (this.inRoom) {
+            (this.room as any).doSendChat((msg: string) => {
+                if (!msg) return;
+                this.send_data(C2S_TALK, msg);
+            });
+            (this.room as any).doSendMode((mode0, mode1, mode2) => {
+                if (mode0.length > 0) this.send_data(C2S_MESSAGE, mode0);
+                if (mode1.length > 0) this.send_data(C2S_MESSAGE_TO_HOST, mode1);
+                if (mode2.length > 0) this.send_data(C2S_MESSAGE_TO_OTHER, mode2);
+            });
+            const ps = this.room.all;
+            for (let i = 0; i < ps.length; i++) {
+                const p = ps[i];
+                (p as any).doFinishGame(() => { this.send_data(C2S_FINISH, p.Id); });
+            }
+        } else if (this.inTeam) {
+            (this.team as any).doSendChat((msg: string) => {
+                if (!msg) return;
+                this.send_data(C2S_TALK, msg);
+            });
+        }
+    }
+    private dirtyDataCheck() {
+        if (!this.inRoom) return;
+        (this.room.own as any).doDirtyData((dirty) => {
+            this.send_data(C2S_UPDATE_USER_GAMEDATA, { userKey: this.room.own.Id, data: dirty });
+        })
+        if (!this.room.isHost) return;
+        (this._room as any).doDirtyData((dirty) => {
+            this.send_data(C2S_UPDATE_ROOM_GAMEDATA, dirty);
+        })
+        const ais = this.room.all;
+        for (let i = 0; i < ais.length; i++) {
+            const ai = ais[i];
+            (ai as any).doDirtyData((dirty) => {
+                this.send_data(C2S_UPDATE_USER_GAMEDATA, { userKey: ai.Id, data: dirty });
+            })
+        }
+    }
+    /**
+     * 分享队伍信息,邀请进队
+     * @param extend (可选)分享数据
+     * @param title 分享显示标题
+     * @param imageUrl 分享显示图片
+     */
+    public shareNetTeam<T extends { [key: string]: any }>(title?: string, imageUrl?: string, extend?: T) {
+        if (!this.inTeam) return;
+        chsdk.shareAppMessage(title, '', imageUrl, JSON.stringify({ type: 'NetTeam', pw: this.team.Password, extends: extend }));
+    }
+    /**
+    *获取从好友分享的net数据进入游戏的数据
+    */
+    public getShareNetTeam<T extends { [key: string]: any }>(): { password: string, extend?: T } {
+        const query = chsdk.getQuery();
+        if (!query) return null;
+        const message = query.message;
+        if (!message) return null;
+        const data = JSON.parse(message);
+        if (!data) return null;
+        if (data.type != 'NetTeam') return null;
+        query.message = null;
+        return { password: data.pw, extend: data.extends as T };
+    }
+}

+ 1 - 0
idiom/assets/ch/net/net.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"3f10abbc-5e71-46d9-bd19-8d7cb59e5d22","files":[],"subMetas":{},"userData":{}}

+ 9 - 0
idiom/assets/ch/pvp.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "f99f28eb-fd03-4ae2-be42-4872a3d23ba1",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 241 - 0
idiom/assets/ch/pvp/ch_pvp.ts

@@ -0,0 +1,241 @@
+import ch_util from "../ch_util";
+/** 
+ * pvp扩展玩法 基于 chsdk
+*/
+export type pvp_player = { head: string, nickName: string, uid: number, hid: number, province: string, pvp: number, rank: number };
+export type pvp_own_data<T> = { extends: T, type: 0 | 1 | 2, time: number, own: pvp_player };
+export type pvp_data<T> = { extends: T, other: pvp_player, own: pvp_player, type?: 0 | 1 | 2, status?: 0 | 1 | 2, time?: number, reward?: number, pvpId?: string };
+export type pvp_result = { other: { curRank: number, rank: number, curScore: number, score: number }, own: { curRank: number, rank: number, curScore: number, score: number } };
+class ch_pvp {
+    private readonly _set_url: string = '/user/setPvpSceneData';
+    private readonly _match_url: string = '/user/startPvpMatch';
+    private readonly _finish_url: string = '/user/pvpFinishAction';
+    private readonly _record_url: string = '/user/pvpChallengeRecord';
+    private static _instance: ch_pvp;
+    public static getInstance(): ch_pvp {
+        if (!this._instance) this._instance = new ch_pvp();
+        return this._instance;
+    }
+    /**
+     * 设置自己的 PVP 擂台数据
+     * 该方法用于更新当前用户在 PVP 擂台上的数据。数据将根据传入的 extend 参数进行更新,  
+     * 并返回一个包含操作状态和可能错误信息的 Promise。
+     * @param extend - 一个pvp擂台数据对象,包含要更新的具体数据。
+     * @returns 一个 Promise,解析为一个包含以下属性的对象:  
+     *   - code: 操作的结果状态。0 表示成功,其他值表示不同的错误类型。  
+     *   - err:  可选字符串,指示错误信息(如果有)。     
+     */
+    public async setSceneData<T extends { [key: string]: any }>(extend: T): Promise<{ code: number, err?: string }> {
+        const check = chsdk.check_req_time('setSceneData');
+        if (check) return check;
+        const body = { extends: ch_util.stringify(extend) };
+        const ret = await chsdk.makePostTokenRequest(chsdk.getUrl(this._set_url), body);
+        if (ret.code != chsdk.code.success) {
+            chsdk.log.warn(ret);
+        } else {
+            chsdk.log.log(`PVP擂台数据上报成功`);
+            if (!this._own) {
+                await this.getOwn<T>();
+            } else {
+                this._own.extends = extend;
+            }
+        }
+        return ret;
+    }
+    /**
+    * 开始匹配
+    * @param uid (可选)具体好友的uid,默为空
+    * @returns null  匹配失败
+    * 
+    *          pvp_data<T>
+                   extends PVP 擂台数据
+    *             other   台主信息
+    *             own     自己的信息
+    *                     head:头像, nickName:名字, uid:id, hid:省id, province:省名, pvp:pvp排位分值, rank:pvp排行榜名次  
+    */
+    public async startMatch<T extends { [key: string]: any }>(uid?: number | null): Promise<pvp_data<T> | null> {
+        const check = chsdk.check_req_time('startMatch');
+        if (check) return null;
+        const ret = await chsdk.makePostTokenRequest(chsdk.getUrl(this._match_url), { uid: uid ? uid.toString() : '' });
+        if (ret.code != chsdk.code.success) {
+            chsdk.log.warn(ret);
+            return null;
+        } else {
+            const data = ret.data.data;
+            data.other.uid = Number.parseInt(data.other.uid);
+            data.other.hid = Number.parseInt(data.other.hid);
+            data.other.province = chsdk.provinceCode2Name(data.other.hid);
+            data.own.uid = Number.parseInt(data.own.uid);
+            data.own.hid = Number.parseInt(data.own.hid);
+            data.own.province = chsdk.provinceCode2Name(data.own.hid);
+            return { extends: ch_util.parse(data.extends) as T, other: data.other, own: data.own }
+        }
+    }
+    /**从一条记录开始匹配,用于复仇*/
+    public async startMatchRecord<T extends { [key: string]: any }>(pd: pvp_data<T>): Promise<pvp_data<T> | null> {
+        const check = chsdk.check_req_time('startMatch');
+        if (check) return null;
+        let pp = await this.startMatch<T>(pd.other.uid);
+        if (!pp) return null;
+        pp.extends = pd.extends;
+        pp.pvpId = pd.pvpId;
+        return pp;
+    }
+    /**
+     * 完成pvp上报
+     * @param otherUid 对方uid
+     * @param status 0:挑战失败 1:挑战胜利
+     * @param ext PVP 擂台数据
+     * @param serverData 服务器处理排行数据,订阅消息
+     * ..ownValue 增减自己排位分值
+     * ..otherValue 增减对方排位分值
+     * ..msg 自定义的订阅消息
+     * @param pvpId 复仇id
+     * @returns 对战结果
+     *          other 对方的排名和分数变化
+     *          own  自己的排名和分数变化
+     *               curRank 当前排名(变动后)
+                     curScore 当前分数(变动后)
+                     rank  名数变动
+                     score 分数变动
+     */
+    public async finish<T extends { [key: string]: any }>(otherUid: number, status: 0 | 1, ext: T,
+        serverData: { ownValue?: number, otherValue?: number, msg?: string }
+        , pvpId?: string
+    ): Promise<pvp_result> {
+        const check = chsdk.check_req_time('finish');
+        if (check) return null;
+        const body = { otherUid: otherUid.toString(), status: status, extends: ch_util.stringify(ext), serverData: ch_util.stringify(serverData), pvpId: pvpId };
+        const ret = await chsdk.makePostTokenRequest(chsdk.getUrl(this._finish_url), body);
+        if (ret.code != chsdk.code.success) {
+            chsdk.log.warn(ret);
+            return null;
+        } else {
+            const data = ret.data.data;
+            chsdk.log.log(`pvp结果数据上报成功,结算:`, data);
+            return data;
+        }
+    }
+    private _own: any;
+    /**获取自己的pvp擂台数据*/
+    public async getOwn<T extends { [key: string]: any }>(): Promise<pvp_own_data<T> | null> {
+        if (!this._own) await this.getRecords<T>(3, false);
+        return this._own;
+    }
+    //
+    private _cache_records: any | null = null;
+    private _cache_time: number;
+    private get_cache_records(): any | null {
+        if (chsdk.date.now() - this._cache_time > 6e4) return null;
+        return this._cache_records;
+    }
+    /**
+    * 获取pvp对战记录
+    * @param type  0:全部纪录 1:被挑战的记录信息(默认) 2:挑战别人的记录 3:不需要纪录
+    * @param cache 默认为true,优先缓存拿数据,缓存存在一分钟
+    * @returns own:自己的PVP  擂台数据
+    *          list:交互纪录
+    *            extends PVP  擂台数据
+    *            reward	奖励惩罚排位分值,由于serverData中得来
+    *            type	  0:自己的数据 1:被挑战的记录信息 2:挑战别人的记录
+    *            status   0:挑战失败 1:挑战胜利 2:复仇成功
+    *            time     时间
+    *            pvpId   pvp复仇id
+    *            other   对方信息
+    *            own     自己信息
+    */
+    public async getRecords<T extends { [key: string]: any }>(typeId: 0 | 1 | 2 | 3 = 1, cache: boolean = true): Promise<{
+        own: pvp_own_data<T>,
+        list: pvp_data<T>[],
+    }
+    > {
+        if (cache) {
+            const data = this.get_cache_records();
+            if (data) return data;
+        }
+        const check = chsdk.check_req_time('getRecords');
+        if (check) return null;
+        const ret = await chsdk.makePostTokenRequest(chsdk.getUrl(this._record_url), { typeId: typeId });
+        if (ret.code != chsdk.code.success) {
+            chsdk.log.warn(ret);
+            return null;
+        } else {
+            let data = ret.data.data;
+            if (data.own) {
+                const d = data.own;
+                d.extends = ch_util.parse(d.extends) as T;
+                d.own.uid = Number.parseInt(d.own.uid);
+                d.own.hid = Number.parseInt(d.own.hid);
+                d.own.province = chsdk.provinceCode2Name(d.own.hid);
+            } else {
+                data.own = null;
+            }
+            data.list ||= [];
+            for (let i = 0; i < data.list.length; i++) {
+                const d = data.list[i];
+                d.extends = ch_util.parse(d.extends) as T;
+                d.own.uid = Number.parseInt(d.own.uid);
+                d.own.hid = Number.parseInt(d.own.hid);
+                d.own.province = chsdk.provinceCode2Name(d.own.hid);
+
+                d.other.uid = Number.parseInt(d.other.uid);
+                d.other.hid = Number.parseInt(d.other.hid);
+                d.other.province = chsdk.provinceCode2Name(d.other.hid);
+            }
+            this._own = data.own;
+            this._cache_records = data;
+            this._cache_time = chsdk.date.now();
+            return data;
+        }
+    }
+    /**
+     * 获取pvp排位分值榜单
+     * @returns
+     */
+    public async getRank(): Promise<{
+        list: {
+            head: string;
+            nickName: string;
+            rank: number;
+            score: number;
+            userId: number;
+            [key: string]: any;
+        }[], ower: {
+            head: string;
+            nickName: string;
+            rank: number;
+            score: number;
+            userId: number;
+            [key: string]: any;
+        }
+    }> {
+        const d = await chsdk.loadRankData('pvp', 1, 100, true);
+        return { list: d.data.list, ower: d.data.own };
+    }
+    /**
+     * 分享pvp数据,邀请对战
+     * @param player 玩家自己信息
+     * @param extend (可选)pvp关卡等数据
+     * @param title 分享显示标题
+     * @param imageUrl 分享显示图片
+     */
+    public async sharePvp<T extends { [key: string]: any }>(player: pvp_player, extend?: T, title?: string, imageUrl?: string) {
+        chsdk.shareAppMessage(title, '', imageUrl, ch_util.stringify({ type: 'pvp', player: player, extends: extend }));
+    }
+    /**
+    *获取从好友分享的pvp数据进入游戏的数据
+    */
+    public getSharePvpExtends<T extends { [key: string]: any }>(): { player: pvp_player, extend?: T } {
+        const query = chsdk.getQuery();
+        if (!query) return null;
+        const message = query.message;
+        if (!message) return null;
+        const data = ch_util.parse(message);
+        if (!data) return null;
+        if (data.type != 'pvp') return null;
+        query.message = null;
+        return { player: data.player, extend: data.extends as T };
+    }
+}
+//
+export default ch_pvp.getInstance();

+ 1 - 0
idiom/assets/ch/pvp/ch_pvp.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"8b65873a-524d-49ab-81e4-1f1d5cb47d8f","files":[],"subMetas":{},"userData":{}}

+ 147 - 0
idiom/assets/ch/sign/sign.ts

@@ -0,0 +1,147 @@
+
+export default class ch_sign{
+    private static _instance:ch_sign;
+    public static getInstance():ch_sign{
+        if(!this._instance) this._instance = new ch_sign();
+        return this._instance;
+    }
+    private _sign_data:Set<number>=new Set();
+    private _max:number = 7;
+    private _creat_date:number=0;
+    private _sign_count:number=0;
+    private _mode:number=1;
+    /**设定的签到天数*/
+    public get max_day():number{return this._max;}
+    private getCreatDayCount():number{
+        if(this._creat_date==0){
+            chsdk.log.warn("签到模块没有初始化");
+            return 0;
+        }
+        return chsdk.date.getDiffDayNum(this._creat_date,chsdk.date.now())+1;
+    }
+    /**
+     * 签到初始化
+     * @param mode =1可以补签 =2不可补签
+     * @param data 签到存档数据,可以为空为第一天
+     * @param max_day 签到最大天数
+     */
+    public init(mode:1|2,data:{sign_list:number[],creat_date:number}|{sign_count:number,last_date:number}|null,max_day:number=7):void{
+        this._mode=mode;
+        if(this._mode==1){
+            if(data){
+                data =data as {sign_list:number[],creat_date:number};
+                const sign_list=data.sign_list ?? [];
+                const creat_date=data.creat_date ?? chsdk.date.now();
+                for(let i=0;i<sign_list.length;i++)this._sign_data.add(sign_list[i]);
+                this._sign_count=this._sign_data.size;
+                this._creat_date=creat_date;
+            }else{
+                this._sign_count=0;
+                this._sign_data.clear();
+                this._creat_date=chsdk.date.now();
+            }
+        }else{
+            if(data){
+                data = data as {sign_count:number,last_date:number};
+                this._sign_count=data.sign_count ?? 0;
+                this._creat_date=data.last_date ?? 0;
+            }else{
+                this._sign_count=0;
+                this._creat_date=0; 
+            }
+        }
+        this._max=max_day;
+    }
+    /**已签到的数据,用于存档*/
+    public getSignData():{sign_list:number[],creat_date:number}|{sign_count:number,last_date:number}{
+        if(this._mode==1){
+           return {sign_list:Array.from(this._sign_data.keys()),creat_date:this._creat_date};  
+        }else if(this._mode==2){
+           return {sign_count:this._sign_count,last_date:this._creat_date};  
+        }
+    }
+    /** 返回某一天的状态
+     *  0 等待签到 
+     *  1 已经签到 
+     *  2 失效等待补签
+     * */
+    public checkSigineState(day:number):number{
+        if(this._mode==1){
+            if(day<1 || day>this._max)return 0;
+            if(this._sign_data.has(day))return 1;
+            const count = this.getCreatDayCount();
+            if(count <=0) return 0;
+            if(day <count) return 2;
+        }else if(this._mode==2){
+            if(day<=this._sign_count) return 1;
+        }
+        return 0;
+    }
+    /**今天是否可以签到*/
+    public checkSigin():boolean{
+        if(this._mode==1){
+           const count = this.getCreatDayCount();
+           if(count<=0 || count>this._max)return false;
+           return !this._sign_data.has(count);
+        }else if(this._mode==2){
+           const count = this._sign_count;
+           if(count>=this._max)return false;
+           return !chsdk.date.isSameDate(this._creat_date,chsdk.date.now());
+        }
+        return false;
+    }
+    /**签到是否全部完成*/
+    public checkSiginComplete():boolean{
+        const count = this._sign_count;
+        return count>=this._max;
+    }
+    /**
+     * 是否能补签
+     * 不能返回0,
+     * 可以补签返回可以补签的那一天*/
+    public checkReSigin():number{
+        if(this._mode!=1) return 0;
+        let count = this.getCreatDayCount()-1;
+        if(count<=0)return 0;
+        if(count>this._max)count=this._max;
+        for(let i=1;i<=count;i++){
+            if(!this._sign_data.has(i)) return i;
+        }
+        return 0;
+    }
+    /**签到
+     * 失败返回 0
+     * 成功返回签到当天
+    */
+    public  signIn(): number {  
+        if (!this.checkSigin()) {  
+            return 0;
+        }
+        if(this._mode==1){
+           const day = this.getCreatDayCount();  
+           this._sign_data.add(day); 
+           this._sign_count=this._sign_data.size;
+           return day;
+        }else  if(this._mode==2){
+            this._sign_count++;
+            this._creat_date=chsdk.date.now();
+            return this._sign_count;
+        }
+        return 0;
+    }
+    /**补签
+    * 失败返回 0
+    * 成功返回补签那天
+    */
+    public reSignIn():number{
+        if(this._mode!=1)return 0;
+        const index = this.checkReSigin();
+        if(index<=0 || index>this._max){
+            return 0;
+        }
+        this._sign_data.add(index);
+        this._sign_count=this._sign_data.size;
+        return index;
+    }
+    //
+}

+ 9 - 0
idiom/assets/ch/sign/sign.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "6d031cab-c8a4-4b61-a88e-1da5ca87802f",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/ch/start.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "5cd73f25-b578-4dce-822c-905430aa8cc0",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 22 - 0
idiom/assets/ch/start/ch_sdk_comp.ts

@@ -0,0 +1,22 @@
+import { _decorator, Component, Game, game, Node } from 'cc';
+const { ccclass, property } = _decorator;
+
+@ccclass('ch_sdk_comp')
+export class ch_sdk_comp extends Component {
+    protected onLoad(): void {
+        game.on(Game.EVENT_SHOW, () => {
+            chsdk.sdk_event.emit(chsdk.sdk_event.key.show);
+        }, this);
+        game.on(Game.EVENT_HIDE, () => {
+            chsdk.sdk_event.emit(chsdk.sdk_event.key.hide);
+        }, this);
+        /*window.addEventListener("blur", () => {
+            chsdk.sdk_event.emit(chsdk.sdk_event.key.hide);
+        });
+        window.addEventListener("focus", () => {
+            chsdk.sdk_event.emit(chsdk.sdk_event.key.show);
+        });*/
+    }
+}
+
+

+ 9 - 0
idiom/assets/ch/start/ch_sdk_comp.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "9a64d7da-d484-4f93-9510-4c2cbd5b1462",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 169 - 0
idiom/assets/ch/start/ch_start_pack.ts

@@ -0,0 +1,169 @@
+import { _decorator, Component, director, Enum, Node } from 'cc';
+import { ch } from '../ch';
+import { loadType } from '../audio/audio';
+import { ch_sdk_comp } from './ch_sdk_comp';
+const { ccclass, property } = _decorator;
+/**包id*/
+export enum pack {
+    id0 = 0,
+    id1 = 1,
+    id2 = 2,
+    id3 = 3,
+    id4 = 4,
+    id5 = 5,
+    id6 = 6,
+    id7 = 7,
+    id8 = 8,
+    id9 = 9,
+}
+@ccclass('ch_start_pack')
+export class ch_start_pack extends Component {
+    @property({ visible: false })
+    private _gname: string[] = [];
+    @property({ visible: false })
+    private _gid: string[] = [];
+    @property({ visible: false })
+    private _is_local: boolean[] = [];
+    @property({ visible: false })
+    private _server: chsdk.serverType[] = [chsdk.serverType.dev];
+    @property({ visible: false })
+    private _report: chsdk.reportType[] = [chsdk.reportType.off];
+    @property({ visible: false })
+    private _log: chsdk.loglevel[] = [];
+    @property({ visible: false })
+    private _ttad: string[] = [];
+    @property({ visible: false })
+    private _ttiad: string[] = [];
+    @property({ visible: false })
+    private _tttmpid: string[] = [];
+    @property({ visible: false })
+    private _wxad: string[] = [];
+    @property({ visible: false })
+    private _wxiad: string[] = [];
+    @property({ visible: false })
+    private _wxtmpid: string[] = [];
+    @property({ visible: false })
+    private _serverIP: string[] = [];
+    @property({ visible: false })
+    private _pid: pack = pack.id0;
+    @property({ type: Enum(pack), displayName: '包id', group: { name: '选包', id: 'pack', displayOrder: 0 } })
+    get pid() { return this._pid; }
+    set pid(val) {
+        this._pid = val;
+        for (let i = 0; i <= this._pid; i++) {
+            this._gname[i] ??= '';
+            this._is_local[i] ??= this._is_local[i - 1] ?? true;
+            this._gid[i] ??= this._gid[i - 1] ?? '0';
+            this._server[i] ??= this._server[i - 1] ?? chsdk.serverType.online;
+            this._report[i] ??= this._report[i - 1] ?? chsdk.reportType.off;
+            this._log[i] ??= this._log[i - 1] ?? chsdk.loglevel.DEBUG;
+            this._ttad[i] ??= '';
+            this._ttiad[i] ??= '';
+            this._tttmpid[i] ??= '';
+            this._wxad[i] ??= '';
+            this._wxiad[i] ??= '';
+            this._wxtmpid[i] ??= '';
+            this._serverIP[i] ??= 'http://192.168.1.120:8787/v1';
+        }
+    }
+    @property({ displayName: '游戏名', group: { name: '选包', id: 'pack', displayOrder: 0 } })
+    get gname() { return this._gname[this._pid] ?? ''; }
+    set gname(val) { this._gname[this._pid] = val; }
+    @property({ displayName: '游戏id', group: { name: '选包', id: 'pack', displayOrder: 0 } })
+    get gid() { return this._gid[this._pid]; }
+    set gid(val) { this._gid[this._pid] = val; }
+    //
+    @property({ displayName: '是否单机', group: { name: 'ch.sdk模块', id: 'sdk', displayOrder: 0 } })
+    get is_local() { return this._is_local[this._pid] }
+    set is_local(val) { this._is_local[this._pid] = val; }
+    //
+    @property({ type: Enum(chsdk.serverType), displayName: '服务器地址', visible: function () { return !this.is_local; }, group: { name: 'ch.sdk模块', id: 'sdk', displayOrder: 0 }, tooltip: "local 局域网 \n dev 测试服 \n online 正式服" })
+    get server() { return this._server[this._pid]; }
+    set server(val) { this._server[this._pid] = val; }
+    @property({ displayName: '本地服测试ip', visible: function () { return this.server == chsdk.serverType.test }, group: { name: 'ch.sdk模块', id: 'sdk', displayOrder: 0 } })
+    get serverIP() { return this._serverIP[this._pid]; }
+    set serverIP(val) { this._serverIP[this._pid] = val; }
+    @property({ type: Enum(chsdk.reportType), displayName: '上报类型', group: { name: '基本参数', id: 'sdk', displayOrder: 0 }, tooltip: "off 不上报 \n ch 使用ch服务器(需要在ch后台配置事件) \n platform 使用微信/抖音平台(需要在平台后台设置事件)\nch__platflorm所有平台自定义事件都上报" })
+    get report() { return this._report[this._pid]; }
+    set report(val) { this._report[this._pid] = val; }
+    @property({ type: Enum(chsdk.loglevel), displayName: '日志等级', group: { name: '基本参数', id: 'sdk', displayOrder: 0 } })
+    get log() { return this._log[this._pid]; }
+    set log(val) { this._log[this._pid] = val; }
+    //
+    @property({ displayName: '抖音奖励广告id', group: { name: '基本参数', id: 'sdk', displayOrder: 0 } })
+    get ttad() { return this._ttad[this._pid]; }
+    set ttad(val) { this._ttad[this._pid] = val; }
+    @property({ displayName: '抖音插屏广告id', group: { name: '基本参数', id: 'sdk', displayOrder: 0 } })
+    get ttiad() { return this._ttiad[this._pid]; }
+    set ttiad(val) { this._ttiad[this._pid] = val; }
+    @property({ displayName: '抖音订阅id(,隔开)', group: { name: '基本参数', id: 'sdk', displayOrder: 0 } })
+    get tttmpid() { return this._tttmpid[this._pid]; }
+    set tttmpid(val) { this._tttmpid[this._pid] = val; }
+
+    @property({ displayName: '微信奖励广告id', group: { name: '基本参数', id: 'sdk', displayOrder: 0 } })
+    get wxad() { return this._wxad[this._pid]; }
+    set wxad(val) { this._wxad[this._pid] = val; }
+    @property({ displayName: '微信插屏广告id', group: { name: '基本参数', id: 'sdk', displayOrder: 0 } })
+    get wxiad() { return this._wxiad[this._pid]; }
+    set wxiad(val) { this._wxiad[this._pid] = val; }
+    @property({ displayName: '微信订阅id(,隔开)', group: { name: '基本参数', id: 'sdk', displayOrder: 0 } })
+    get wxtmpid() { return this._wxtmpid[this._pid]; }
+    set wxtmpid(val) { this._wxtmpid[this._pid] = val; }
+    //
+    @property({ visible: false })
+    private _use_ch_audio: boolean = false;
+    @property({ type: Enum(loadType), visible: false })
+    private _ch_audio_type: loadType = loadType.bundle;
+    @property({ displayName: '是否使用ch.audio模块', group: { name: 'ch.audio模块', id: 'audio', displayOrder: 1 } })
+    get use_ch_audio() { return this._use_ch_audio; }
+    set use_ch_audio(val) { this._use_ch_audio = val; }
+    @property({ type: Enum(loadType), displayName: '音频资源加载模式', group: { name: 'ch.audio模块', id: 'audio', displayOrder: 1 }, visible: function () { return this._use_ch_audio; } })
+    get sount_load_type() { return this._ch_audio_type; }
+    set sount_load_type(val) { this._ch_audio_type = val; }
+    @property({ displayName: '音频资源默认bundle名', group: { name: 'ch.audio模块', id: 'audio', displayOrder: 1 }, visible: function () { return this._use_ch_audio && this._ch_audio_type == loadType.bundle } })
+    private sound_bundle: string = 'res';
+    @property({ displayName: '音频资源远程地址', group: { name: 'ch.audio模块', id: 'audio', displayOrder: 1 }, visible: function () { return this._use_ch_audio && this._ch_audio_type == loadType.remote } })
+    private sound_url: string = '';
+    protected onLoad(): void {
+        console.log('包Id:' + this.pid, '游戏名:' + this.gname, '游戏Id:' + this.gid, '单机:' + this.is_local, '服务器:' + chsdk.serverType[this.server]);
+    }
+    /**初始化*/
+    async init(): Promise<boolean> {
+        chsdk.setConf(chsdk.pf.tt, { adUnitId: this.ttad, multiton: false, inster_unitId: this.ttiad, tmplIds: this.tttmpid ? this.tttmpid.split(',') : null });
+        chsdk.setConf(chsdk.pf.wx, { adUnitId: this.wxad, multiton: false, inster_unitId: this.wxiad, tmplIds: this.wxtmpid ? this.wxtmpid.split(',') : null });
+        let ret = await chsdk.init_inside(this.gid, this.log, this.server == chsdk.serverType.test ? this.serverIP : this.server, this.is_local, this.report);
+        if (ret.code == chsdk.code.success) {
+            if (this.use_ch_audio) {
+                ch.audio.init(this.sount_load_type, this.sound_bundle, this.sound_url);
+            }
+            if (chsdk.get_pf() === chsdk.pf.web) {
+                const sdk = new Node();
+                sdk.name = '__ch_sdk__';
+                sdk.addComponent(ch_sdk_comp);
+                director.getScene().addChild(sdk);
+                director.addPersistRootNode(sdk);
+            }
+            return true;
+        } else {
+            chsdk.log.error(ret.err);
+            return false;
+        }
+    }
+    /**初化始并在遇到问题时以单机模式进入
+    * @param try_count 重试次数
+    * @param try_interval 重试间隔时间(MS)
+    */
+    async try_init(try_count: number = 5, try_interval: number = 2000): Promise<void> {
+        let ret = await this.init();
+        if (ret) return;
+        try_count--;
+        if (try_count < 0) {
+            this.is_local = true;
+            await this.init();
+            return;
+        } else {
+            await new Promise(resolve => setTimeout(resolve, try_interval));
+            return this.try_init(try_count, try_interval);
+        }
+    }
+}

+ 1 - 0
idiom/assets/ch/start/ch_start_pack.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"4aa6336e-6ee2-49a8-b6a4-7011475e21d5","files":[],"subMetas":{},"userData":{}}

+ 9 - 0
idiom/assets/core.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "3b3f6940-39b4-46e8-afdf-98a9f29d2cbe",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/core/sdk.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "5e338b79-2198-44e3-9c16-75dca600c1f6",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/core/ui.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "e6a9b4aa-6d44-413d-9cde-6cdbc4383184",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 279 - 0
idiom/assets/core/ui/UICanvas.prefab

@@ -0,0 +1,279 @@
+[
+  {
+    "__type__": "cc.Prefab",
+    "_name": "UICanvas",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "_native": "",
+    "data": {
+      "__id__": 1
+    },
+    "optimizationPolicy": 0,
+    "persistent": false
+  },
+  {
+    "__type__": "cc.Node",
+    "_name": "UICanvas",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "_parent": null,
+    "_children": [
+      {
+        "__id__": 2
+      }
+    ],
+    "_active": true,
+    "_components": [
+      {
+        "__id__": 6
+      },
+      {
+        "__id__": 8
+      },
+      {
+        "__id__": 10
+      }
+    ],
+    "_prefab": {
+      "__id__": 12
+    },
+    "_lpos": {
+      "__type__": "cc.Vec3",
+      "x": 375,
+      "y": 817,
+      "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": "Camera",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "_parent": {
+      "__id__": 1
+    },
+    "_children": [],
+    "_active": true,
+    "_components": [
+      {
+        "__id__": 3
+      }
+    ],
+    "_prefab": {
+      "__id__": 5
+    },
+    "_lpos": {
+      "__type__": "cc.Vec3",
+      "x": 0,
+      "y": 0,
+      "z": 1000
+    },
+    "_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.Camera",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 2
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 4
+    },
+    "_projection": 0,
+    "_priority": 3,
+    "_fov": 45,
+    "_fovAxis": 0,
+    "_orthoHeight": 817,
+    "_near": 1,
+    "_far": 2000,
+    "_color": {
+      "__type__": "cc.Color",
+      "r": 0,
+      "g": 0,
+      "b": 0,
+      "a": 255
+    },
+    "_depth": 1,
+    "_stencil": 0,
+    "_clearFlags": 6,
+    "_rect": {
+      "__type__": "cc.Rect",
+      "x": 0,
+      "y": 0,
+      "width": 1,
+      "height": 1
+    },
+    "_aperture": 19,
+    "_shutter": 7,
+    "_iso": 0,
+    "_screenScale": 1,
+    "_visibility": 33554432,
+    "_targetTexture": null,
+    "_postProcess": null,
+    "_usePostProcess": false,
+    "_cameraType": -1,
+    "_trackingType": 0,
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "16M8JVk9VF9atARCiKOrPt"
+  },
+  {
+    "__type__": "cc.PrefabInfo",
+    "root": {
+      "__id__": 1
+    },
+    "asset": {
+      "__id__": 0
+    },
+    "fileId": "19BU5oG8BLa6iToEDyHene",
+    "instance": null,
+    "targetOverrides": null,
+    "nestedPrefabInstanceRoots": null
+  },
+  {
+    "__type__": "cc.UITransform",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 1
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 7
+    },
+    "_contentSize": {
+      "__type__": "cc.Size",
+      "width": 750,
+      "height": 1634
+    },
+    "_anchorPoint": {
+      "__type__": "cc.Vec2",
+      "x": 0.5,
+      "y": 0.5
+    },
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "4b4OHKjUJDkqqayDp7qwXj"
+  },
+  {
+    "__type__": "cc.Canvas",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 1
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 9
+    },
+    "_cameraComponent": {
+      "__id__": 3
+    },
+    "_alignCanvasWithScreen": true,
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "948xnQTy9G67bQyxqN+kAb"
+  },
+  {
+    "__type__": "cc.Widget",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 1
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 11
+    },
+    "_alignFlags": 45,
+    "_target": null,
+    "_left": 0,
+    "_right": 0,
+    "_top": 0,
+    "_bottom": 0,
+    "_horizontalCenter": 0,
+    "_verticalCenter": 0,
+    "_isAbsLeft": true,
+    "_isAbsRight": true,
+    "_isAbsTop": true,
+    "_isAbsBottom": true,
+    "_isAbsHorizontalCenter": true,
+    "_isAbsVerticalCenter": true,
+    "_originalWidth": 0,
+    "_originalHeight": 0,
+    "_alignMode": 2,
+    "_lockFlags": 0,
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "d6fQQwQvVMwp+zkYKxZj8S"
+  },
+  {
+    "__type__": "cc.PrefabInfo",
+    "root": {
+      "__id__": 1
+    },
+    "asset": {
+      "__id__": 0
+    },
+    "fileId": "127Q8VPQtLMpWgj80VXTIF",
+    "instance": null,
+    "targetOverrides": null
+  }
+]

+ 13 - 0
idiom/assets/core/ui/UICanvas.prefab.meta

@@ -0,0 +1,13 @@
+{
+  "ver": "1.1.50",
+  "importer": "prefab",
+  "imported": true,
+  "uuid": "24225708-b628-4277-a833-320331a74e72",
+  "files": [
+    ".json"
+  ],
+  "subMetas": {},
+  "userData": {
+    "syncNodeName": "UICanvas"
+  }
+}

+ 320 - 0
idiom/assets/core/ui/ui.ts

@@ -0,0 +1,320 @@
+import { _decorator, Component, director, error, instantiate, Node, Prefab, UITransform, Widget, assetManager, AssetManager, tween, v3, UIOpacity, Vec3, ProgressBar, Sprite, Tween } from "cc";
+import ui_base from "./ui_base";
+import { ResolutionAutoFit } from "./ui_ResolutionAutoFit";
+const { ccclass, property } = _decorator;
+@ccclass('ui_updater')
+class ui_updater extends Component {
+    update(dt: number) {
+        ui_base.updateAll(dt);
+    }
+}
+export enum GameUILayers {
+    GAME,
+    JOY_STICK,
+    HUD,
+    POPUP,
+    ALERT,
+    NOTICE,
+    LOADING,
+    OVERLAY
+}
+type Constructor<T extends ui_base> = new () => T;
+/**ui管理模块*/
+class ui {
+    private static _instance: ui;
+    public static getInstance(): ui {
+        if (!this._instance) this._instance = new ui();
+        return this._instance;
+    }
+    public delay(ms: number): Promise<void> {
+        return new Promise(resolve => setTimeout(resolve, ms));
+    }
+    public delay_second(s: number): Promise<void> {
+        return new Promise(resolve => setTimeout(resolve, s * 1000));
+    }
+    public progressBar_anim(bar: ProgressBar, progress: number, duration: number = 0.2): Promise<void> {
+        if (bar.progress >= progress) {
+            bar.progress = progress;
+            return;
+        } else {
+            return new Promise(resolve => tween(bar).to(duration, { progress: progress }).call(() => { resolve() }).start());
+        }
+    }
+    public fillRange_anim(sp: Sprite, progress: number, duration: number = 0.2, limt: -1 | 0 | 1 = 0): Promise<void> {
+        if ((limt > 0 && sp.fillRange >= progress) && (limt < 0 && sp.fillRange <= progress)) {
+            sp.fillRange = progress;
+            return;
+        } else {
+            return new Promise(resolve => tween(sp).to(duration, { fillRange: progress }).call(() => { resolve() }).start());
+        }
+    }
+    public scale_anim(node: Node, duration: number = 0.2, start: number = 0.5, end: number = 1,): Promise<void> {
+        node.setScale(start, start, 1);
+        return new Promise(resolve => tween(node).to(duration, { scale: v3(end, end, 1) }).call(() => { resolve() }).start());
+    }
+    public scale_elasticOut_anim(node: Node, duration: number = 0.2, start: number = 0.5, end: number = 1,): Promise<void> {
+        node.setScale(start, start, 1);
+        return new Promise(resolve => tween(node).to(duration, { scale: v3(end, end, 1) }, { easing: 'elasticOut' }).call(() => { resolve() }).start());
+    }
+    public fade_anim(node: Node, duration: number = 0.2, startOpacity: number = 0, targetOpacity: number = 255): Promise<void> {
+        const opacity = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity);
+        opacity.opacity = startOpacity;
+        return new Promise((resolve) => { tween(opacity).to(duration, { opacity: targetOpacity }).call(() => { resolve() }).start(); });
+    }
+    public shake_anim(node: Node, duration: number = 0.2, intensity: number = 5, shakeCount: number = 7): Promise<void> {
+        return new Promise((resolve) => {
+            const originalPosition = node.position.clone(); // 保存原始位置  
+            const shakeDuration = duration / (shakeCount * 2); // 每次抖动的持续时间  
+            const shakeTween = tween(node);
+            for (let i = 0; i < shakeCount; i++) {
+                const offsetX = (i % 2 === 0 ? intensity : -intensity); // 交替偏移  
+                shakeTween.to(shakeDuration, { position: v3(originalPosition.x + offsetX, originalPosition.y, originalPosition.z) })
+                    .to(shakeDuration, { position: originalPosition }); // 回到原位  
+            }
+            shakeTween.call(() => { resolve() }).start(); // 开始抖动动画  
+        });
+    }
+    public rotate_shake_anim(node: Node, duration: number = 0.3, intensity: number = 5, shakeCount: number = 5): Promise<void> {
+        return new Promise((resolve) => {
+            const originalAngle = node.angle; // 保存原始角度  
+            const shakeDuration = duration / (shakeCount * 2); // 每次晃动的持续时间  
+            const shakeTween = tween(node);
+            for (let i = 0; i < shakeCount; i++) {
+                const offsetAngle = (i % 2 === 0 ? intensity : -intensity); // 交替旋转  
+                shakeTween.to(shakeDuration, { angle: originalAngle + offsetAngle })
+                    .to(shakeDuration, { angle: originalAngle }); // 回到原位  
+            }
+            shakeTween.call(() => { resolve() }).start(); // 开始角度晃动动画  
+        });
+    }
+    public scale_shake_anim(node: Node, duration: number = 0.3, intensity: number = 0.1, shakeCount: number = 10, reset: number | null = null): Promise<void> {
+        if (reset) {
+            Tween.stopAllByTarget(node);
+            node.setScale(reset, reset, reset);
+        }
+        return new Promise((resolve) => {
+            const originalScale = node.scale.clone(); // 保存原始缩放  
+            const shakeDuration = duration / (shakeCount * 2); // 每次震动的持续时间  
+            const shakeTween = tween(node);
+            for (let i = 0; i < shakeCount; i++) {
+                const offsetScale = (i % 2 === 0 ? intensity : -intensity); // 交替缩放  
+                shakeTween.to(shakeDuration, { scale: v3(originalScale.x + offsetScale, originalScale.y + offsetScale, originalScale.z) })
+                    .to(shakeDuration, { scale: originalScale }); // 回到原位  
+            }
+            shakeTween.call(() => { resolve() }).start(); // 开始缩放震动动画  
+        });
+    }
+    public opacity_shake_anim(node: Node, duration: number = 0.8, intensity: number = 80, shakeCount: number = 5): Promise<void> {
+        const opacity = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity);
+        const originalOpacity = opacity.opacity;
+        const shakeDuration = duration / (shakeCount * 2);
+        return new Promise((resolve) => {
+            const shakeTween = tween(opacity);
+            for (let i = 0; i < shakeCount; i++) {
+                const offsetOpacity = (i % 2 === 0 ? intensity : -intensity);
+                shakeTween.to(shakeDuration, { opacity: Math.min(Math.max(originalOpacity + offsetOpacity, 0), 255) })
+                    .to(shakeDuration, { opacity: originalOpacity });
+            }
+            shakeTween.call(() => { resolve() }).start(); // 开始透明度震动动画  
+        });
+    }
+    /**处理弹跳动画*/
+    public bounce_anim(node: Node, height: number = 100, duration: number = 0.5): Promise<void> {
+        return new Promise((resolve) => {
+            tween(node)
+                .to(duration, { position: node.position.clone().add(new Vec3(0, height, 0)) }, { easing: 'bounceOut' }) // 向上弹跳  
+                .to(duration, { position: node.position }, { easing: 'bounceIn' }) // 回到原位  
+                .call(() => { resolve() }) //完成动画  
+                .start(); //开始动画
+        });
+    }
+    /**
+     * 奖励物飞行动画
+     * @param node 奖励物节点
+     * @param startPos 起始爆开点
+     * @param targetPos 最终目标点
+     * @param explosionDistance 爆开距离
+     * @param explosionDuration 爆开时间
+     * @param stayDuration 停留时间
+     * @param moveDuration 移动到目标的时间
+     * @returns 
+     */
+    public reward_fly_anim(node: Node, startPos: Vec3, targetPos: Vec3, explosionDistance: number = 100, explosionDuration: number = 0.3, stayDuration: number = 0.5, moveDuration: number = 0.3): Promise<void> {
+        node.setPosition(startPos);
+        return new Promise((resolve) => {
+            //随机方向
+            const randomDirection = new Vec3(Math.random() * 2 - 1, Math.random() * 2 - 1, 0).normalize();
+            const explosionEndPos = startPos.add(randomDirection.multiplyScalar(explosionDistance)); // 爆炸效果的位置  
+            tween(node)
+                .to(explosionDuration, { worldPosition: explosionEndPos }) // 爆炸动画  
+                .delay(stayDuration) //停留时间  
+                .to(moveDuration, { worldPosition: targetPos }, { easing: 'cubicIn' })
+                .call(() => { resolve() }).start();
+        });
+    }
+    private _uiCanvas: Node;
+    private _uiRoot: Node;
+    private createFullScreenNode() {
+        let canvas = this._uiCanvas.getComponent(UITransform);
+        let node = new Node();
+        node.layer = this._uiCanvas.layer;
+        let uiTransform = node.addComponent(UITransform);
+        uiTransform.width = canvas.width;
+        uiTransform.height = canvas.height;
+
+        let widget = node.addComponent(Widget);
+        widget.isAlignBottom = true;
+        widget.isAlignTop = true;
+        widget.isAlignLeft = true;
+        widget.isAlignRight = true;
+
+        widget.left = 0;
+        widget.right = 0;
+        widget.top = 0;
+        widget.bottom = 0;
+        return node;
+    }
+    private getLayers(): GameUILayers[] {
+        return Object.values(GameUILayers).filter(value => typeof value === 'number') as GameUILayers[];
+    }
+
+    /**
+     * @en init,`don't call more than once`.
+     * @zh 初始化UIMgr,`不要多次调用`
+     *  */
+    public init(uiCanvas: Node | Prefab) {
+        if (this._uiCanvas) return;
+        if (!uiCanvas) throw error('uiCanvas must be a Node or Prefab');
+        if (uiCanvas instanceof Node) {
+            this._uiCanvas = uiCanvas;
+        }
+        else {
+            this._uiCanvas = instantiate(uiCanvas);
+            director.getScene().addChild(this._uiCanvas);
+        }
+        this._uiCanvas.name = '__ui_canvas__';
+        director.addPersistRootNode(this._uiCanvas);
+        if (!this._uiCanvas.getComponent(ui_updater)) {
+            this._uiCanvas.addComponent(ui_updater);
+        }
+        let canvas = this._uiCanvas.getComponent(UITransform);
+        this._uiCanvas.addComponent(ResolutionAutoFit);
+        this._uiRoot = this.createFullScreenNode();
+        this._uiRoot.name = 'root'
+        canvas.node.addChild(this._uiRoot);
+        const layers = this.getLayers();
+        //create layers
+        for (let i = 0; i < layers.length; ++i) {
+            let layerNode = this.createFullScreenNode();
+            layerNode.name = 'layer_' + GameUILayers[layers[i]];
+            this._uiRoot.addChild(layerNode);
+        }
+    }
+    /**获取层级节点*/
+    public getLayerNode(layerIndex: number): Node {
+        return this._uiRoot.children[layerIndex] || this._uiRoot;
+    }
+    /**关闭所有界面*/
+    public closeAll() {
+        ui_base.closeAll();
+    }
+    /**关闭和释放所有界面 */
+    public closeAndReleaseAll() {
+        ui_base.closeAndReleaseAll();
+    }
+    /**关闭某个界面*/
+    public close<T extends ui_base>(uiCls: Constructor<T>): void {
+        this.get(uiCls)?.close();
+    }
+    /**获取界面*/
+    public get<T extends ui_base>(uiCls: Constructor<T>): T {
+        let all = (ui_base as any)._uis;
+        for (let i = 0; i < all.length; ++i) {
+            let c = all[i];
+            if (c instanceof uiCls) {
+                return c;
+            }
+        }
+        return null;
+    }
+    /**某个界面是否显示中*/
+    public isShowing<T extends ui_base>(uiCls: Constructor<T>): boolean {
+        return (ui_base as any)._hasCls(uiCls);
+    }
+    private _clss_loading: Set<any> = new Set();
+    /**是否有正在加载中的界面*/
+    public isLoading<T extends ui_base>(uiCls: Constructor<T> | null): boolean {
+        if (!uiCls) return this._clss_loading.size > 0;
+        return this._clss_loading.has(uiCls);
+    }
+    /***
+     * @en show ui by the given parameters.
+     * @zh 显示UI
+     * @param uiCls the class, must inherits from the class `UIController`.
+     * @returns the instance of `uiCls`
+     *  */
+    public async show<T extends ui_base>(uiCls: Constructor<T>, ...data: any[]): Promise<T | null> {
+        if (this.isLoading(uiCls)) return null;
+        if (this.isShowing(uiCls)) return null;
+        let ui = new uiCls();
+        this._clss_loading.add(uiCls);
+        let bundleName = ui.bundle;
+        if (bundleName) {
+            let bundle = assetManager.getBundle(bundleName);
+            if (!bundle) {
+                try {
+                    const loadedBundle = await this.loadBundleAsync(bundleName);
+                    return await this._create(loadedBundle, ui, uiCls, ...data);
+                } catch (err) {
+                    console.error(err);
+                    this._clss_loading.delete(uiCls);
+                }
+            } else {
+                return await this._create(bundle, ui, uiCls, ...data);
+            }
+        } else {
+            this._clss_loading.delete(uiCls);
+            console.error("ui no bundle name");
+            return null;
+        }
+    }
+    private async _create<T extends ui_base>(bundle: AssetManager.Bundle, ui: T, uiCls: Constructor<T>, ...p: any[]): Promise<T> {
+        try {
+            const data: Prefab = await this.loadPrefabAsync(bundle, ui.prefab);
+            let node: Node = instantiate(data);
+            let parent = this.getLayerNode(ui.layer);
+            parent.addChild(node);
+            (ui as any)._setup(uiCls, node, ...p);
+            this._clss_loading.delete(uiCls);
+            return ui;
+        } catch (err) {
+            console.error(err);
+            this._clss_loading.delete(uiCls);
+            return;
+        }
+    }
+    private loadBundleAsync(bundleName: string): Promise<AssetManager.Bundle> {
+        return new Promise((resolve, reject) => {
+            assetManager.loadBundle(bundleName, null, (err, loadedBundle) => {
+                if (err) {
+                    reject(err);
+                } else {
+                    resolve(loadedBundle);
+                }
+            });
+        });
+    }
+    private loadPrefabAsync(bundle: AssetManager.Bundle, prefabName: string): Promise<Prefab> {
+        return new Promise((resolve, reject) => {
+            bundle.load(prefabName, (err, data: Prefab) => {
+                if (err) {
+                    reject(err);
+                } else {
+                    resolve(data);
+                }
+            });
+        });
+    }
+}
+export const gui = ui.getInstance();
+export { ui_base };

+ 1 - 0
idiom/assets/core/ui/ui.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"81e48c31-8c60-4c3e-9dab-40e2439634c6","files":[],"subMetas":{},"userData":{}}

+ 41 - 0
idiom/assets/core/ui/ui_ResolutionAutoFit.ts

@@ -0,0 +1,41 @@
+import { _decorator, Component, Node, size, Size, view,screen, ResolutionPolicy} from 'cc';
+const { ccclass, property } = _decorator;
+
+const CHECK_INTERVAL = 0.1;
+@ccclass('ch.ResolutionAutoFit')
+export class ResolutionAutoFit extends Component {
+    private _oldSize:Size = size();
+    start() {
+        this.adjustResolutionPolicy();
+    }
+    private lastCheckTime = 0;
+    update(deltaTime: number) {
+        this.lastCheckTime+=deltaTime;
+        if(this.lastCheckTime < CHECK_INTERVAL){
+            return;
+        }
+        this.lastCheckTime = 0;
+
+        this.adjustResolutionPolicy();
+    }
+
+    adjustResolutionPolicy(){
+        let winSize = screen.windowSize;
+        if(!this._oldSize.equals(winSize)){
+            let ratio = winSize.width / winSize.height;
+            let drs = view.getDesignResolutionSize();
+            let drsRatio = drs.width / drs.height;
+
+            if(ratio > drsRatio){
+                //wider than desgin. fixed height
+                view.setResolutionPolicy(ResolutionPolicy.FIXED_HEIGHT);
+            }
+            else{
+                //
+                view.setResolutionPolicy(ResolutionPolicy.FIXED_WIDTH);
+            }
+            this._oldSize.set(winSize);
+        }
+    }
+}
+

+ 9 - 0
idiom/assets/core/ui/ui_ResolutionAutoFit.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "c7d72489-a84b-4886-92e2-028dc77602d1",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 358 - 0
idiom/assets/core/ui/ui_base.ts

@@ -0,0 +1,358 @@
+import { _decorator, assetManager, Button, Component, EventHandler, EventTouch, find, isValid, Node, Prefab, Toggle, ToggleContainer } from 'cc';
+const { ccclass, property } = _decorator;
+/***
+ * @en internal class, used for handling node event.
+ * @zh 内部类,用于节点事件监听
+ * 
+ *  */
+@ccclass('tgxNodeEventAgent')
+class __NodeEventAgent__ extends Component {
+    /***
+     * @en recieve button click event and deliver them to the real handlers.
+     * @zh 接受按钮事件,并转发给真正的处理函数
+     * */
+    onButtonClicked(evt: EventTouch, customEventData) {
+        let btn = (evt.target as Node).getComponent(Button);
+        let clickEvents = btn.clickEvents;
+        for (let i = 0; i < clickEvents.length; ++i) {
+            let h = clickEvents[i];
+            if (h.customEventData == customEventData) {
+                let cb = h['$cb$'];
+                let target = h['$target$']
+                let args = h['$args$'];
+                cb.apply(target, [btn, args]);
+            }
+        }
+    }
+
+    /***
+     * @en recieve toggle event and deliver them to the real handlers.
+     * @zh 接受Toggle事件,并转发给真正的处理函数
+     * */
+    onToggleEvent(toggle: Toggle, customEventData) {
+        let checkEvents = toggle.checkEvents;
+        //if (toggle['_toggleContainer']) {
+        //    checkEvents = toggle['_toggleContainer'].checkEvents;
+        //}
+        for (let i = 0; i < checkEvents.length; ++i) {
+            let h = checkEvents[i];
+            if (h.customEventData == customEventData) {
+                let cb = h['$cb$'];
+                let target = h['$target$']
+                let args = h['$args$'];
+                cb.apply(target, [toggle, args]);
+            }
+        }
+    }
+}
+let _id:number=0;
+export default class ui_base  {
+    private static _clss:Set<any>=new Set();
+    private static _uis: ui_base[] = [];
+    /***
+    * @en hide and destroy all ui panel.
+    * @zh 隐藏并销毁所有UI面板
+    * */
+    public static closeAll() {
+        while (this._uis.length) {
+            this._uis[0].close();
+        }
+        this._clss.clear();
+    }
+    //
+    public static closeAndReleaseAll(){
+        while (this._uis.length) {
+            this._uis[0].closeAndRelease();
+        }
+        this._clss.clear();
+    }
+    //update all ui, called by UI.
+    public static updateAll(dt:number) {
+        for (let i = 0; i < this._uis.length; ++i) {
+            let ctrl = this._uis[i];
+            if (ctrl.node && isValid(ctrl.node)) {
+                this._uis[i].onUpdate(dt);
+            }
+        }
+    }
+    private static _addCls(cls:any):void{
+        if(!cls) return;this._clss.add(cls);
+    }
+    private static _removeCls(cls:any):void{
+        if(!cls) return;this._clss.delete(cls);
+    }
+    private static _hasCls(cls:any):boolean{
+        return this._clss.has(cls);
+    }
+    private _id: number = 0;
+    private _bundle:string;
+    private _prefab: string;
+    private _layer: number;
+    private _layout: any;
+    private _cls:any;
+    protected node: Node;
+    private _destroyed:boolean = false;
+    /***
+     * @en the instance id to indicate an unique ui panel.
+     * @zh 实例ID,用于标记一个唯一面板实例
+     *  */
+    public get id(): number {return this._id;}
+    /***
+    * @en url of the prefab used by this ui panel.
+    * @zh 本UI使用prefab路径
+    *  */
+    public get prefab(): string  {return this._prefab;}
+    public get bundle():string{return this._bundle;}
+    /***
+     * @en layer of this ui panel.
+     * @zh 本UI所在的UI层级
+     *  */
+    public get layer(): number {return this._layer;}
+    /***
+     * @en layout of this ui panel.
+     * @zh 本UI组件
+     *  */
+    public get layout(): Component { return this._layout;}
+    public getLayout<T extends Component>():T{ return this._layout as T;}
+    constructor(bundle:string,prefab: string, layer: number, layoutCls: any) {
+        this._cls = null;
+        this._bundle=bundle;
+        this._prefab = prefab;
+        this._layer = layer;
+        this._layout = layoutCls;
+        this._id = _id++;
+    }
+    //setup this ui,called by UIMgr.
+    private _setup(cls:any,node: Node,...data: any[]) {
+        ui_base._uis.push(this);
+        this._cls=cls;
+        (ui_base as any)._addCls(this._cls);
+        this.node = node;
+        if (this._layout) this._layout = this.node.getComponent(this._layout);
+        //notify sub class to handle something.
+        //节点创建完毕,调用子类的处理函数。
+        this.onCreated(...data);
+        //check whether it has been destroyed, if has, hide it.
+        //检查是否为已销毁,如果已销毁,则走销毁流程
+        if(this._destroyed) this.close();
+    }
+    /**
+     * @en hide and destroy this ui panel.
+     * @zh 隐藏并销毁此UI面板
+     *  */
+    public close(){
+        this._resolve_close?.();
+        this._resolve_close=null;
+        this._destroyed = true;
+        if(!this.node) return;
+        this.node.removeFromParent();
+        for (let i = 0; i < ui_base._uis.length; ++i) {
+            if (ui_base._uis[i] == this) {
+                ui_base._uis.splice(i, 1);
+                break;
+            }
+        }
+        this.onDispose();
+        this.node.destroy();
+        this.node = null;
+        (ui_base as any)._removeCls(this._cls);
+        this._cls=null;
+    }
+    public closeAndRelease(){
+        this.close();
+        assetManager.getBundle(this._bundle)?.release(this._prefab);
+    }
+    private _resolve_close: (() => void) | null = null;
+    /**等待此ui关闭*/
+    public wait_close():Promise<void>{
+        if(this._resolve_close) return;
+        return new Promise((resolve) => {this._resolve_close = resolve;});
+    }
+    /**
+     * @en add button event handler
+     * @zh 添加按钮事件
+     * @param relativeNodePath to indicate a button node, can pass `string`|`Node`|`Button` here.
+     * @param cb will be called when event emits. method format:(btn:Button,args:any)=>void
+     * @param target the `this` argument of `cb`
+     *  */
+    onButtonEvent(relativeNodePath: string | Node | Button, cb: Function, target?: any, args?: any) {
+        let buttonNode: Node = null;
+        if (relativeNodePath instanceof Node) {
+            buttonNode = relativeNodePath;
+        }
+        else if (relativeNodePath instanceof Button) {
+            buttonNode = relativeNodePath.node;
+        }
+        else {
+            buttonNode = find(relativeNodePath, this.node);
+        }
+        if (!buttonNode) {
+            return null;
+        }
+        //添加转发器
+        let agent = this.node.getComponent(__NodeEventAgent__);
+        if (!agent) {
+            agent = this.node.addComponent(__NodeEventAgent__);
+        }
+
+        let btn = buttonNode.getComponent(Button);
+        let clickEvents = btn.clickEvents;
+        let handler = new EventHandler();
+        handler.target = this.node;
+        handler.component = 'tgxNodeEventAgent';
+        handler.handler = 'onButtonClicked';
+        handler.customEventData = '' + _id++;
+
+        //附加额外信息 供事件转发使用
+        handler['$cb$'] = cb;
+        handler['$target$'] = target;
+        handler['$args$'] = args;
+
+        clickEvents.push(handler);
+        btn.clickEvents = clickEvents;
+    }
+
+    /**
+     * @en remove button event handler
+     * @zh 移除按钮事件
+     * @param relativeNodePath to indicate a button node, can pass `string`|`Node`|`Button` here.
+     * @param cb will be called when event emits.
+     * @param target the `this` argument of `cb`
+     *  */
+    offButtonEvent(relativeNodePath: string | Node | Button, cb: Function, target: any) {
+        let buttonNode: Node = null;
+        if (relativeNodePath instanceof Node) {
+            buttonNode = relativeNodePath;
+
+        }
+        else if (relativeNodePath instanceof Button) {
+            buttonNode = relativeNodePath.node;
+        }
+        else {
+            buttonNode = find(relativeNodePath, this.node);
+        }
+
+        if (!buttonNode) {
+            return; ``
+        }
+        let agent = this.node.getComponent(__NodeEventAgent__);
+        if (!agent) {
+            return;
+        }
+        let btn = buttonNode.getComponent(Button);
+        if (!btn) {
+            return;
+        }
+        let clickEvents = btn.clickEvents;
+        for (let i = 0; i < clickEvents.length; ++i) {
+            let h = clickEvents[i];
+            if (h['$cb$'] == cb && h['$target$'] == target) {
+                clickEvents.splice(i, 1);
+                btn.clickEvents = clickEvents;
+                break;
+            }
+        }
+    }
+
+    /**
+     * @en add toggle event handler
+     * @zh 添加Toggle事件
+     * @param relativeNodePath to indicate a button node, can pass `string`|`Node`|`Button` here.
+     * @param cb will be called when event emits. method format:(btn:Toggle,args:any)=>void
+     * @param target the `this` argument of `cb`
+      */
+
+    onToggleEvent(relativeNodePath: string | Node | Toggle | ToggleContainer, cb: Function, target?: any, args?: any) {
+        let buttonNode: Node = null;
+        if (relativeNodePath instanceof Node) {
+            buttonNode = relativeNodePath;
+        }
+        else if (relativeNodePath instanceof Toggle) {
+            buttonNode = relativeNodePath.node;
+        }
+        else if (relativeNodePath instanceof ToggleContainer) {
+            buttonNode = relativeNodePath.node;
+        }
+        else {
+            buttonNode = find(relativeNodePath, this.node);
+        }
+
+        if (!buttonNode) {
+            return null;
+        }
+
+        //添加转发器
+        let agent = this.node.getComponent(__NodeEventAgent__);
+        if (!agent)  agent = this.node.addComponent(__NodeEventAgent__);
+        let btn = buttonNode.getComponent(Toggle) as any;
+        if (!btn)  btn = buttonNode.getComponent(ToggleContainer) as any;
+        let checkEvents = btn.checkEvents;
+        let handler = new EventHandler();
+        handler.target = this.node;
+        handler.component = 'tgxNodeEventAgent';
+        handler.handler = 'onToggleEvent';
+        handler.customEventData = '' + _id++;
+
+        //附加额外信息 供事件转发使用
+        handler['$cb$'] = cb;
+        handler['$target$'] = target;
+        handler['$args$'] = args;
+
+        checkEvents.push(handler);
+        btn.checkEvents = checkEvents;
+    }
+
+    /**
+     * @en remove toggle event handler
+     * @zh 移除Toggle事件
+     * @param relativeNodePath to indicate a button node, can pass `string`|`Node`|`Button` here.
+     * @param cb will be called when event emits. method format:(btn:Toggle,args:any)=>void
+     * @param target the `this` argument of `cb`
+     *  */
+    offToggleEvent(relativeNodePath: string | Node | Toggle | ToggleContainer, cb: Function, target: any) {
+        let buttonNode: Node = null;
+        if (relativeNodePath instanceof Node) {
+            buttonNode = relativeNodePath;
+        }
+        else if (relativeNodePath instanceof Toggle) {
+            buttonNode = relativeNodePath.node;
+        }
+        else if (relativeNodePath instanceof ToggleContainer) {
+            buttonNode = relativeNodePath.node;
+        }
+        else {
+            buttonNode = find(relativeNodePath, this.node);
+        }
+
+        if (!buttonNode) {
+            return null;
+        }
+
+        //添加转发器
+        let agent = this.node.getComponent(__NodeEventAgent__);
+        if (!agent) {
+            return;
+        }
+        let btn = buttonNode.getComponent(Toggle) as any;
+        if (!btn) {
+            btn = buttonNode.getComponent(ToggleContainer) as any;
+        }
+        let checkEvents = btn.checkEvents;
+        for (let i = 0; i < checkEvents.length; ++i) {
+            let h = checkEvents[i];
+            if (h['$cb$'] == cb && h['$target$'] == target) {
+                checkEvents.splice(i, 1);
+                btn.checkEvents = checkEvents;
+                break;
+            }
+        }
+    }
+    //子类的所有操作,需要在这个函数之后。
+    protected onCreated(...data: any[]){}
+    //当界面销毁时调用
+    protected onDispose(){}
+    //
+    protected onUpdate(dt?:number) { }
+}
+
+

+ 9 - 0
idiom/assets/core/ui/ui_base.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "a34ebbf0-9efe-44d8-b1b6-27c447c17536",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
idiom/assets/core/util.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "3e9ea6dc-0e45-4763-812d-7a1c1d325a49",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 209 - 0
idiom/assets/core/util/ArrayUtil.ts

@@ -0,0 +1,209 @@
+export default class ArrayUtil {
+    /**随机打乱一个数组 */
+    public static  shuffleArray<T>(array: T[]): T[] {
+        let currentIndex = array.length;
+        let temporaryValue;
+        let randomIndex;
+      
+        // While there remain elements to shuffle...
+        while (currentIndex !== 0) {
+          // Pick a remaining element...
+          randomIndex = Math.floor(Math.random() * currentIndex);
+          currentIndex--;
+      
+          // And swap it with the current element.
+          temporaryValue = array[currentIndex];
+          array[currentIndex] = array[randomIndex];
+          array[randomIndex] = temporaryValue;
+        }
+        return array;
+    }
+    // Fisher-Yates 洗牌算法
+    //const originalArray = [1, 2, 3, 4, 5];  
+    //const shuffledArray = shuffleArray([...originalArray]);
+    public static shuffleArray2<T>(array: T[]): T[] {  
+        for (let i = array.length - 1; i > 0; i--) {  
+            // 生成一个随机索引  
+            const j = Math.floor(Math.random() * (i + 1));  
+            // 交换元素  
+            [array[i], array[j]] = [array[j], array[i]];  
+        }  
+        return array;  
+    }  
+    /**合并两个数组元素,移除相同的 */
+    public static MergeAndRemoveDuplicates<T>(arr1: T[], arr2: T[]): T[] {
+        const merged = [...arr1, ...arr2]; // 合并两个数组
+        const uniqueValues = Array.from(new Set(merged)); // 去重
+        return uniqueValues;
+    }
+    /**排除数组1在数组2里有的元素,返回新数组*/
+    public static ExcludeElements<T>(array1:T[],array2:T[]):T[]{
+        const set = new Set(array2); // 将 array2 转换为 Set
+        return array1.filter(item => !set.has(item));
+    }
+    /**检测数组元素是否能满足顺序的组合*/
+    public static checkSequentialCombination(arr: number[]): number[] {
+        const sortedArr = arr.slice().sort((a, b) => a - b);
+        for (let i = 0; i < sortedArr.length - 1; i++) {
+            if (sortedArr[i] + 1 !== sortedArr[i + 1]) {
+                return [];
+            }
+        }
+        return sortedArr;
+    }
+    /**从一个数组中找出是否有连续的组合*/
+    public static findSequentialCombinations(arr: number[]): number[][] {
+        const result: number[][] = [];
+        arr.sort((a, b) => a - b);
+        let currentSequence: number[] = [];
+        for (let i = 0; i < arr.length; i++) {
+            if (currentSequence.length === 0 || arr[i] === currentSequence[currentSequence.length - 1] + 1) {
+                currentSequence.push(arr[i]);
+            } else {
+                if (currentSequence.length > 1) {
+                    result.push(currentSequence);
+                }
+                currentSequence = [arr[i]];
+            }
+        }
+        if (currentSequence.length > 1) {
+            result.push(currentSequence);
+        }
+        return result;
+    }
+    /*eg:const arr = [2, 4,2, 5, 3, 6, 9,9,10,11,];
+    const combinations = this.findSequentialCombinations(arr);
+    
+    if (combinations.length > 0) {
+        console.log("所有可能的连续顺序组合为:");
+        combinations.forEach(combination => {
+            console.log(combination);
+        });
+    } else {
+        console.log("无法找到连续顺序组合");
+    }*/
+
+    /**数组中删除一个合条件的*/
+    public static DeleteOneItem<T>(list:Array<T>,check:(item:T)=>boolean):T|null{
+        let length=list.length;
+        for(let i:number=0;i<length;i++){
+            if(check(list[i])){
+                return list.splice(i,1)[0];
+                //i--;
+                //length--;
+            }
+        }
+        return null;
+    }
+
+    /**插入排序(适用于差异不会很大,相对有序的数据)Array.sort() 快速排序 算法更适用于混乱无章的数据 */
+    public static InsertionSort(array:any[]):void{
+            const count = array.length;
+            if(count<=1)return;
+            let t1:any;
+            let t2:any;
+            for(let i=1;i<count;i++){
+                t1 = array[i];
+                let j:number;
+                for(j=i;j>0 && t1.sortValue>(t2=array[j-1]).sortValue;j--){
+                    array[j]=t2;
+                }
+                array[j]=t1;
+            }
+    }
+    // 元素排序
+    /*示例
+     负数在前
+    const checkMinusNumber = function (val: number) {
+            return val > 0;
+    };
+    const arr = [2, 4, 5, 6, 7, -8, -10 - 12, -2];
+    adjustArrayOrder.reorder(arr, checkMinusNumber);
+    console.log(arr);
+    */
+    public static reorder(arr: Array<number>, checkFun: (checkVal: number) => boolean): void {
+         let end = arr.length - 1;
+         let begin =0;
+         while (begin < end) {
+               // 向后移动begin
+               while (begin < end && !checkFun(arr[begin])) {
+                     begin++;
+               }
+               // 向前移动end
+               while (begin < end && checkFun(arr[end])) {
+                   end--;
+               }
+               // begin与end都指向了正确的位置
+               if (begin < end) {
+                    // 交换两个元素的顺序
+                    [arr[begin], arr[end]] = [arr[end], arr[begin]];
+               }
+          }
+    }
+    //冒泡排序
+    /// <summary>
+	/// 各种类型冒泡排序比
+	 /// </summary>/// 例子:
+                      /// CommonSort<int>(new int[]{2,3,1,45,123,4},compare)
+                      /// bool compare(int a,int b){
+                      ///    	 if(a>b)return true;
+                      ///   	 reutn false;
+                      /// }
+	/// <param name="sortArray"></param>
+	/// <param name="compareMethod"></param>
+	/// <typeparam name="T"></typeparam>
+	static  CommonSort<T>(sortArray:Array<T>, compareMethod:(a:T,b:T)=>boolean):void
+	{
+		let swapped = true;
+		do
+		{
+			swapped = false;
+			for (let i = 0; i < sortArray.length - 1; i++)
+			{
+				if (compareMethod(sortArray[i], sortArray[i + 1]))
+				{
+					let temp = sortArray[i];
+					sortArray[i] = sortArray[i + 1];
+					sortArray[i + 1] = temp;
+					swapped = true;
+				}
+			}
+		} while (swapped);
+    }
+    /**归并排序 */
+    /*var arr: number[] = [];
+	for (var i: number = 0; i < 1000; i++) {
+	    arr.push(Math.floor(1000 - i));//生成降序的数组
+	}
+	MergeSort(arr, 0, arr.length);//调用归并排序算法*/
+    //拆分数组  分
+    static MergeSort(arr: number[], lo: number, hi: number): void {
+        if (hi - lo < 2) return;//单个元素无需考虑
+        var mi: number = (lo + hi) >> 1;//已中点为界 或者改成Math.floor((lo + hi) / 2)
+        this.MergeSort(arr, lo, mi);//对左边排序
+        this.MergeSort(arr, mi, hi);//对右边排序
+        this.merge(arr, lo, mi, hi);//归并
+    }
+    //归并算法实现 合
+    private static  merge(arr: number[], lo: number, mi: number, hi: number): void {
+        var A: number[] = arr.slice(lo, hi);//A[lo, hi)=arr[lo, hi)
+        var lb: number = mi - lo;
+        var B: number[] = new Array(lb);
+        for (var i = 0; i < lb; B[i] = A[i++]);//复制左子向量B[0, lb) = arr[lo, mi)
+        var lc: number = hi - mi;
+        var C: number[] = arr.slice(mi, hi);//后子向量C[0,lc) = arr[mi, hi)
+        for (var i = 0, j = 0, k = 0; j < lb;) {//反复冲B和C中取出更小者
+            if (k < lc && C[k] < B[j]) {//将其归入A中
+                A[i++] = C[k++];
+            }
+            if (lc <= k || B[j] <= C[k]) {
+                A[i++] = B[j++];
+            }
+        }
+        for (var i = 0; i < A.length; arr[lo + i] = A[i++]);//把A中的值赋给原来的数组
+        B.length = 0;
+        C.length = 0;
+        A.length = 0;
+    }
+}
+

+ 9 - 0
idiom/assets/core/util/ArrayUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "8334108e-7a2b-4f44-81dc-c35f154bb837",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 420 - 0
idiom/assets/core/util/DataTimeUtil.ts

@@ -0,0 +1,420 @@
+export default class  DataTimeUtil {
+        /**时间偏移*/
+        public static offset: number = 0;
+        /**
+         * 当前时间戳(毫秒)
+         * @returns 
+         */
+        public static currentTimeMillis (): number {
+            return new Date().getTime() + this.offset;
+        }
+    
+        /**
+         * 当前时间戳(秒)
+         * @returns
+         */
+        public static currentTimeSeconds (): number {
+            return Math.floor((new Date().getTime() + this.offset) / 1000);
+        }
+        /**当前服务器日期*/
+        public static currentDate():Date{
+            return new Date(this.currentTimeMillis ());
+        }
+        /**将毫秒时间戳转到当天0点时的毫秒时间戳*/
+        public static timestampToStartOfDay(timestamp:number):number {
+            let date = new Date(timestamp); // 将时间戳转换为日期对象
+            date.setHours(0, 0, 0, 0); // 将时、分、秒、毫秒部分设置为0
+            return date.getTime(); // 将日期转换回时间戳
+        }
+        /**将秒时间戳转到当天0点时的秒时间戳*/
+        public static timestampSecondsToStartOfDay(timestamp:number):number {
+            let date = new Date(timestamp*1000); // 将时间戳转换为日期对象
+            date.setHours(0, 0, 0, 0); // 将时、分、秒、毫秒部分设置为0
+            return Math.floor(date.getTime()/1000); // 将日期转换回时间戳
+        }
+        /**获取服务器到明天0点时长 */
+        public static longToTomorrow():number{
+            // 获取当前时间
+           const now = this.currentDate();
+           // 获取明天的日期
+           const tomorrow = new Date(now);
+           tomorrow.setDate(now.getDate() + 1);
+           tomorrow.setHours(0, 0, 0, 0);
+           // 计算时间差
+           return tomorrow.getTime() - now.getTime();
+        }
+        /**当前服务器月份*/
+        public static currentMonth():number{
+              return this.currentDate().getMonth() + 1; // 月份从0开始,所以需要加1
+        }
+        /**当前服务器日期月有多少天 */
+        public static daysInCurrentMonth():number{
+              const currentYear = this.currentDate().getFullYear();
+              const daysInCurrentMonth = new Date(currentYear, this.currentMonth(), 0).getDate();
+              return daysInCurrentMonth;
+        }
+        public static timeMillisToDate(ms:number):Date{
+            return new Date(ms);
+        }
+        public static stringToStamp(dateString:string):number{
+            const timestamp: number = Date.parse(dateString) - new Date().getTimezoneOffset() * 60 * 1000;
+            return timestamp;
+        }
+        public static stringToDate(dateString:string):Date{
+            return new Date(dateString);
+        }
+        public static dateToString(date:Date):string{
+             const year: number = date.getFullYear();
+             const month: number = date.getMonth() + 1;
+             const day: number = date.getDate();
+             const hours: number = date.getHours();
+             const minutes: number = date.getMinutes();
+             const seconds: number = date.getSeconds();
+             // 自定义格式化日期和时间
+             const formattedDateTime: string = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+             return formattedDateTime;
+        }
+        /**本周一0点的时间戳*/
+        public static mondayTimestamp():number{
+            const currentDate=this.currentDate();
+            // 获取当前日期的星期几(星期天为0,星期一为1,以此类推)
+            const currentDayOfWeek = currentDate.getDay();
+            // 计算当前日期距离周一的天数差值
+            const daysUntilMonday = (currentDayOfWeek === 0) ? 6 : currentDayOfWeek - 1;
+            return currentDate.getTime() - (daysUntilMonday * 24 * 60 * 60 * 1000);
+        }
+
+
+        /*private _day_s = 864e5;
+        private _diff:number=0;
+        private updateServerTime(s:number):void{ this._diff = s - (new Date).getTime()}
+        public now():number{return this.getTime()}
+        public getTime():number { return (new Date).getTime() + this._diff;} 
+        public getDayStartTime(s:number):number {return new Date(s).setHours(0, 0, 0, 0)}
+        public getDayEndTime(e:number):number { return new Date(e).setHours(23, 59, 59, 999)}
+        public getWeekEndTime (e:number):number {
+              var t = new Date(e).getDay();
+              return this.getDayEndTime(e) + (0 === t ? 0 : (7 - t) * this._day_s)
+        }
+        public getMonthEndTime(e:number):number {
+              var t = new Date(e);
+              return 11 === t.getMonth() ? t.setFullYear(t.getFullYear() + 1, 0, 0) : t.setMonth(t.getMonth() + 1, 0), t.setHours(23, 59, 59, 999)
+        }
+        public getDiffDayNum (e:number, t:number) {
+              var r = this.getDayStartTime(e),o = this.getDayStartTime(t);
+              return Math.ceil(Math.abs(r - o) / this._day_s)
+        }*/
+
+
+
+
+        /**获取时间戳*/
+        private static getTickCount() {
+            if(window && window.performance){
+                return window.performance.now();
+            }
+            return (new Date()).getTime();
+        }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+    //-----------------------------------------------------------------------------------------------------------------------------
+    /**
+     * 根据秒数格式化字符串
+     * @param second 秒数
+     * @param type 1:00:00:00   2:yyyy-mm-dd h:m:s    3:00:00(分:秒)   4:xx天前,xx小时前,xx分钟前    6:00:00(时:分)  
+     * @return
+     *
+     */
+    public static getFormatBySecond(second: number, type: number = 1): string {
+        var str: string = "";
+        switch (type) {
+            case 1:
+                str = this.getFormatBySecond1(second);
+                break;
+            case 2:
+                str = this.getFormatBySecond2(second);
+                break;
+            case 3:
+                str = this.getFormatBySecond3(second);
+                break;
+            case 4:
+                str = this.getFormatBySecond4(second);
+                break;
+            case 5:
+                str = this.getFormatBySecond5(second);
+                break;
+            case 6:
+                str = this.getFormatBySecond6(second);
+                break;
+        }
+        return str;
+    }
+
+    //1: 00:00:00
+    private static getFormatBySecond1(t: number = 0): string {
+        var hourst: number = Math.floor(t / 3600);
+        var hours: string;
+        if (hourst == 0) {
+            hours = "00";
+        } else {
+            if (hourst < 10)
+                hours = "0" + hourst;
+            else
+                hours = "" + hourst;
+        }
+        var minst: number = Math.floor((t - hourst * 3600) / 60);
+        var secondt: number = Math.floor((t - hourst * 3600) % 60);
+        var mins: string;
+        var sens: string;
+        if (minst == 0) {
+            mins = "00";
+        } else if (minst < 10) {
+            mins = "0" + minst;
+        } else {
+            mins = "" + minst;
+        }
+        if (secondt == 0) {
+            sens = "00";
+        } else if (secondt < 10) {
+            sens = "0" + secondt;
+        } else {
+            sens = "" + secondt;
+        }
+        return hours + ":" + mins + ":" + sens;
+    }
+
+    //3:00:00(分:秒)
+    private static getFormatBySecond3(t: number = 0): string {
+        var hourst: number = Math.floor(t / 3600);
+        var minst: number = Math.floor((t - hourst * 3600) / 60);
+        var secondt: number = Math.floor((t - hourst * 3600) % 60);
+        var mins: string;
+        var sens: string;
+        if (minst == 0) {
+            mins = "00";
+        } else if (minst < 10) {
+            mins = "0" + minst;
+        } else {
+            mins = "" + minst;
+        }
+        if (secondt == 0) {
+            sens = "00";
+        } else if (secondt < 10) {
+            sens = "0" + secondt;
+        } else {
+            sens = "" + secondt;
+        }
+        return mins + ":" + sens;
+    }
+
+    //2:yyyy-mm-dd h:m:s
+    private static getFormatBySecond2(time: number): string {
+        var date: Date = new Date(time*1000);
+        var year: number = date.getFullYear();
+        var month: number = date.getMonth() + 1; 	//返回的月份从0-11;
+        var day: number = date.getDate();
+        var hours: number = date.getHours();
+        var minute: number = date.getMinutes();
+        var second: number = date.getSeconds();
+        return year + "-" + month + "-" + day + " " + hours + ":" + minute + ":" + second;
+
+    }
+
+    //4:xx天前,xx小时前,xx分钟前
+    private static getFormatBySecond4(time: number): string {
+        var t = Math.floor(time / 3600);
+        if (t > 0) {
+            if (t > 24) {
+                return Math.floor(t / 24) + "天前";
+            }
+            else {
+                return t + "小时前";
+            }
+        }
+        else {
+            return Math.floor(time / 60) + "分钟前";
+        }
+    }
+
+    private static getFormatBySecond5(time: number): string {
+        //每个时间单位所对应的秒数
+        var oneDay: number = 3600 * 24;
+        var oneHourst: number = 3600;
+        var oneMinst: number = 60;
+
+        var days = Math.floor(time / oneDay);
+        var hourst: number = Math.floor(time % oneDay / oneHourst)
+        var minst: number = Math.floor((time - hourst * oneHourst) / oneMinst)  //Math.floor(time % oneDay % oneHourst / oneMinst);
+        var secondt: number = Math.floor((time - hourst * oneHourst) % oneMinst) //time;
+
+        var dayss: string = "";
+        var hourss: string = ""
+        var minss: string = "";
+        var secss: string = ""
+        if (time > 0) {
+            //天
+            if (days == 0) {
+                dayss = "";
+                //小时
+                if (hourst == 0) {
+                    hourss = "";
+                    //分
+                    if (minst == 0) {
+                        minss = "";
+                        if (secondt == 0) {
+                            secss = "";
+                        } else if (secondt < 10) {
+                            secss = "0" + secondt + "秒";
+                        } else {
+                            secss = "" + secondt + "秒";
+                        }
+
+                        return secss;
+                    }
+                    else {
+                        minss = "" + minst + "分";
+                        if (secondt == 0) {
+                            secss = "";
+                        } else if (secondt < 10) {
+                            secss = "0" + secondt + "秒";
+                        } else {
+                            secss = "" + secondt + "秒";
+                        }
+
+                    }
+
+                    return minss + secss;
+                }
+                else {
+                    hourss = hourst + "小时";
+                    if (minst == 0) {
+                        minss = "";
+                        if (secondt == 0) {
+                            secss = "";
+                        } else if (secondt < 10) {
+                            secss = "0" + secondt + "秒";
+                        } else {
+                            secss = "" + secondt + "秒";
+                        }
+
+                        return secss
+
+                    } else if (minst < 10) {
+                        minss = "0" + minst + "分";
+                    } else {
+                        minss = "" + minst + "分";
+                    }
+
+                    return hourss + minss;
+
+                }
+            }
+            else {
+                dayss = days + "天";
+                if (hourst == 0) {
+                    hourss = "";
+                } else {
+                    if (hourst < 10)
+                        hourss = "0" + hourst + "小时";
+                    else
+                        hourss = "" + hourst + "小时";
+                    ;
+                }
+                return dayss + hourss;
+            }
+        }
+        return "";
+    }
+
+    //6:00:00(时:分) 
+    private static getFormatBySecond6(t: number = 0): string {
+        var hourst: number = Math.floor(t / 3600);
+        var minst: number = Math.floor((t - hourst * 3600) / 60);
+        var houers: string;
+        var mins: string;
+        if (hourst == 0) {
+            houers = "00";
+        } else if (hourst < 10) {
+            houers = "0" + hourst;
+        } else {
+            houers = "" + hourst;
+        }
+        if (minst == 0) {
+            mins = "00";
+        } else if (minst < 10) {
+            mins = "0" + minst;
+        } else {
+            mins = "" + minst;
+        }
+        return houers + ":" + mins;
+    }
+
+
+    /**
+     * 获取当前是周几
+     * ["星期日","星期一","星期二","星期三","星期四","星期五","星期六"]
+     */
+    public static getDay(timestamp: number): number {
+        let date = new Date(timestamp);
+        return date.getDay();
+    }
+
+    /**
+     * 判定两个时间是否是同一天
+     */
+    public static isSameDate(timestamp1: number, timestamp2: number): boolean {
+        let date1 = new Date(timestamp1);
+        let date2 = new Date(timestamp2);
+        return date1.getFullYear() == date2.getFullYear()
+            && date1.getMonth() == date2.getMonth()
+            && date1.getDate() == date2.getDate();
+    }
+
+    /**
+     * 日期格式化
+     */
+    public static format(d: Date, fmt: string = "yyyy-MM-dd hh:mm:ss"): string {
+        let o = {
+            "M+": d.getMonth() + 1, //month
+            "d+": d.getDate(),    //day
+            "h+": d.getHours(),   //hour
+            "m+": d.getMinutes(), //minute
+            "s+": d.getSeconds(), //second
+            "q+": Math.floor((d.getMonth() + 3) / 3),  //quarter
+            "S": d.getMilliseconds() //millisecond
+        }
+        if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1,
+            (d.getFullYear() + "").substr(4 - RegExp.$1.length));
+        for (var k in o) if (new RegExp("(" + k + ")").test(fmt))
+            fmt = fmt.replace(RegExp.$1,
+                RegExp.$1.length == 1 ? o[k] :
+                    ("00" + o[k]).substr(("" + o[k]).length));
+        return fmt;
+    }
+    /**
+     * 计算两个时间(秒)相差天数(不满一天算0天)
+     */
+    public static getDayCountSeconds(timestamp1: number, timestamp2: number): number {
+        const d_value: number = Math.abs(timestamp2 - timestamp1);
+        return Math.floor(d_value / (24 * 60 * 60));
+    }
+    /**
+     * 计算两个时间相差天数
+     */
+    public static dateDifference(timestamp1: number, timestamp2: number): number {
+        const d_value: number = Math.abs(timestamp2 - timestamp1);
+        return Math.ceil(d_value / (24 * 60 * 60 * 1000));
+    }
+}

+ 9 - 0
idiom/assets/core/util/DataTimeUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "6aa3e5d2-e8f8-414a-ba3b-5f2c9e97456c",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 172 - 0
idiom/assets/core/util/DirectorUtil.ts

@@ -0,0 +1,172 @@
+import { director } from "cc";
+import { TweenSystem } from "cc";
+import { PhysicsSystem } from "cc";
+import { PhysicsSystem2D } from "cc";
+import { AnimationManager } from "cc";
+import { sp } from "cc";
+import { Animation, AnimationState, Node } from "cc";
+export default class DirectorUtil {
+	//没有用到龙骨 dragon_bones_as:dragonBones.ArmatureDisplay[]
+	private static pause_data: { state_b: boolean, physics_2d_b: boolean, physics_3d_b: boolean, scheduler_as: any[], anim_as: AnimationState[], tween_target_as: any[], spine_as: sp.Skeleton[] } = {
+		/**暂停状态 */
+		state_b: false,
+		/**2d物理系统状态 */
+		physics_2d_b: false,
+		/**3d物理系统状态 */
+		physics_3d_b: false,
+		/**定时器对象列表 */
+		scheduler_as: [],
+		/**动画列表 */
+		anim_as: [],
+		/**缓动对象列表 */
+		tween_target_as: [],
+		/**龙骨组件列表 */
+		//dragon_bones_as:[],
+		spine_as: []
+	};
+	/**暂停游戏 */
+	static pause(config_?: {/**排除节点 */exclude_as?: Node[];/**递归排除节点 */recu_exclude_as?: Node[] }): void {
+		if (this.pause_data.state_b) return;
+		// 暂停定时器
+		this.pause_data.scheduler_as = director.getScheduler().pauseAllTargets();
+		// 暂停当前动画
+		{
+			let anim_system = director.getSystem(AnimationManager.ID);
+			this.pause_data.anim_as.splice(0, this.pause_data.anim_as.length, ...anim_system["_anims"].array);
+			this.pause_data.anim_as.forEach(v1 => { v1.pause(); });
+		}
+		// 暂停spine动画
+		{
+			this.pause_data.spine_as = director.getScene().getComponentsInChildren(sp.Skeleton);
+			this.pause_data.spine_as.forEach(v1 => { v1.timeScale = 0; });
+		}
+		// 暂停龙骨动画
+		// {
+		//this.pause_data.dragon_bones_as = director.getScene().getComponentsInChildren(dragonBones.ArmatureDisplay);
+		//this.pause_data.dragon_bones_as.forEach(v1 => {v1.timeScale = 0;});
+		//}
+		// 暂停当前缓动
+		this.pause_data.tween_target_as = TweenSystem.instance.ActionManager.pauseAllRunningActions();
+		// 暂停物理系统
+		{
+			if (PhysicsSystem2D && PhysicsSystem2D.instance.enable) {
+				this.pause_data.physics_2d_b = PhysicsSystem2D.instance.enable;
+				PhysicsSystem2D.instance.enable = false;
+			}
+			if (PhysicsSystem && PhysicsSystem.instance.enable) {
+				this.pause_data.physics_3d_b = PhysicsSystem.instance.enable;
+				PhysicsSystem.instance.enable = false;
+			}
+		}
+		// 恢复排除节点
+		if (config_) {
+			let exclude_as: Node[] = [];
+			exclude_as.push(...config_.exclude_as);
+			config_.recu_exclude_as?.forEach(v1 => {
+				exclude_as.push(...this.recu_node_list(v1));
+			});
+			exclude_as.forEach(v1 => {
+				this.resume_node(v1);
+			});
+		}
+		this.pause_data.state_b = true;
+	}
+	private static recu_node_list(node_: Node, result_as: Node[] = []): Node[] {
+		if (!node_) {
+			return result_as;
+		}
+		result_as.push(node_);
+		node_.children.forEach(v1 => {
+			result_as.push(v1);
+			this.recu_node_list(v1);
+		});
+		return result_as;
+	}
+	/**恢复游戏 */
+	public static resume(): void {
+		// 恢复定时器
+		director.getScheduler().resumeTargets(this.pause_data.scheduler_as);
+		// 恢复动画
+		this.pause_data.anim_as.forEach(v1 => {
+			if (v1.isPlaying && v1.isPaused) {
+				v1.play();
+			}
+		});
+		// 恢复龙骨动画
+		//this.pause_data.dragon_bones_as.forEach(v1 => {
+		//v1.timeScale = 1;
+		//});
+		this.pause_data.spine_as.forEach(v1 => {
+			v1.timeScale = 1;
+		});
+		// 恢复缓动
+		TweenSystem.instance.ActionManager.resumeTargets(this.pause_data.tween_target_as);
+		// 恢复物理系统
+		{
+			if (this.pause_data.physics_2d_b) {
+				PhysicsSystem2D.instance.enable = this.pause_data.physics_2d_b;
+			}
+			if (this.pause_data.physics_3d_b) {
+				PhysicsSystem.instance.enable = this.pause_data.physics_3d_b;
+			}
+		}
+		this.pause_data.state_b = false;
+	}
+	/**暂停节点
+	* -物理系统需手动启用/禁用
+	*/
+	static pause_node(node_: Node): void;
+	static pause_node(node_as_: Node[]): void;
+	static pause_node(args1_: Node | Node[]): void {
+		let node_as: Node[];
+		if (Array.isArray(args1_)) {
+			node_as = args1_;
+		} else {
+			node_as = [args1_];
+		}
+		node_as.forEach(v1 => {
+			// 暂停定时器
+			director.getScheduler().pauseTarget(v1);
+			// 暂停动画
+			v1.getComponent(Animation)?.pause();
+			//暂停spine动画
+			if (v1.getComponent(sp.Skeleton)) {
+				v1.getComponent(sp.Skeleton).timeScale = 0;
+			}
+			// 暂停龙骨
+			//if (v1.getComponent(dragonBones.ArmatureDisplay)) {
+			//v1.getComponent(dragonBones.ArmatureDisplay).timeScale = 0;
+			//}
+			// 暂停缓动
+			TweenSystem.instance.ActionManager.pauseTarget(v1);
+		});
+	}
+	/**恢复节点 */
+	static resume_node(node_: Node): void;
+	static resume_node(node_as_: Node[]): void;
+	static resume_node(args1_: Node | Node[]): void {
+		let node_as: Node[];
+		if (Array.isArray(args1_)) {
+			node_as = args1_;
+		} else {
+			node_as = [args1_];
+		}
+		node_as.forEach(v1 => {
+			// 恢复定时器
+			director.getScheduler().resumeTarget(v1);
+			// 恢复动画
+			v1.getComponent(Animation)?.resume();
+			//恢复spine动画
+			if (v1.getComponent(sp.Skeleton)) {
+				v1.getComponent(sp.Skeleton).timeScale = 1;
+			}
+			//恢复龙骨
+			//if (v1.getComponent(dragonBones.ArmatureDisplay)) {
+			//v1.getComponent(dragonBones.ArmatureDisplay).timeScale = 1;
+			//}
+			// 恢复缓动
+			TweenSystem.instance.ActionManager.resumeTarget(v1);
+		});
+	}
+	//
+}

+ 9 - 0
idiom/assets/core/util/DirectorUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "0dd04c52-58cd-46d3-b730-6fd73ced38da",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 46 - 0
idiom/assets/core/util/Instance.ts

@@ -0,0 +1,46 @@
+/**单例
+ * 加方法型
+ * public static getInstance(): XXX{
+        return Instance.get(XXX);
+   }
+*/
+
+export default class Instance {
+    public static get<T>(clazz:new (...param: any[]) => T, ...param: any[]): T {
+        if (clazz["__Instance__"] == null) {
+            clazz["__Instance__"] = new clazz(...param);
+        }
+        return clazz["__Instance__"];
+    }
+}
+/**单例
+ * 继承型,静止实例化
+ */
+export class Singleton {
+    // 实例
+    private static _instance: Singleton;
+    // 是否是通过getInstance实例化
+    private static _instantiateByGetInstance: boolean = false;
+    /**
+     * 获取实例
+     */
+    public static getInstance<T extends Singleton>(this: (new () => T) | typeof Singleton): T {
+        const _class = this as typeof Singleton;
+        if (!_class._instance) {
+            _class._instantiateByGetInstance = true;
+            _class._instance = new _class();
+            _class._instantiateByGetInstance = false;
+        }
+        return _class._instance as T;
+    }
+    
+    /**
+     * 构造函数
+     * @protected
+     */
+    protected constructor() {
+        if (!(this.constructor as typeof Singleton)._instantiateByGetInstance) {
+            throw new Error("Singleton class can't be instantiated more than once.");
+        }
+    }
+}

+ 9 - 0
idiom/assets/core/util/Instance.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "f298abb1-84e6-4870-98ea-2b09f59063fe",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 158 - 0
idiom/assets/core/util/LocalStorageUtil.ts

@@ -0,0 +1,158 @@
+import { sys } from "cc"
+export class LocalStorageUtil {
+    private static _inst:LocalStorageUtil;
+    public static getInstance(): LocalStorageUtil{
+        if(!this._inst)this._inst=new LocalStorageUtil();
+        return this._inst;
+    }
+    //缓存
+    temporary: Map<string, any>;
+  
+    constructor() {
+        this.temporary = new Map<string, any>();
+    }
+    private _user_id:string|null;
+    public setUserId(uk:string):void{
+        this._user_id=uk;
+    }
+    /**
+     * 缓存变量存储区别用户
+     * @param {*} key 
+     * @param {*} value 
+     */
+    set(key: string, value: any): any {
+        if (typeof value === 'object') {
+            try {
+                value = JSON.stringify(value);
+            }
+            catch (e) {
+                console.error(`解析失败,str = ${value}`);
+                return;
+            }
+        }
+        else if (typeof value === 'number') {
+            value = value + "";
+        }
+
+        if (this._user_id)
+            sys.localStorage.setItem(this._user_id + "_" + key, value);
+        else
+            sys.localStorage.setItem(key,value);
+        return value;
+    }
+    /**
+     * 获取缓存变量区别用户
+     * @param {*} key 
+     * @returns 
+     */
+    private get(key: string): string { 
+        if (null == key) {
+            console.error("存储的key不能为空");
+            return null!;
+        }
+        let data;
+        if (this._user_id)
+            data = sys.localStorage.getItem(this._user_id + "_" + key);
+        else
+            data = sys.localStorage.getItem(key);
+        if (!data) return undefined;
+        return data;
+       
+    }
+     /** 获取指定关键字的数值 */
+     getNumber(key: string, defaultValue: number = 0): number {
+        var r = this.get(key);
+        if (r == "0") {
+            return Number(r);
+        }
+        return Number(r) || defaultValue;
+    }
+
+    /** 获取指定关键字的布尔值 */
+    getBoolean(key: string): boolean {
+        var r = this.get(key);
+        return Boolean(r) || false;
+    }
+
+    /** 获取指定关键字的JSON对象 */
+    getJson(key: string, defaultValue?: any): any {
+        var r = this.get(key);
+        return (r && JSON.parse(r)) || defaultValue;
+    }
+    /** 获取指定关键字的JSON对象 */
+    getObject(key: string): any {
+        let data= this.get(key);
+        try {
+            const jsonObj = JSON.parse(data)
+            return jsonObj;
+        } catch (error) {
+            return data;
+        }
+    }
+    /**
+     * 删除缓存变量
+     * @param {*} key 
+     */
+    public removeNormalObject(key: string): any {
+        sys.localStorage.removeItem(key);
+    }
+    /**
+     * 缓存变量存储
+     * @param {*} key 
+     * @param {*} value 
+     */
+    setNormalObject(key: string, value: any): any {
+        sys.localStorage.setItem(key, JSON.stringify(value));
+        return value;
+    }
+
+    /**
+     * 获取缓存变量
+     * @param {*} key 
+     * @returns 
+     */
+    getNormalObject(key: string): string {
+        let data;
+        data = sys.localStorage.getItem(key);
+        if (!data) return undefined;
+        try {
+            const jsonObj = JSON.parse(data)
+            return jsonObj;
+        } catch (error) {
+            return data;
+        }
+    }
+
+    /**
+     * 删除缓存变量
+     * @param {*} key 
+     */
+    removeObject(key: string) {
+        sys.localStorage.removeItem(key);
+    }
+
+    /**
+     * 临时变量存储
+     * @param {*} key 
+     * @param {*} value 
+     */
+    setTempObject(key: string, value: any) {
+        this.temporary.set(key, value);
+    }
+    /**
+     * 获取临时变量
+     * @param {*} key 
+     * @returns 
+     */
+    getTempObject(key: string): any {
+        return this.temporary.get(key);
+    }
+    /**
+     * 删除临时变量
+     * @param {*} key 
+     */
+    removeTempObject(key: string) {
+        this.temporary.delete(key);
+    }
+}
+

+ 9 - 0
idiom/assets/core/util/LocalStorageUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "d8e8827e-43d4-4fb5-8ddf-15aa29e19ee2",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 496 - 0
idiom/assets/core/util/MathUtil.ts

@@ -0,0 +1,496 @@
+import { misc, Rect, Vec2, Vec3 } from "cc";
+
+
+/** 用于计算的临时工 */
+const tempVec3 = new Vec3();
+/** 用于弧度转角度 */
+const rad2Deg = 180 / Math.PI;
+/** 用于角度转弧度 */
+const deg2Rad = Math.PI / 180;
+export class MathUtil {
+    static readonly zore: Vec2 = new Vec2(1, 0);
+    /**
+    * 用于弧度转角度
+    */
+    public static get rad2Deg() {
+        return rad2Deg;
+    }
+
+    /**
+     * 用于角度转弧度
+     */
+    public static get deg2Rad() {
+        return deg2Rad;
+    }
+    /**
+    * 弧度转角度
+    * @param radians 
+    */
+    public static radiansToDegrees(radians: number) {
+        return radians * rad2Deg;
+    }
+
+    /**
+     * 角度转弧度
+     * @param degree 
+     */
+    public static degreesToRadians(degree: number) {
+        return degree * deg2Rad;
+    }
+
+    /**限制数大小 */
+    static Clamp(value: number, min: number, max: number): number {
+        if (value < min) {
+            value = min;
+        } else if (value > max) {
+            value = max;
+        }
+        return value;
+    }
+    static Clamp01(value: number): number {
+        return this.Clamp(value, 0, 1);
+    }
+
+    static PingPong(t: number, length: number): number {
+        t = this.Repeat(t, length * 2)
+        return length - Math.abs(t - length)
+    }
+
+    static Repeat(t: number, length: number): number {
+        return t - (Math.floor(t / length) * length)
+    }
+    static Round(num: number): number {
+        return Math.floor(num + 0.5)
+    }
+    static Sign(num: number): number {
+        if (num > 0) {
+            num = 1
+        } else if (num < 0) {
+            num = -1
+        } else {
+            num = 0;
+        }
+        return num;
+    }
+    static InverseLerp(from: number, to: number, value: number): number {
+        if (from < to) {
+            if (value < from) return 0;
+            if (value > to) return 1;
+            value = value - from
+            value = value / (to - from)
+            return value;
+        }
+        if (from <= to) return 0;
+        if (value < to) return 1;
+        if (value > from) return 0;
+        return 1 - ((value - to) / (from - to));
+    }
+    static Lerp(from: number, to: number, t: number): number {
+        return from + (to - from) * this.Clamp01(t);
+    }
+    static LerpUnclamped(a: number, b: number, t: number): number {
+        return a + (b - a) * t;
+    }
+    static DeltaAngle(current: number, target: number): number {
+        let num = this.Repeat(target - current, 360);
+        if (num > 180) num = num - 360;
+        return num;
+    }
+    static LerpAngle(a: number, b: number, t: number): number {
+        let num = this.Repeat(b - a, 360)
+        if (num > 180) num = num - 360;
+        return a + num * this.Clamp01(t)
+    }
+    /**
+     * 在最小值和最大值之间进行插值,并在极限处进行平滑处理
+     * @param from 
+     * @param to 
+     * @param t 
+     */
+    public static smoothStep(from: number, to: number, t: number) {
+        t = this.Clamp01(t);
+        t = (-2.0 * t * t * t + 3.0 * t * t);
+        return (to * t + from * (1.0 - t));
+    }
+    /**
+    * 平滑控制
+    * @param current 当前值
+    * @param target 目标值
+    * @param currentVelocity 当前速度
+    * @param smoothTime 平滑时间
+    * @param maxSpeed 最大速度
+    * @param deltaTime 时间增量
+    */
+    public static smoothDamp(current: number, target: number, currentVelocity: number, smoothTime: number, deltaTime: number, maxSpeed: number = Number.POSITIVE_INFINITY) {
+        smoothTime = Math.max(0.0001, smoothTime);
+        const num1 = 2 / smoothTime;
+        const num2 = num1 * deltaTime;
+        const num3 = (1 / (1 + num2 + 0.47999998927116394 * num2 * num2 + 0.23499999940395355 * num2 * num2 * num2));
+        const num4 = current - target;
+        const num5 = target;
+        const max = maxSpeed * smoothTime;
+        const num6 = this.Clamp(num4, -max, max);
+        target = current - num6;
+        const num7 = (currentVelocity + num1 * num6) * deltaTime;
+        let velocity = (currentVelocity - num1 * num7) * num3;
+        let num8 = target + (num6 + num7) * num3;
+        if ((num5 - current > 0) === (num8 > num5)) {
+            num8 = num5;
+            velocity = (num8 - num5) / deltaTime;
+        }
+        return {
+            value: num8,
+            velocity: velocity,
+        };
+    }
+    /**
+     * 垂直于原向量V的单位向量
+     * @param v 一个向量
+     * @returns 
+     */
+    static getPerpendicular(v: Vec2): Vec2 {
+        const perpendicular = new Vec2(-v.y, v.x);  //计算垂直向量
+        const normal = perpendicular.normalize();//归一化
+        return normal;
+    }
+    //角度转向量  
+    static angleToVector(angle: number): Vec2 {
+        return this.zore.clone().rotate(-misc.degreesToRadians(angle));
+    }
+    // 向量转角度
+    static vectorToAngle(dir: Vec2): number {
+        return -misc.radiansToDegrees(dir.signAngle(this.zore));
+    }
+    // 角度转向量   
+    static angle_to_vector(angle: number): Vec2 {
+        // tan = sin / cos
+        // 将传入的角度转为弧度
+        let radian = this.degreesToRadians(angle);
+        // 算出cos,sin和tan
+        let cos = Math.cos(radian);// 邻边 / 斜边
+        let sin = Math.sin(radian);// 对边 / 斜边
+        let tan = sin / cos;// 对边 / 邻边
+        // 结合在一起并归一化
+        let vec = new Vec2(cos, sin).normalize();
+        // 返回向量
+        return (vec);
+    }
+    // !!!!!!!!其实使用Math.atan2求出弧度再转角度一样的效果
+    // 向量转角度
+    static vector_to_angle(dir: Vec2): number {
+        // 将传入的向量归一化 一般已经归一化了
+        //dir.normalize();
+        // 计算出目标角度的弧度
+        let radian = dir.signAngle(this.zore);
+        // 把弧度计算成角度
+        let angle = -this.radiansToDegrees(radian);
+        // 返回角度
+        return (angle);
+    }
+    //向量转弧度
+    static vector_to_radian(dir: Vec2): number {
+        return dir.signAngle(this.zore);
+    }
+    /// <summary>
+    /// 旋转向量,使其方向改变,大小不变
+    /// </summary>
+    /// <param name="v">需要旋转的向量</param>
+    /// <param name="angle">旋转的角度</param>
+    /// <returns>旋转后的向量</returns>
+    static RotationMatrix(x: number, y: number, angle: number): Vec2 {
+        let radian = this.degreesToRadians(angle);
+        let sin = Math.sin(radian);
+        var cos = Math.cos(radian);
+        var newX = x * cos + y * sin;
+        var newY = x * -sin + y * cos;
+        //var newX = x * cos - y * sin;
+        //var newY = x * sin + y * cos;
+        return new Vec2(newX, newY);
+    }
+    static RotationDir(dir: Vec2, angle: number): Vec2 {
+        let radian = this.radiansToDegrees(angle);
+        let sin = Math.sin(radian);
+        var cos = Math.cos(radian);
+        var newX = dir.x * cos + dir.y * sin;
+        var newY = dir.x * -sin + dir.y * cos;
+        dir.x = newX;
+        dir.y = newY;
+        return dir;
+    }
+    //扇形范围
+    public static CheckInView(mPos: Vec2, faceDir: Vec2, tPos: Vec2, dis: number, angle: number): boolean {
+        //let t = tPos.subtract(mPos);
+        //let distance = Vec2.lengthSqr(t);
+        //if(distance<dis*dis)
+        let distance = Vec2.distance(mPos, tPos);
+        if (distance < dis) {
+            if (angle >= 360) return true;
+            let dir = tPos.subtract(mPos).normalize();
+            let ang = this.radiansToDegrees(Vec2.angle(faceDir, dir));
+            if (ang <= angle * 0.5) {
+                return true;
+            }
+        }
+        return false;
+    }
+    //检测点是否在一个凸四边形内
+    public static InConvexQuad(point: Vec2, pointA: Vec2, pointB: Vec2, pointC: Vec2, pointD: Vec2): boolean {
+        let vec1: Vec2, vec2: Vec2;
+        vec1.x = pointB.x - pointA.x;
+        vec1.y = pointB.y - pointA.y;
+        vec2.x = point.x - pointA.x;
+        vec2.y = point.y - pointA.y;
+        if (this.cross2DPoint(vec2.x, vec2.y, vec1.x, vec1.y)) return false;
+        vec1.x = pointC.x - pointB.x;
+        vec1.y = pointC.y - pointB.y;
+        vec2.x = point.x - pointB.x;
+        vec2.y = point.y - pointB.y;
+        if (this.cross2DPoint(vec2.x, vec2.y, vec1.x, vec1.y)) return false;
+        vec1.x = pointD.x - pointC.x;
+        vec1.y = pointD.y - pointC.y;
+        vec2.x = point.x - pointC.x;
+        vec2.y = point.y - pointC.y;
+        if (this.cross2DPoint(vec2.x, vec2.y, vec1.x, vec1.y)) return false;
+        vec1.x = pointA.x - pointD.x;
+        vec1.y = pointA.y - pointD.y;
+        vec2.x = point.x - pointD.x;
+        vec2.y = point.y - pointD.y;
+        if (this.cross2DPoint(vec2.x, vec2.y, vec1.x, vec1.y)) return false;
+        return true;
+    }
+    /**等同于Vec2.cross */
+    public static cross2DPoint(x1: number, y1: number, x2: number, y2: number): number {
+        return x1 * y2 - x2 * y1;
+    }
+    /**检测两个2D包围盒(xy为中心点 wh分别为x轴半径宽 y轴半径宽) 是否相交*/
+    public static AABBAABB2D(pointX1: number, pointY1: number, w1: number, h1: number,
+        pointX2: number, pointY2: number, w2: number, h2: number): boolean {
+        if (Math.abs(pointX1 - pointX2) > (w1 + w2)) return false;
+        if (Math.abs(pointY1 - pointY2) > (h1 + h2)) return false;
+        return true;
+    }
+    /**检测两个3D包围盒 是否相交*/
+    public static AABBAABB3D(pointX1: number, pointY1: number, pointZ1: number, w1: number, h1: number, t1: number,
+        pointX2: number, pointY2: number, pointZ2: number, w2: number, h2: number, t2: number): boolean {
+        if (Math.abs(pointX1 - pointX2) > (w1 + w2)) return false;
+        if (Math.abs(pointY1 - pointY2) > (h1 + h2)) return false;
+        if (Math.abs(pointZ1 - pointZ2) > (t1 + t2)) return false;
+        return true;
+    }
+    /**点与点*/
+    public static pointPoint(point1: Vec2, point2: Vec2): boolean {
+        if (point1.x == point2.x && point1.y == point2.y) {
+            return true;
+        }
+        return false;
+    }
+    //判断点是否在线上,在返回1,不在返回0
+    public static onSegement(Q: Vec2, p1: Vec2, p2: Vec2): boolean {
+        let maxx: number, minx: number, maxy: number, miny: number;
+        maxx = p1.x > p2.x ? p1.x : p2.x;    //矩形的右边长
+        minx = p1.x > p2.x ? p2.x : p1.x;     //矩形的左边长
+        maxy = p1.y > p2.y ? p1.y : p2.y;    //矩形的上边长
+        miny = p1.y > p2.y ? p2.y : p1.y;     //矩形的下边长
+        if (((Q.x - p1.x) * (p2.y - p1.y) == (p2.x - p1.x) * (Q.y - p1.y)) && (Q.x >= minx && Q.x <= maxx) && (Q.y >= miny && Q.y <= maxy)) {
+            return true;
+        }
+        return false;
+    }
+    //点与圆的碰撞
+    //通过计算点到圆心的距离与半径比较判断circle(由中心点和半径构成)
+    public static pointInCircle(point: Vec2, center: Vec2, radius: number): boolean {
+        return Vec2.squaredDistance(point, center) <= radius * radius;
+    }
+    //点与矩形的碰撞
+    //通过判断点坐标是否在矩形四个顶点围成的坐标区域内
+    public static pointInRect(point: Vec2, rect: Rect): boolean {
+        if (point.x >= rect.x
+            && point.x <= rect.x + rect.width
+            && point.y >= rect.y
+            && point.y <= rect.y + rect.height) return true;
+        return false
+    }
+    //线与矩形的碰撞
+    public static lineInRect(p1: Vec2, p2: Vec2, rect: Rect): boolean {
+        let height = p1.y - p2.y;
+        let width = p2.x - p1.x;
+        let c = p1.x * p2.y - p2.x - p1.y;//计算叉乘
+        if ((height * rect.xMin + width * rect.yMin + c >= 0 && height * rect.xMax + width * rect.yMax + c <= 0)
+            || (height * rect.xMin + width * rect.yMin + c <= 0 && height * rect.xMax + width * rect.yMax + c >= 0)
+            || (height * rect.xMin + width * rect.yMax + c >= 0 && height * rect.xMax + width * rect.yMin + c <= 0)
+            || (height * rect.xMin + width * rect.yMax + c <= 0 && height * rect.xMax + width * rect.yMin + c >= 0)
+        ) {
+            if ((p1.x < rect.xMin && p2.x < rect.xMin)
+                || (p1.x > rect.xMax && p2.x > rect.xMax)
+                || (p1.y > rect.yMin && p2.y > rect.yMin)
+                || (p1.y < rect.yMax && p2.y < rect.yMax)
+            ) {
+                return false;
+            } else {
+                return true;
+            }
+        } else {
+            return false
+        }
+    }
+    //两个rect是否相交
+    public static collisionRectWithRect(rect1: Rect, rect2: Rect): boolean {
+        //计算相交部分的矩形
+        //左下角坐标:( lx , ly )    //右上角坐标:( rx , ry )
+        let lx = Math.max(rect1.xMin, rect2.xMin);
+        let ly = Math.max(rect1.yMin, rect2.yMin);
+        let rx = Math.min(rect1.xMax, rect2.xMax);
+        let ry = Math.min(rect1.yMax, rect2.yMax);
+        //判断是否能构成小矩形
+        if (lx > rx || ly > ry) return false; //矩形不相交
+        return true;  //发生碰撞
+    }
+    //圆与矩形是否相交
+    public static collisionRectWithCircle(rect: Rect, p: Vec2, r: number): boolean {
+        //获取矩形信息
+        //左下角坐标:( lx , ly )
+        //右上角坐标:( rx , ry )
+        let lx = rect.xMin;
+        let ly = rect.yMin;
+        let rx = rect.xMax;
+        let ry = rect.yMax;
+        //计算圆心到四个顶点的距离
+        let d1 = Vec2.distance(p, new Vec2(lx, ly));
+        let d2 = Vec2.distance(p, new Vec2(lx, ry));
+        let d3 = Vec2.distance(p, new Vec2(rx, ly));
+        let d4 = Vec2.distance(p, new Vec2(rx, ry));
+        //判断是否碰撞//判断距离是否小于半径
+        if (d1 < r || d2 < r || d3 < r || d4 < r) return true;
+        //是否在圆角矩形的,横向矩形内
+        if (p.x > (lx - r) && p.x < (rx + r) && p.y > ly && p.y < ry) return true;
+        //是否在圆角矩形的,纵向矩形内
+        if (p.x > lx && p.x < rx && p.y > (ly - r) && p.y < (ry + r)) return true;
+        //不发生碰撞
+        return false;
+    }
+    /**
+       * 计算向量在指定平面上的投影
+       * @param vector 被投影的向量
+       * @param planeNormal 平面法线
+       */
+    public static projectOnPlane(vector: Vec3, planeNormal: Vec3) {
+        // 也可以直接用 Vec3 自带的平面投影函数
+        // return Vec3.projectOnPlane(new Vec3, targetDir, planeNormal);
+
+        // 使用点乘计算方向矢量在平面法线上的投影长度
+        const projectionLength = Vec3.dot(vector, planeNormal);
+        // 平面法线与长度相乘得到方向矢量在平面法线上的投影矢量
+        const vectorOnPlane = tempVec3.set(planeNormal).multiplyScalar(projectionLength);
+        // 方向矢量减去其在平面法线上的投影矢量即是其在平面上的投影矢量
+        return Vec3.subtract(new Vec3, vector, vectorOnPlane);
+    }
+
+    /**
+     * 计算两个向量基于指定轴的夹角(逆时针方向为正方向,值范围 -180 ~ 180)
+     * @param a 向量 a
+     * @param b 向量 b
+     * @param axis 参照轴向量(请确保是归一化的)
+     */
+    public static signedAngle(a: Vec3, b: Vec3, axis: Vec3) {
+        // 将向量 a 和 b 分别投影到以 axis 为法线的平面上
+        const aOnAxisPlane = this.projectOnPlane(a, axis);
+        const bOnAxisPlane = this.projectOnPlane(b, axis);
+        // 归一化处理
+        const aNormalized = aOnAxisPlane.normalize();
+        const bNormalized = bOnAxisPlane.normalize();
+        // 求出同时垂直于 a 和 b 的法向量
+        const abNormal = Vec3.cross(new Vec3, aNormalized, bNormalized).normalize();
+        // 将法向量到 axis 上的投影长度
+        // 若投影长度为正值(+1)则表示法向量与 axis 同向(向量叉乘的右手法则)
+        const sign = Vec3.dot(abNormal, axis);
+        // 求出向量 a 和 b 的夹角
+        const radian = Math.acos(Vec3.dot(aNormalized, bNormalized));
+        // 混合在一起!
+        return radian * sign * this.rad2Deg;
+    }
+
+    //获取某点到某点的Y轴方向
+    static GetYDir(from: Vec3, to: Vec3) {
+        let dir = to.subtract(from);
+        dir.y = from.y
+        return dir.normalize();
+    }
+    //向量绕Y轴转一个角度
+    static RotateAroundAxisY(dir: Vec3, angle: number): Vec3 {
+        dir = dir.clone();
+        let rad = this.degreesToRadians(angle);
+        let cos = Math.cos(rad);
+        let sin = Math.sin(rad);
+        dir.x = dir.x * cos + dir.z * sin;
+        dir.z = dir.x * (-sin) + dir.z * cos;
+        return dir;
+    }
+    //将一个向量围绕X轴旋转angle个角度
+    static RotateAroundAxisX(dir: Vec3, angle: number): Vec3 {
+        dir = dir.clone();
+        let rad = this.degreesToRadians(angle);
+        let cos = Math.cos(rad);
+        let sin = Math.sin(rad);
+        dir.y = dir.y * cos - dir.z * sin;
+        dir.z = dir.y * sin + dir.z * cos;
+        return dir;
+    }
+    //将一个向量围绕Z轴旋转angle个角度
+    static RotateAroundAxisZ(dir: Vec3, angle: number): Vec3 {
+        dir = dir.clone();
+        let rad = this.degreesToRadians(angle);
+        let cos = Math.cos(rad);
+        let sin = Math.sin(rad);
+        dir.x = dir.x * cos - dir.y * sin;
+        dir.y = dir.x * sin + dir.y * cos;
+        return dir;
+    }
+    //线段是否与矩形相交
+    static LineRectIntersection(lineStartPoint: Vec2, lineEndPoint: Vec2, rectMinX: number, rectMaxX: number, rectMinY: number, rectMaxY: number): boolean {//针对四种不同的情况,进行碰撞检测
+        let minXLinePoint = lineEndPoint;
+        if (lineStartPoint.x <= lineEndPoint.x) {
+            minXLinePoint = lineStartPoint;
+        }
+        let maxXLinePoint = lineStartPoint;
+        if (lineStartPoint.x <= lineEndPoint.x) {
+            maxXLinePoint = lineEndPoint;
+        }
+        let minYLinePoint = lineEndPoint;
+        if (lineStartPoint.y <= lineEndPoint.y) {
+            minYLinePoint = lineStartPoint;
+        }
+        let maxYLinePoint = lineStartPoint;
+        if (lineStartPoint.y <= lineEndPoint.y) {
+            maxYLinePoint = lineEndPoint;
+        }
+        if (minXLinePoint.x <= rectMinX && rectMinX <= maxXLinePoint.x) {
+            let m = (maxXLinePoint.y - minXLinePoint.y) / (maxXLinePoint.x - minXLinePoint.x);
+            let intersectionY = ((rectMinX - minXLinePoint.x) * m) + minXLinePoint.y;
+            if (minYLinePoint.y <= intersectionY && intersectionY <= maxYLinePoint.y && rectMinY <= intersectionY && intersectionY <= rectMaxY) {
+                return true;//new Vec2(rectMinX, intersectionY)
+            }
+        }
+        if (minXLinePoint.x <= rectMaxX && rectMaxX <= maxXLinePoint.x) {
+            let m = (maxXLinePoint.y - minXLinePoint.y) / (maxXLinePoint.x - minXLinePoint.x);
+            let intersectionY = ((rectMaxX - minXLinePoint.x) * m) + minXLinePoint.y;
+            if (minYLinePoint.y <= intersectionY && intersectionY <= maxYLinePoint.y && rectMinY <= intersectionY && intersectionY <= rectMaxY) {
+                return true;//new Vec2(rectMaxX, intersectionY)
+            }
+        }
+        if (minYLinePoint.y <= rectMaxY && rectMaxY <= maxYLinePoint.y) {
+            let rm = (maxYLinePoint.x - minYLinePoint.x) / (maxYLinePoint.y - minYLinePoint.y);
+            let intersectionX = ((rectMaxY - minYLinePoint.y) * rm) + minYLinePoint.x;
+            if (minXLinePoint.x <= intersectionX && intersectionX <= maxXLinePoint.x && rectMinX <= intersectionX && intersectionX <= rectMaxX) {
+                return true;//new Vec2(intersectionX, rectMaxY)
+            }
+        }
+        if (minYLinePoint.y <= rectMinY && rectMinY <= maxYLinePoint.y) {
+            let rm = (maxYLinePoint.x - minYLinePoint.x) / (maxYLinePoint.y - minYLinePoint.y);
+            let intersectionX = ((rectMinY - minYLinePoint.y) * rm) + minYLinePoint.x;
+            if (minXLinePoint.x <= intersectionX && intersectionX <= maxXLinePoint.x && rectMinX <= intersectionX && intersectionX <= rectMaxX) {
+                return true;//new Vec2(intersectionX, rectMinY)
+            }
+        }
+        return false
+    }
+}

+ 9 - 0
idiom/assets/core/util/MathUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "5df23a8d-909e-4972-8e1b-ef66d92c925f",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 40 - 0
idiom/assets/core/util/PathUtil.ts

@@ -0,0 +1,40 @@
+
+export default class PathUtil {
+    /**
+     * 返回 Path 的扩展名
+     * @param path 路径
+     * @returns {string}
+     */
+    public static extname(path: string): string {
+        var temp = /(\.[^\.\/\?\\]*)(\?.*)?$/.exec(path);
+        return temp ? temp[1] : '';
+    }
+
+    /**
+     * 获取文件路径的文件名。
+     * @param path 路径
+     * @param extname 扩展名
+     * @returns {string}
+     */
+    public static basename(path: string, extname?: string): string {
+        let index = path.indexOf("?");
+        if (index > 0) path = path.substring(0, index);
+        let reg = /(\/|\\)([^\/\\]+)$/g;
+        let result = reg.exec(path.replace(/(\/|\\)$/, ""));
+        if (!result) return path;
+        let baseName = result[2];
+        if (extname && path.substring(path.length - extname.length).toLowerCase() === extname.toLowerCase())
+            return baseName.substring(0, baseName.length - extname.length);
+        return baseName;
+    }
+
+    /**
+     * 获取文件路径的目录名
+     * @param path 路径
+     * @returns {string} 
+     */
+    public static dirname(path: string): string {
+        var temp = /((.*)(\/|\\|\\\\))?(.*?\..*$)?/.exec(path);
+        return temp ? temp[2] : '';
+    }
+}

+ 9 - 0
idiom/assets/core/util/PathUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "59900f42-6224-4f49-b3fe-e8c2641562d8",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 189 - 0
idiom/assets/core/util/ProjectileMathUtil.ts

@@ -0,0 +1,189 @@
+/** 用于弧度转角度 */
+const rad2Deg = 180 / Math.PI;
+/** 用于角度转弧度 */
+const deg2Rad = Math.PI / 180;
+/**
+ * 抛射运动的数学工具
+ */
+export default class ProjectileMathUtil {
+    /**
+       * 计算耗时
+       * @param x 水平位移
+       * @param angle 初始角度
+       * @param velocity 初始速度
+       */
+    public static calculateTotalTime(x: number, angle: number, velocity: number) {
+        // 初始角度(弧度制)
+        const θ = angle * deg2Rad;
+        // 时间
+        // t = x / ( v * cos(θ) )
+        const t = x / (velocity * Math.cos(θ));
+
+        return t;
+    }
+    /**
+    * 计算指定时刻的运动角度
+    * @param angle 初始角度
+    * @param velocity 初始速度
+    * @param time 时间
+    * @param gravity 重力加速度
+    * @param returnInRadians 是否返回弧度制结果
+    */
+    public static calculateAngleAtMoment(angle: number, velocity: number, time: number, gravity: number, returnInRadians: boolean = false) {
+        // 重力加速度(垂直向下)
+        const g = gravity;//
+        // 初始角度(弧度制)
+        const θ = angle * deg2Rad;
+
+        // 水平瞬时速度
+        // vx = v * cos(θ)
+        const vx = velocity * Math.cos(θ);
+
+        // 垂直瞬时速度
+        // vy = v * sin(θ) - g * t
+        const vy = velocity * Math.sin(θ) - g * time;
+
+        // 该时刻的运动角度(弧度制)
+        const θt = Math.atan(vy / vx);
+
+        return (returnInRadians ? θt : θt * rad2Deg);
+    }
+
+    /**
+     * 计算指定时刻的位移距离
+     * @param angle 初始角度
+     * @param velocity 初始速度
+      * @param gravity 重力加速度
+     * @param time 时间点
+     */
+    public static calculateDisplacementAtMoment(angle: number, velocity: number, gravity: number, time: number) {
+        // 重力加速度(垂直向下)
+        const g = gravity;
+        // 初始角度(弧度制)
+        const θ = angle * deg2Rad;
+
+        // 水平位移
+        // x = v * cos(θ) * t
+        const x = velocity * Math.cos(θ) * time;
+
+        // 垂直位移
+        // y = v * sin(θ) * t - 0.5 * g * t^2
+        const y = velocity * Math.sin(θ) * time - 0.5 * g * Math.pow(time, 2);
+
+        return { x, y };
+    }
+
+    /**
+     * 根据初始角度计算初始速度
+     * @param x 水平距离
+     * @param y 垂直距离
+ * @param gravity 重力加速度
+     * @param angle 初始角度(角度制)
+     */
+    public static calculateWithAngle(x: number, y: number, gravity: number, angle: number) {
+        // 重力加速度(垂直向下)
+        const g = Math.abs(gravity);
+        // 初始角度(弧度制)
+        const θ = angle * deg2Rad;
+
+        // 速度公式
+        // v = sqrt( ( x^2 * g ) / ( 2 * x * sin(θ) * cos(θ) - 2 * y * cos(θ)^2 ) )
+
+        // 部分计算结果
+        const p1 = (2 * x * Math.sin(θ) * Math.cos(θ)) - (2 * y * Math.pow(Math.cos(θ), 2));
+        // 负数没有平方根
+        if (p1 < 0) {
+            return NaN;
+        }
+        // 速度
+        const v = Math.sqrt((g * Math.pow(x, 2)) / p1);
+
+        return v;
+    }
+
+    /**
+     * 根据初始速度计算初始角度
+     * @param x 水平距离
+     * @param y 垂直距离
+ * @param gravity 重力加速度
+     * @param velocity 初始速度
+     */
+    public static calculateWithVelocity(x: number, y: number, velocity: number, gravity: number) {
+        // 重力加速度(垂直向下)
+        const g = gravity;
+        // 初始速度
+        const v = velocity;
+
+        // 角度公式
+        // θ = atan( ( -v^2 ± sqrt( v^4 - g * ( g * x^2 + 2 * y * v^2 ) ) / ( -g * x ) ) )
+
+        // 部分计算结果
+        const p1 = Math.pow(v, 2);
+        const p2 = Math.pow(v, 4) - g * (g * Math.pow(x, 2) + 2 * y * p1);
+        // 负数没有平方根
+        if (p2 < 0) {
+            return {
+                angle1: NaN,
+                angle2: NaN,
+            };
+        }
+        // 部分计算结果
+        const p3 = Math.sqrt(p2);
+        // 角度(两个解)
+        const θ1 = Math.atan((-p1 + p3) / (-g * x));
+        const θ2 = Math.atan((-p1 - p3) / (-g * x));
+
+        return {
+            angle1: θ1 * rad2Deg,
+            angle2: θ2 * rad2Deg,
+        };
+    }
+
+    /**
+     * 根据最大高度计算速度和角度
+     * @param x 水平距离
+     * @param y 垂直距离
+     * @param gravity 重力加速度
+     * @param maxHeight 最大高度
+     */
+    public static calculateWithMaxHeight(x: number, y: number, maxHeight: number, gravity: number) {
+        // 重力加速度(垂直向下)
+        const g = gravity;
+        // 最大高度
+        const h = maxHeight;
+
+        // 最大高度不能小于 0,也不能够小于垂直距离
+        if (h < 0 || (h - y) < 0) {
+            return {
+                angle: NaN,
+                velocity: NaN,
+                time: NaN,
+            };
+        }
+
+        // 部分计算结果
+        const p1 = Math.sqrt(2 * g * h);
+        const p2 = Math.sqrt(2 * g * (h - y));
+
+        // 时间公式
+        // t = ( -sqrt( 2 * g * h ) ± sqrt( 2 * g * ( h - y ) ) ) / -g
+        const t1 = (-p1 + p2) / -g;
+        const t2 = (-p1 - p2) / -g;
+        // 始终使用较大的解
+        const t = Math.max(t1, t2);
+
+        // 角度公式
+        // θ = atan( ( sqrt( 2 * g * h ) * t ) / x )
+        const θ = Math.atan(p1 * t / x);
+
+        // 速度公式
+        // v = sqrt( 2 * g * h ) / sin(θ)
+        const v = p1 / Math.sin(θ);
+
+        return {
+            angle: θ * rad2Deg,
+            velocity: v,
+            time: t,
+        };
+    }
+}

+ 1 - 0
idiom/assets/core/util/ProjectileMathUtil.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"16ac3b52-38ce-45c2-965e-f0214a854d86","files":[],"subMetas":{},"userData":{}}

+ 190 - 0
idiom/assets/core/util/ResUtil.ts

@@ -0,0 +1,190 @@
+/**
+ *
+ * @file ResUtil.ts
+ * @author
+ * @description Cocos方法整合,如果Cocos版本升级,造成API修改,仅需修改此处
+ */
+import { Asset, assetManager, AssetManager, director, ImageAsset, resources, SceneAsset, Texture2D } from "cc";
+//export type Constructor<T extends Asset> = new () => T;
+export const headImgExt = ".head";
+export module ResUtil {
+    /**
+    * @description 原生加载资源
+    * @param object {url: 远程地址 option: 参数类型}
+    * @returns 
+    */
+    export function loadRemote<T extends Asset>(url:string, option?: any):Promise<T>{
+        return new Promise((resolve, reject) => {
+            assetManager.loadRemote<T>(url, option, (err: Error | null, asset: T) => {
+                resolve && resolve(err ? null : asset);
+            });
+        });
+    }
+    /**
+     * 加载bundle
+     * @param bundleName 
+     * @returns 
+     */
+    export function loadBundle(bundleName: string):Promise<AssetManager.Bundle> {
+        return new Promise((resolve, reject) => {
+            assetManager.loadBundle(bundleName, (err, bundle) => {
+                resolve && resolve(err ? null : bundle);
+            });
+        });
+    }
+    /**获取已加载的bundle*/
+    export function getBundle(bundleName?: string):AssetManager.Bundle|null{
+        if (null == bundleName || '' === bundleName) {
+            return resources;
+        } else {
+            return assetManager.getBundle(bundleName)!;
+        }
+    }
+    /**
+     * 加载资源
+     * @param  path  资源名
+     * @param  bundle 所属包名或 包
+     * @type 资源类型
+     * @returns 
+    */
+    export async function loadAsset<T extends Asset>(path: string, bundle?: string | AssetManager.Bundle):Promise<T>{
+        let bd:AssetManager.Bundle;
+        if(!bundle) bundle='';
+        if(typeof bundle === 'string'){
+           bd = getBundle(bundle);
+           if(!bd) bd = await loadBundle(bundle);
+        } else if (bundle instanceof AssetManager.Bundle) {  
+           bd = bundle;
+        }
+        const asset = bd.get<T>(path);
+        if (null != asset) {
+            return Promise.resolve(asset);
+        }
+        return new Promise((resolve, reject) => {
+            bd.load<T>(path,(err, asset: T) => {
+                resolve(err ? null : asset);
+            });
+        });
+    }
+    export async function loadDir<T extends Asset>(path: string, bundle?: string | AssetManager.Bundle,progressCallback?: (completed: number, total: number) => void):Promise<T[]>{
+        let bd:AssetManager.Bundle;
+        if(!bundle) bundle='';
+        if(typeof bundle === 'string'){
+           bd = getBundle(bundle);
+           if(!bd) bd = await loadBundle(bundle);
+        } else if (bundle instanceof AssetManager.Bundle) {  
+           bd = bundle;
+        }
+        return new Promise((resolve, reject) => {
+            bd.loadDir<T>(path,(finished:number,total:number,item)=>{
+                progressCallback?.(finished,total);
+            }, (err, assets:T[]) => {    
+                resolve(err ? null : assets);
+            });
+        });
+    }
+    export async function loadAssets<T extends Asset>(list: string[], bundle?: string | AssetManager.Bundle,progressCallback?: (completed: number, total: number) => void):Promise<T[]>{
+        let bd:AssetManager.Bundle;
+        if(!bundle) bundle='';
+        if(typeof bundle === 'string'){
+           bd = getBundle(bundle);
+           if(!bd) bd = await loadBundle(bundle);
+        } else if (bundle instanceof AssetManager.Bundle) {  
+           bd = bundle;
+        }
+        return new Promise((resolve, reject) => {
+            bd.load<T>(list,(finished: number, total: number, item:any) => {
+                progressCallback?.(finished,total);
+            }, (err: Error | null, data) => {
+                resolve(err ? null : data);
+            });
+        });
+    }
+
+    export async function loadScene(scene_name:string, bundle?: string | AssetManager.Bundle,show:Boolean=false):Promise<SceneAsset>{
+        let bd:AssetManager.Bundle;
+        if(!bundle) bundle='';
+        if(typeof bundle === 'string'){
+           bd = getBundle(bundle);
+           if(!bd) bd = await loadBundle(bundle);
+        } else if (bundle instanceof AssetManager.Bundle) {  
+           bd = bundle;
+        }
+        return new Promise((resolve, reject) => {
+            bd.loadScene(scene_name, (err, asset:SceneAsset) => {    
+                resolve(err ? null : asset);
+                if(show)director.loadScene(scene_name);
+            });
+        });
+    }
+    export function releaseAll():void{
+        assetManager.releaseAll();
+    }
+    export function releaseAsset(asset:Asset){
+        assetManager.releaseAsset(asset);
+    }
+    export function release(path:string|null,bundle?: string | AssetManager.Bundle){
+        let bd:AssetManager.Bundle;
+        if(!bundle) bundle='';
+        if(typeof bundle === 'string'){
+           bd = getBundle(bundle);
+        } else if (bundle instanceof AssetManager.Bundle) {  
+           bd = bundle;
+        }
+        if(path && path!=''){
+            bd?.release(path); 
+        }else{
+            bd?.releaseAll();
+        }
+    }
+    /**
+    * 自定义头像加载流程
+    * 加载头像使用 ResUtil.loadRemote({url, option:{ext:headImgExt}})
+    */
+    export function registerHeadImgLoader() {
+        assetManager.downloader.register(headImgExt, (content, options, onComplete) => {
+            onComplete(null, content);
+        });
+        assetManager.parser.register(headImgExt, downloadDomImage);
+        assetManager.factory.register(headImgExt, createTexture);
+    }
+    function createTexture(id: string, data: any, options: any, onComplete: Function) {
+        let out: Texture2D | null = null;
+        let err: Error | null = null;
+        try {
+            out = new Texture2D();
+            const imageAsset = new ImageAsset(data);
+            out.image = imageAsset;
+        } catch (e) {
+            err = e as any as Error;
+        }
+        onComplete && onComplete(err, out);
+    }
+
+    function downloadDomImage(url: string, options: any, onComplete: Function) {
+        const img = new Image();
+        if (window.location.protocol !== 'file:') {
+            img.crossOrigin = 'anonymous';
+        }
+        function loadCallback() {
+            img.removeEventListener('load', loadCallback);
+            img.removeEventListener('error', errorCallback);
+            if (onComplete) {
+                onComplete(null, img);
+            }
+        }
+
+        function errorCallback() {
+            img.removeEventListener('load', loadCallback);
+            img.removeEventListener('error', errorCallback);
+            if (onComplete) {
+                onComplete(new Error(url));
+            }
+        }
+
+        img.addEventListener('load', loadCallback);
+        img.addEventListener('error', errorCallback);
+        img.src = url;
+        return img;
+    }
+}

+ 9 - 0
idiom/assets/core/util/ResUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "fdca46ee-3efc-42b6-a776-d0c4afa42ed3",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 99 - 0
idiom/assets/core/util/StringUtil.ts

@@ -0,0 +1,99 @@
+export class StringUtil {
+    
+    /**小数转成保留几位小数的字符串*/
+    public static ToFixed(s:number,count:number):string{
+        return s.toFixed(count).toString();
+    }
+    
+    /**秒转成 00:00时间格式*/
+    public static ToTimeString(s:number):string{
+         s = Math.floor(s);
+         let m = Math.floor(s/60);
+         s=s-m*60;  
+         let mm=m.toString();
+         if(mm.length<=1){
+            mm = "0".concat(mm);
+         }
+         if(s<10){
+            return  `${mm}:${"0".concat(s.toString())}`;
+         }
+         return  `${mm}:${s}`;
+    }
+    /**秒*/
+    public static ToSce(s:number,u:string="s"):string{
+        s = Math.floor(s);
+        return  `${s}${u}`;
+   }
+    /**货币值转字符 */
+    public static ToMoneyString(n:number):string{
+        if(n>1000000){
+            let m=Math.floor(n*0.000001);
+            n-=m*1000000;
+            if(n>1000){
+               return `${m}M${Math.floor(n*0.001)}K`;
+            }
+            return `${m}M`;
+        }else if(n>1000){
+            let m=Math.floor(n*0.001);
+            n-=m*1000;
+            if(n>100){
+               n=Math.floor(n*0.01);
+               return `${m}K${n}`; 
+            }
+            return `${m}K`; 
+        }
+        return n.toString();
+    }
+    /**毫秒转秒显示*/
+    public static MS2S(ms:number):string{
+        return (ms*0.001).toFixed(1);
+    }
+    /**
+     * 去掉前后空格
+     * @param str
+     * @returns {string}
+     */
+    public trimSpace(str: string): string {
+        return str.replace(/^\s*(.*?)[\s\n]*$/g, '$1');
+    }
+
+    /**
+     * 获取字符串长度,中文为2
+     * @param str
+     */
+    public getStringLength(str: string): number {
+        var strArr = str.split("");
+        var length = 0;
+        for (var i = 0; i < strArr.length; i++) {
+            var s = strArr[i];
+            if (this.isChinese(s)) {
+                length += 2;
+            } else {
+                length += 1;
+            }
+        }
+        return length;
+    }
+
+    /**
+     * 判断一个字符串是否包含中文
+     * @param str
+     * @returns {boolean}
+     */
+    public isChinese(str: string): boolean {
+        var reg = /^.*[\u4E00-\u9FA5]+.*$/;
+        return reg.test(str);
+    }
+
+    /**
+     * 格式化字符串 "{0},{1}.format("text0","text1")
+     */
+    public format(val: string, ...param: any[]): string {
+        for (let i = 0, len = param.length; i < len; i++) {
+            let reg = new RegExp("({)" + i + "(})", "g");
+            val = val.replace(reg, param[i]);
+        }
+        return val;
+    }
+}
+

+ 9 - 0
idiom/assets/core/util/StringUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "77722058-8f23-4726-9278-0ee300084ad9",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 23 - 0
idiom/assets/core/util/TableLoadUtil.ts

@@ -0,0 +1,23 @@
+import { _decorator, assetManager,JsonAsset} from 'cc';
+export default class TableLoadUtil {
+    public static  preloadAll(bundleName:string,path:string,callback: Function,util_cb:(string,any)=>void): void {
+        assetManager.loadBundle(bundleName, (err, bundle) => {
+                if (err) {
+                    console.log(err);
+                    return;
+                }
+                bundle.loadDir(path,JsonAsset, function (err, assets:JsonAsset[]) {
+                    if (err) {
+                        console.log(err);
+                        return;
+                    }
+                    for (let i = 0; i < assets.length; i++) {
+                        util_cb?.(assets[i].name,assets[i].json);
+                    }
+                    callback(assets);
+                }.bind(this));
+        });
+    }
+
+}
+

+ 9 - 0
idiom/assets/core/util/TableLoadUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "0d2ab41a-5985-4b3e-9b57-88553af5262f",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 53 - 0
idiom/assets/core/util/UrlUtil.ts

@@ -0,0 +1,53 @@
+export default class UrlUtil {
+    /**
+     * 获取URL参数(字符串)
+     * @param url 地址
+     * @returns {string}
+     */
+    private static getParamString(url?: string): string {
+        url = url || window.location?.href;
+        if (url != void 0) {
+            let index = url.indexOf('?');
+            if (index != -1) {
+                return url.substring(index + 1);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获取URL参数
+     * @param url 地址
+     * @returns {JSON}
+     */
+    public static getParam(url?: string): { [key: string]: string } {
+        let param = {};
+        let paramString = this.getParamString(url);
+        if (paramString) {
+            paramString.split("&").forEach((value: string) => {
+                let values = value.split("=");
+                if (values.length == 2) {
+                    param[values[0]] = values[1];
+                }
+            });
+        }
+        return param;
+    }
+
+    /**
+     * 根据key获取URL参数
+     * @param key key
+     * @param url 地址
+     * @returns {string}
+     */
+    public static getParamValue(key: string, url?: string): string {
+        let paramString = this.getParamString(url);
+        if (paramString) {
+            let values = paramString.match(`(^|&)${key}=([^&]*)(&|$)`);
+            if (values) {
+                return values[2];
+            }
+        }
+        return null;
+    }
+}

+ 9 - 0
idiom/assets/core/util/UrlUtil.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "27d0798c-6b21-4105-9ac1-9dcc54c92213",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff