소스 검색

no message

汤金涛 2 주 전
부모
커밋
4d1d6e5c11
100개의 변경된 파일8576개의 추가작업 그리고 0개의 파일을 삭제
  1. 2 0
      Flowers/.creator/asset-template/typescript/Custom Script Template Help Documentation.url
  2. 5 0
      Flowers/.creator/default-meta.json
  3. 9 0
      Flowers/assets/ch.meta
  4. 9 0
      Flowers/assets/ch/audio.meta
  5. 365 0
      Flowers/assets/ch/audio/audio.ts
  6. 9 0
      Flowers/assets/ch/audio/audio.ts.meta
  7. 9 0
      Flowers/assets/ch/ch-sdk.meta
  8. 0 0
      Flowers/assets/ch/ch-sdk/ch-sdk.umd.js
  9. 17 0
      Flowers/assets/ch/ch-sdk/ch-sdk.umd.js.meta
  10. 341 0
      Flowers/assets/ch/ch-sdk/chsdk.d.ts
  11. 9 0
      Flowers/assets/ch/ch-sdk/chsdk.d.ts.meta
  12. 45 0
      Flowers/assets/ch/ch-sdk/提示说明.txt
  13. 11 0
      Flowers/assets/ch/ch-sdk/提示说明.txt.meta
  14. 31 0
      Flowers/assets/ch/ch.ts
  15. 9 0
      Flowers/assets/ch/ch.ts.meta
  16. 9 0
      Flowers/assets/ch/ch_module.meta
  17. 9 0
      Flowers/assets/ch/ch_module/Mgr.meta
  18. 89 0
      Flowers/assets/ch/ch_module/Mgr/BundleMgr.ts
  19. 1 0
      Flowers/assets/ch/ch_module/Mgr/BundleMgr.ts.meta
  20. 171 0
      Flowers/assets/ch/ch_module/Mgr/ResMgr.ts
  21. 1 0
      Flowers/assets/ch/ch_module/Mgr/ResMgr.ts.meta
  22. 9 0
      Flowers/assets/ch/ch_module/Module.meta
  23. 212 0
      Flowers/assets/ch/ch_module/Module/Ch_jigsaw.ts
  24. 9 0
      Flowers/assets/ch/ch_module/Module/Ch_jigsaw.ts.meta
  25. 260 0
      Flowers/assets/ch/ch_module/Module/Ch_task.ts
  26. 1 0
      Flowers/assets/ch/ch_module/Module/Ch_task.ts.meta
  27. 160 0
      Flowers/assets/ch/ch_module/Module/Ch_turn.ts
  28. 1 0
      Flowers/assets/ch/ch_module/Module/Ch_turn.ts.meta
  29. 351 0
      Flowers/assets/ch/ch_util.ts
  30. 9 0
      Flowers/assets/ch/ch_util.ts.meta
  31. 402 0
      Flowers/assets/ch/chsdk_inside.d.ts
  32. 9 0
      Flowers/assets/ch/chsdk_inside.d.ts.meta
  33. 9 0
      Flowers/assets/ch/net.meta
  34. 112 0
      Flowers/assets/ch/net/NetBase.ts
  35. 1 0
      Flowers/assets/ch/net/NetBase.ts.meta
  36. 233 0
      Flowers/assets/ch/net/NetPlayer.ts
  37. 1 0
      Flowers/assets/ch/net/NetPlayer.ts.meta
  38. 475 0
      Flowers/assets/ch/net/NetRoom.ts
  39. 1 0
      Flowers/assets/ch/net/NetRoom.ts.meta
  40. 255 0
      Flowers/assets/ch/net/NetTeam.ts
  41. 1 0
      Flowers/assets/ch/net/NetTeam.ts.meta
  42. 114 0
      Flowers/assets/ch/net/TTWsClient.ts
  43. 1 0
      Flowers/assets/ch/net/TTWsClient.ts.meta
  44. 109 0
      Flowers/assets/ch/net/WXWsClient.ts
  45. 1 0
      Flowers/assets/ch/net/WXWsClient.ts.meta
  46. 105 0
      Flowers/assets/ch/net/WsClient.ts
  47. 9 0
      Flowers/assets/ch/net/WsClient.ts.meta
  48. 9 0
      Flowers/assets/ch/net/modules.meta
  49. 4 0
      Flowers/assets/ch/net/modules/msgpack.min.d.ts
  50. 9 0
      Flowers/assets/ch/net/modules/msgpack.min.d.ts.meta
  51. 0 0
      Flowers/assets/ch/net/modules/msgpack.min.js
  52. 17 0
      Flowers/assets/ch/net/modules/msgpack.min.js.meta
  53. 723 0
      Flowers/assets/ch/net/net.ts
  54. 1 0
      Flowers/assets/ch/net/net.ts.meta
  55. 9 0
      Flowers/assets/ch/pvp.meta
  56. 241 0
      Flowers/assets/ch/pvp/ch_pvp.ts
  57. 1 0
      Flowers/assets/ch/pvp/ch_pvp.ts.meta
  58. 9 0
      Flowers/assets/ch/start.meta
  59. 22 0
      Flowers/assets/ch/start/ch_net_help.ts
  60. 1 0
      Flowers/assets/ch/start/ch_net_help.ts.meta
  61. 22 0
      Flowers/assets/ch/start/ch_sdk_comp.ts
  62. 9 0
      Flowers/assets/ch/start/ch_sdk_comp.ts.meta
  63. 214 0
      Flowers/assets/ch/start/ch_start.ts
  64. 1 0
      Flowers/assets/ch/start/ch_start.ts.meta
  65. 9 0
      Flowers/assets/core.meta
  66. 9 0
      Flowers/assets/core/sdk.meta
  67. 9 0
      Flowers/assets/core/ui.meta
  68. 279 0
      Flowers/assets/core/ui/UICanvas.prefab
  69. 13 0
      Flowers/assets/core/ui/UICanvas.prefab.meta
  70. 321 0
      Flowers/assets/core/ui/ui.ts
  71. 1 0
      Flowers/assets/core/ui/ui.ts.meta
  72. 41 0
      Flowers/assets/core/ui/ui_ResolutionAutoFit.ts
  73. 9 0
      Flowers/assets/core/ui/ui_ResolutionAutoFit.ts.meta
  74. 379 0
      Flowers/assets/core/ui/ui_base.ts
  75. 9 0
      Flowers/assets/core/ui/ui_base.ts.meta
  76. 9 0
      Flowers/assets/core/util.meta
  77. 209 0
      Flowers/assets/core/util/ArrayUtil.ts
  78. 9 0
      Flowers/assets/core/util/ArrayUtil.ts.meta
  79. 420 0
      Flowers/assets/core/util/DataTimeUtil.ts
  80. 9 0
      Flowers/assets/core/util/DataTimeUtil.ts.meta
  81. 172 0
      Flowers/assets/core/util/DirectorUtil.ts
  82. 9 0
      Flowers/assets/core/util/DirectorUtil.ts.meta
  83. 46 0
      Flowers/assets/core/util/Instance.ts
  84. 9 0
      Flowers/assets/core/util/Instance.ts.meta
  85. 158 0
      Flowers/assets/core/util/LocalStorageUtil.ts
  86. 9 0
      Flowers/assets/core/util/LocalStorageUtil.ts.meta
  87. 496 0
      Flowers/assets/core/util/MathUtil.ts
  88. 9 0
      Flowers/assets/core/util/MathUtil.ts.meta
  89. 40 0
      Flowers/assets/core/util/PathUtil.ts
  90. 9 0
      Flowers/assets/core/util/PathUtil.ts.meta
  91. 189 0
      Flowers/assets/core/util/ProjectileMathUtil.ts
  92. 1 0
      Flowers/assets/core/util/ProjectileMathUtil.ts.meta
  93. 190 0
      Flowers/assets/core/util/ResUtil.ts
  94. 9 0
      Flowers/assets/core/util/ResUtil.ts.meta
  95. 99 0
      Flowers/assets/core/util/StringUtil.ts
  96. 9 0
      Flowers/assets/core/util/StringUtil.ts.meta
  97. 23 0
      Flowers/assets/core/util/TableLoadUtil.ts
  98. 9 0
      Flowers/assets/core/util/TableLoadUtil.ts.meta
  99. 53 0
      Flowers/assets/core/util/UrlUtil.ts
  100. 9 0
      Flowers/assets/core/util/UrlUtil.ts.meta

+ 2 - 0
Flowers/.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

+ 5 - 0
Flowers/.creator/default-meta.json

@@ -0,0 +1,5 @@
+{
+  "image": {
+    "type": "sprite-frame"
+  }
+}

+ 9 - 0
Flowers/assets/ch.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "c55ae636-8b3a-4f8b-8e86-420b65de6fa1",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

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

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "38e1adf1-e497-447e-ac89-61392a94ed0a",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 365 - 0
Flowers/assets/ch/audio/audio.ts

@@ -0,0 +1,365 @@
+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,
+}
+interface AudioConfig {
+    volume_music: number;
+    volume_effect: number;
+    switch_music: boolean;
+    switch_effect: boolean;
+}
+/**音频播放控制
+ * 需要初始化资源加载方式
+ * 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();
+    private _remote_cache: Map<string, AudioClip> = new Map();
+    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.endsWith('/') ? 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);
+            }
+        } else if (this._load_type == loadType.remote) {
+            if (!sound) {
+                this._remote_cache.forEach((v, k) => {
+                    assetManager.releaseAsset(v);
+                });
+                this._remote_cache.clear();
+            } else {
+                const path = this._remote_url + sound;
+                const clip = this._remote_cache.get(path);
+                if (clip) {
+                    assetManager.releaseAsset(clip);
+                    this._remote_cache.delete(path);
+                }
+            }
+        }
+    }
+    /** 保存音乐音效的音量、开关配置数据到本地 */
+    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: AudioConfig) {
+        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) {
+                const fullPath = this._remote_url + sound + remote_ext;
+                if (this._remote_cache.has(fullPath)) {
+                    this.doPlayOneShot(this._remote_cache.get(fullPath)!, interval);
+                    return;
+                }
+                assetManager.loadRemote(fullPath, (err: Error | null, clip: AudioClip) => {
+                    if (err) {
+                        ch_log.error(err);
+                    } else {
+                        this._remote_cache.set(fullPath, clip);
+                        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
Flowers/assets/ch/audio/audio.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "800c25c3-c527-42e8-9c74-d1964938c8cb",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

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

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "7e49db0b-86bc-4aea-8ef8-2aa8a18d95d8",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
Flowers/assets/ch/ch-sdk/ch-sdk.umd.js


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

@@ -0,0 +1,17 @@
+{
+  "ver": "4.0.24",
+  "importer": "javascript",
+  "imported": true,
+  "uuid": "98a7aaa0-9482-4d5f-b82b-657c06abeaca",
+  "files": [
+    ".js"
+  ],
+  "subMetas": {},
+  "userData": {
+    "loadPluginInEditor": true,
+    "loadPluginInWeb": true,
+    "loadPluginInNative": true,
+    "loadPluginInMiniGame": true,
+    "isPlugin": true
+  }
+}

+ 341 - 0
Flowers/assets/ch/ch-sdk/chsdk.d.ts

@@ -0,0 +1,341 @@
+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
+    }
+    /**平台*/
+    enum pf {
+        /**测试*/
+        web = "web",
+        /**微信*/
+        wx = "wx",
+        /**抖音*/
+        tt = "tt"
+    }
+    type pf_conf = {
+        adUnitId: string;
+        multiton: boolean;
+        inster_unitId?: string;
+        tmplIds?: string[];
+        bannerAds?: {
+            adUnitId: string;
+            adIntervals: number;
+            style: {
+                left: number;
+                top: number;
+                width: number;
+                height: number;
+            };
+        }[];
+    } | null;
+    /**初始化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(): pf;
+    /**玩家平台信息*/
+    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 scene 奖励目的场景
+           playRewardAd("复活")
+           .then(success => {
+             if (success) {
+                 console.log("广告播放成功,用户获得奖励");
+                 // 在这里执行成功后的操作
+             } else {
+                 console.log("广告播放失败,用户未获得奖励");
+                 // 处理广告播放失败的逻辑
+             }
+           })
+           .catch(error => {
+               console.error("发生未处理的错误: ", error);
+            });
+            */
+    function playRewardAd(reason: string): Promise<boolean>;
+    /**展示插屏广告*/
+    function showInsterAd(scene: string): Promise<boolean>;
+    /**关闭插屏广告*/
+    function destroyInsterAd(): void;
+    /**展示Banner广告*/
+    function showBannerAd(index: number, scene: string): Promise<boolean>;
+    /**隐藏Banner广告 */
+    function hideBannerAd(index: number): void;
+    /**设置广告配置*/
+    function setConf(pf: pf, conf: pf_conf): void;
+    /**当前平台是否有侧边栏功能*/
+    function checkHasSidebar(): boolean;
+    /**抖音是否从侧边栏进入游戏,微信是否从我的小程序进入游戏*/
+    function checkFromSidebar(): boolean;
+    /**抖音进入侧边栏*/
+    function goToSidebar(): Promise<boolean>;
+    /**上报自定义埋点事件
+    * @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
Flowers/assets/ch/ch-sdk/chsdk.d.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "e78216d9-6fde-4483-9c8b-1e4644183842",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 45 - 0
Flowers/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
Flowers/assets/ch/ch-sdk/提示说明.txt.meta

@@ -0,0 +1,11 @@
+{
+  "ver": "1.0.1",
+  "importer": "text",
+  "imported": true,
+  "uuid": "e7e6477c-07a0-484e-82fa-0dbfb107ccff",
+  "files": [
+    ".json"
+  ],
+  "subMetas": {},
+  "userData": {}
+}

+ 31 - 0
Flowers/assets/ch/ch.ts

@@ -0,0 +1,31 @@
+import ch_audio from "./audio/audio";
+import pvp from "./pvp/ch_pvp";
+import util from "./ch_util";
+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 audio(): ch_audio { return ch_audio.getInstance(); },
+    wsurl: '',
+}
+

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

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "6a89b820-e575-4417-a7aa-fa7735d59d35",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
Flowers/assets/ch/ch_module.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "8e438c8a-f090-4bf1-8535-6a028648fa32",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
Flowers/assets/ch/ch_module/Mgr.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "24709e72-58c4-470f-9f0c-854eb50eaab1",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 89 - 0
Flowers/assets/ch/ch_module/Mgr/BundleMgr.ts

@@ -0,0 +1,89 @@
+import { assetManager, AssetManager } from "cc";
+
+/**
+ * 分包管理器
+ * 提供分包加载、获取、移除功能。
+ */
+class BundleMgr {
+    /** 私有构造函数*/
+    private constructor() { }
+
+    /** 单例实例 */
+    public static readonly instance: BundleMgr = new BundleMgr();
+
+    /**
+     * 获取指定分包,如果未加载则进行加载。
+     * @param nameOrUrl - 分包名称或URL。
+     * @param onProgress - 进度回调函数。
+     * @returns Promise<AssetManager.Bundle | null> - 加载完成后的Promise。
+     */
+    public async getBundle(nameOrUrl: string, onProgress?: (progress: number) => void): Promise<AssetManager.Bundle | null> {
+        const bundle = assetManager.getBundle(nameOrUrl);
+        if (bundle) return bundle;
+
+        try {
+            const loadedBundle = await this.loadBundle(nameOrUrl);
+            if (onProgress) {
+                await this.loadAssetsWithProgress(loadedBundle, onProgress);
+            }
+            return loadedBundle;
+        } catch (error) {
+            console.error(`分包 ${nameOrUrl} 加载失败`, error.message);
+            return null;
+        }
+    }
+
+    /**
+     * 加载指定分包。
+     * @param nameOrUrl - 分包名称或URL。
+     * @returns Promise<AssetManager.Bundle> - 加载完成后的Promise。
+     */
+    private loadBundle(nameOrUrl: string): Promise<AssetManager.Bundle> {
+        return new Promise((resolve, reject) => {
+            assetManager.loadBundle(nameOrUrl, (err, loadedBundle) => {
+                if (err) {
+                    reject(err);
+                } else {
+                    resolve(loadedBundle);
+                }
+            });
+        });
+    }
+
+    /**
+     * 加载分包中的资源并提供进度反馈。
+     * @param bundle - 已加载的分包。
+     * @param onProgress - 进度回调函数。
+     * @returns Promise<void> - 加载完成后的Promise。
+     */
+    private loadAssetsWithProgress(bundle: AssetManager.Bundle, onProgress: (progress: number) => void): Promise<void> {
+        return new Promise((resolve, reject) => {
+            const assets = bundle.getDirWithPath('');
+            const totalAssets = assets.length;
+            let loadedAssets = 0;
+
+            if (totalAssets === 0) {
+                onProgress(1);
+                resolve();
+                return;
+            }
+
+            assets.forEach((asset) => {
+                bundle.load(asset.path, (err) => {
+                    if (err) {
+                        reject(err);
+                        return;
+                    }
+                    loadedAssets++;
+                    onProgress(loadedAssets / totalAssets);
+                    if (loadedAssets === totalAssets) {
+                        resolve();
+                    }
+                });
+            });
+        });
+    }
+}
+
+/** 分包管理器实例 */
+export const bundleMgr = BundleMgr.instance;

+ 1 - 0
Flowers/assets/ch/ch_module/Mgr/BundleMgr.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"e9e0d8a3-ef3c-492e-b917-1f60ee5d7514","files":[],"subMetas":{},"userData":{}}

+ 171 - 0
Flowers/assets/ch/ch_module/Mgr/ResMgr.ts

@@ -0,0 +1,171 @@
+import { Asset, AssetManager, assetManager } from "cc";
+import { bundleMgr } from "./BundleMgr";
+
+
+/** 
+ * 资源管理器
+ * 提供资源加载、释放功能。
+ */
+class ResMgr {
+    /** 私有构造函数 */
+    private constructor() { }
+
+    /** 单例实例 */
+    public static readonly instance: ResMgr = new ResMgr();
+
+    /**
+     * 加载资源
+     * @param resPath 资源路径
+     * @param onProgress 进度回调函数
+     * @param onComplete 完成回调函数
+     * @returns Promise<T> 加载完成后的Promise
+     */
+    public async loadRes<T extends Asset>(
+        resPath: string,
+        onProgress?: (completedCount: number, totalCount: number, item: any) => void,
+        onComplete?: (err: Error | null, asset: T) => void
+    ): Promise<T> {
+        try {
+            const { bundleName, path } = this.path(resPath);
+            const bundle = await bundleMgr.getBundle(bundleName);
+            return await this.loadAsset<T>(bundle, path, onProgress, onComplete);
+        } catch (error) {
+            console.error(`加载资源失败: ${resPath}`, error.message);
+            throw error;
+        }
+    }
+
+    /**
+     * 加载目录下的所有资源
+     * @param resPath 资源路径
+     * @param onProgress 进度回调函数
+     * @param onComplete 完成回调函数
+     * @returns Promise<Array<Asset>> 加载完成后的Promise
+     */
+    public async loadResDir(
+        resPath: string,
+        onProgress?: (completedCount: number, totalCount: number, item: any) => void,
+        onComplete?: (err: Error | null, assets: Array<Asset>) => void
+    ): Promise<Array<Asset>> {
+        try {
+            const { bundleName, path } = this.path(resPath);
+            const bundle = await bundleMgr.getBundle(bundleName);
+            return await this.loadAssetDir(bundle, path, onProgress, onComplete);
+        } catch (error) {
+            console.error(`加载目录失败: ${resPath}`, error.message);
+            throw error;
+        }
+    }
+
+    /**
+     * 释放指定分包单个资源
+     * @param resPath 资源路径
+     */
+    public releaseRes(resPath: string): void {
+        const { bundleName, path } = this.path(resPath);
+        const bundle = assetManager.getBundle(bundleName);
+        if (bundle) {
+            bundle.release(path);
+        } else {
+            console.error(`分包 ${bundleName} 未找到,无法释放资源 ${path}。`);
+        }
+    }
+
+    /**
+     * 释放指定分包全部资源
+     * @param bundleName 分包名称
+     */
+    public releaseBundle(bundleName: string): void {
+        const bundle = assetManager.getBundle(bundleName);
+        if (bundle) {
+            bundle.releaseAll();
+            assetManager.removeBundle(bundle);
+        } else {
+            console.error(`分包 ${bundleName} 未找到,无法移除。`);
+        }
+    }
+
+    /** 移除所有分包 */
+    public releaseAll(): void {
+        assetManager.releaseAll();
+    }
+
+    /**
+     * 加载单个资源的辅助方法
+     * @param bundle 资源所在的分包
+     * @param path 资源路径
+     * @param onProgress 进度回调函数
+     * @param onComplete 完成回调函数
+     * @returns Promise<T> 加载完成后的Promise
+     */
+    private loadAsset<T extends Asset>(
+        bundle: AssetManager.Bundle,
+        path: string,
+        onProgress?: (completedCount: number, totalCount: number, item: any) => void,
+        onComplete?: (err: Error | null, asset: T) => void
+    ): Promise<T> {
+        return new Promise<T>((resolve, reject) => {
+            bundle.load(
+                path,
+                (completedCount, totalCount, item) => onProgress?.(completedCount, totalCount, item),
+                (err, asset) => {
+                    onComplete?.(err, asset as T);
+                    if (err) {
+                        console.error(`从分包加载资源 ${path} 失败`, err.message);
+                        reject(err);
+                    } else {
+                        resolve(asset as T);
+                    }
+                }
+            );
+        });
+    }
+
+    /**
+     * 加载目录下所有资源的辅助方法
+     * @param bundle 资源所在的分包
+     * @param path 目录路径
+     * @param onProgress 进度回调函数
+     * @param onComplete 完成回调函数
+     * @returns Promise<Array<Asset>> 加载完成后的Promise
+     */
+    private loadAssetDir(
+        bundle: AssetManager.Bundle,
+        path: string,
+        onProgress?: (completedCount: number, totalCount: number, item: any) => void,
+        onComplete?: (err: Error | null, assets: Array<Asset>) => void
+    ): Promise<Array<Asset>> {
+        return new Promise<Array<Asset>>((resolve, reject) => {
+            bundle.loadDir(
+                path,
+                (completedCount, totalCount, item) => onProgress?.(completedCount, totalCount, item),
+                (err, assets) => {
+                    onComplete?.(err, assets);
+                    if (err) {
+                        console.error(`从分包加载目录 ${path} 失败`, err.message);
+                        reject(err);
+                    } else {
+                        resolve(assets);
+                    }
+                }
+            );
+        });
+    }
+
+    /**
+ * 解析资源路径并将其分解为包名和路径
+ *
+ * @param resPath - 要解析的资源路径
+ * @returns 返回包含 bundleName 和 path 的对象
+ */
+    private path(resPath: string): { bundleName: string; path: string } {
+        const [bundleName, ...pathParts] = resPath.split('/');
+        return {
+            bundleName,
+            path: pathParts.join('/')
+        };
+    }
+}
+
+/** 资源管理器实例 */
+export const resMgr = ResMgr.instance;

+ 1 - 0
Flowers/assets/ch/ch_module/Mgr/ResMgr.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"3ba040f7-b937-46a0-8061-14e03a47d940","files":[],"subMetas":{},"userData":{}}

+ 9 - 0
Flowers/assets/ch/ch_module/Module.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "d38e9a66-0829-405b-8f94-ce4d64cbc8e0",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 212 - 0
Flowers/assets/ch/ch_module/Module/Ch_jigsaw.ts

@@ -0,0 +1,212 @@
+import { AssetManager, director, instantiate, isValid, Node, Prefab } from "cc";
+import { bundleMgr } from "../Mgr/BundleMgr";
+import { resMgr } from "../Mgr/ResMgr";
+
+export interface evt {
+    onComClick: () => void;
+    onCloseGame: () => void;
+    onGetReward: (itemId: number, count: number) => void;
+    onViedoReward: (success: boolean) => void;
+    onSaveGameData: (save_data: JigsawObj) => void;
+}
+
+export interface JigsawObj {
+    /** 放置拼图数据 */
+    jigsawData: { [key: number]: Array<[number, number]> };
+    /** 拥有的碎片数据 */
+    cardData: { [key: number]: Array<number> };
+    /** 拥有道具数据 */
+    itemsData: { [key: number]: number };
+    /** 完成领取拼图奖励数据 */
+    rewardImgData: Array<number>;
+    /** 解锁索引位置点 */
+    unlockIndex: number;
+    /** 添加的碎片数量 */
+    addCardCount: number;
+}
+
+/** 主窗口 */
+const mainPopup = "Jigsaw/Prefab/Popup/jigsawListPopup";
+/** 包名 */
+const bundleName = "Jigsaw";
+
+/** 定义一个类 */
+class Ch_jigsaw {
+    /** 私有构造函数 */
+    private constructor() { }
+    /** 单例模式 */
+    public static readonly instance: Ch_jigsaw = new Ch_jigsaw();
+    private _firstParent: Node | null = null;
+    private _secondParent: Node | null = null;
+    private _data: JigsawObj = {
+        /** 放置拼图数据 */
+        jigsawData: {},
+        /** 拥有的碎片数据 */
+        cardData: {},
+        /** 拥有道具数据 */
+        itemsData: {},
+        /** 完成领取拼图奖励数据 */
+        rewardImgData: [],
+        /** 解锁索引位置点 */
+        unlockIndex: 0,
+        /** 添加的碎片数量 */
+        addCardCount: 0
+    };
+    private _evt = chsdk.get_new_event<evt>();
+    get evt() {
+        return this._evt;
+    }
+    /**
+     *  初始化数据
+     * @param data 
+     */
+    init(data: JigsawObj) {
+        // 初始化
+        this._data = data ? data : this._data;
+    }
+
+
+    /** 公共点击事件 */
+    onComClick() {
+        this.evt.emit(this.evt.key.onComClick);
+    }
+
+    /**
+     * 获得奖励
+     * @param itemId 
+     */
+    onGetReward(itemId: number, count: number) {
+        // console.log("获得奖励", itemId);
+        this.evt.emit(this.evt.key.onGetReward, itemId, count);
+    }
+
+    /**
+     * 看视频广告
+     */
+    onViedoReward(success: boolean) {
+        this.evt.emit(this.evt.key.onViedoReward, success);
+    }
+
+    /**
+     * 保存游戏数据
+     * @param save_data 
+     */
+    onSaveGameData(save_data: JigsawObj) {
+        this._data = save_data;
+        this.evt.emit(this.evt.key.onSaveGameData, save_data);
+    }
+
+    /**
+     * 添加碎片
+     * @param count 
+     */
+    onAddCard(count: number) {
+        this._data.addCardCount += count;
+        this.evt.emit(this.evt.key.onSaveGameData, this._data);
+    }
+
+    /**
+     * 获取未使用的碎片数量
+     */
+    getNoUseCardCount() {
+        let count = this._data.addCardCount;
+        let cardData = this._data.cardData;
+        let placeData = this._data.jigsawData;
+        for (let key in placeData) {
+            if (placeData.hasOwnProperty(key)) {
+                let arr = placeData[key];
+                for (let i = 0; i < arr.length; i++) {
+                    let [x, y] = arr[i];
+                    if (x != y) {
+                        count++;
+                    }
+                }
+            }
+        }
+        for (let key in cardData) {
+            if (cardData.hasOwnProperty(key)) {
+                let arr = cardData[key];
+                count += arr.length;
+            }
+        }
+        return count;
+    }
+
+    /**
+     * 获取游戏数据
+     */
+    getGameData() {
+        return this._data;
+    }
+
+
+    /**
+     *  获取父节点
+     * @returns 
+     */
+    getInterParent() {
+        return this._secondParent;
+    }
+
+    /**
+     * 加载分包
+     * @param preogerss 
+     * @param onComplete 
+     */
+    loadBundle(preogerss?: (progress: number) => void, onComplete?: (err: Error | null, bundle: AssetManager.Bundle | null) => void): void {
+        const onProgress = (progress: number) => {
+            chsdk.log.debug(`加载进度:${progress}%`);
+            if (preogerss) {
+                preogerss(progress);
+            }
+        };
+        bundleMgr.getBundle(bundleName, onProgress)
+            .then((bundle) => {
+                if (bundle) {
+                    chsdk.log.debug('分包加载成功!');
+                    // 在这里可以执行加载完成后的操作
+                    if (onComplete) {
+                        onComplete(null, bundle);
+                    }
+                } else {
+                    chsdk.log.debug('分包加载失败!');
+                    // 这里可以执行失败后的操作
+                    setTimeout(() => {
+                        // 这里可以尝试重新加载分包
+                        this.loadBundle(preogerss, onComplete);
+                    }, 1000);
+                }
+            })
+    }
+
+    /**
+     * 显示主窗口
+     * @param parent 父节点
+     * @param interParent 界面父节点
+     */
+    show(parent?: Node, interParent?: Node) {
+        this._firstParent = parent ? parent : null;
+        this._secondParent = interParent ? interParent : null;
+        resMgr.loadRes<Prefab>(mainPopup).then((prefab: Prefab) => {
+            let node = instantiate(prefab);
+            if (parent) {
+                if (isValid(parent)) {
+                    node.parent = parent;
+                } else {
+                    node.destroy();
+                }
+            }
+            else {
+                let canvas = director.getScene().getChildByName("Canvas");
+                if (canvas) {
+                    node.parent = canvas;
+                }
+            }
+        });
+    }
+}
+
+/** 导出单例模式的实例 */
+export const ch_jigsaw: Ch_jigsaw = Ch_jigsaw.instance;
+
+

+ 9 - 0
Flowers/assets/ch/ch_module/Module/Ch_jigsaw.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "19696f4c-96c5-43b7-8958-37755f0d0500",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 260 - 0
Flowers/assets/ch/ch_module/Module/Ch_task.ts

@@ -0,0 +1,260 @@
+import { AssetManager, director, instantiate, isValid, Node, Prefab } from "cc";
+import { bundleMgr } from "../Mgr/BundleMgr";
+import { resMgr } from "../Mgr/ResMgr";
+
+export interface evt {
+    onComClick: () => void;
+    onGetReward: (itemId: Array<{ itemId: number, num: number }>) => void;
+    onViedoReward: (success: boolean) => void;
+    onSaveGameData: (save_data: TaskObj) => void;
+    onJumpGameLevel: (callBack: Function) => void;
+    onShareGame: (callBack: Function) => void;
+    onUpdatePreTask: (count: number) => void;
+    onJump: (type: TaskType) => void;
+}
+/**任务类型 */
+export enum TaskType {
+    ITEM, //看视频直接领取
+    LEVEL,//完成关卡直接领取
+    SHARE,// 分享直接领取
+    VIDEO, //看视频领取奖励
+    JIGSAW, // 完成拼图
+    PVP, // 完成pvp
+}
+export class TaskObj {
+    taskIds: Array<number>; //完成任务领取id
+    preTaskIds: Array<number>; //完成任务未领取id
+    taskTypeData: { [key: number]: { taskType: number, count: number } }; //任务类型数据
+    curTimetatmp: number; //当前时间戳
+}
+/** 主窗口 */
+const mainPopup = "Task/Prefab/Popup/taskPopup";
+/** 包名 */
+const bundleName = "Task";
+/** 定义一个类 */
+class Ch_task {
+    /** 私有构造函数 */
+    private constructor() { }
+    /** 单例模式 */
+    public static readonly instance: Ch_task = new Ch_task();
+    private _firstParent: Node | null = null;
+    private _secondParent: Node | null = null;
+    private _taskCfg: { [key: number]: { id: number, type: number, value: number } } = null;
+    private _data: TaskObj = {
+        taskIds: [],
+        preTaskIds: [],
+        taskTypeData: {},
+        curTimetatmp: 0
+    };
+    private _evt = chsdk.get_new_event<evt>();
+    get evt() {
+        return this._evt;
+    }
+
+    /**
+     *  初始化数据
+     * @param data 
+     */
+    init(data: TaskObj) {
+        // 初始化
+        this._data = data ? data : this._data;
+
+    }
+
+    /**
+     * 初始配置
+     * @param data 
+     */
+    initTaskCfg(data: { [key: number]: { id: number, type: number, value: number } }) {
+        if (!this._taskCfg || JSON.stringify(this._taskCfg) != JSON.stringify(data)) {
+            this._taskCfg = data;
+        }
+    }
+
+    /** 公共点击事件 */
+    onComClick() {
+        this.evt.emit(this.evt.key.onComClick);
+    }
+
+    /**
+     * 获得奖励
+     * @param itemId 
+     */
+    onGetReward(items: Array<{ itemId: number, num: number }>) {
+        this.evt.emit(this.evt.key.onGetReward, items);
+    }
+
+    /**
+    * 看视频广告
+    */
+    onViedoReward(success: boolean) {
+        this.evt.emit(this.evt.key.onViedoReward, success);
+    }
+
+    /**
+     * 跳到游戏关卡
+     */
+    onJumpGameLevel() {
+        this.evt.emit(this.evt.key.onJumpGameLevel, (isFinish: boolean) => {
+            if (isFinish) {
+                //更新任务数据
+                this.updateTaskTypeData(TaskType.LEVEL);
+                this.onSaveGameData();
+                this.onUpdatePreTask();
+            }
+        });
+    }
+    onJump(type: TaskType) {
+        this.evt.emit(this.evt.key.onJump, type);
+    }
+    /**
+     * 分享游戏
+     */
+    onShareGame() {
+        this.evt.emit(this.evt.key.onShareGame, (isSuccess: boolean) => {
+            if (isSuccess) {
+                //更新任务数据
+                this.updateTaskTypeData(TaskType.SHARE);
+                this.onSaveGameData();
+                this.onUpdatePreTask();
+            }
+        });
+    }
+
+    /**
+    *  更新任务类型数据
+    * @param type 任务类型
+    * @param count 
+    */
+    updateTaskTypeData(type: number, count: number = 1) {
+        let taskTypeData = this._data.taskTypeData[type];
+        if (taskTypeData) {
+            taskTypeData.count += count;
+        } else {
+            this._data.taskTypeData[type] = { taskType: type, count: count };
+        }
+        //更新待领取任务
+        this.updatePreTaksIds(this._data.taskTypeData[type].taskType, this._data.taskTypeData[type].count);
+    }
+
+
+    /**
+       * 更新待领取的奖励id
+       * @param itemId 
+       */
+    private updatePreTaksIds(type: number, count: number) {
+        for (let key in this._taskCfg) {
+            let item = this._taskCfg[key];
+            if (item.type == type && item.value <= count) {
+                if (this._data.preTaskIds.indexOf(item.id) == -1) {
+                    this._data.preTaskIds.push(item.id);
+                }
+            }
+        }
+    }
+
+    /**
+     * 更新待领取的任务id
+     */
+    onUpdatePreTask() {
+        this.evt.emit(this.evt.key.onUpdatePreTask, this._data.preTaskIds.length);
+    }
+
+
+    /**
+     * 保存游戏数据
+     */
+    onSaveGameData() {
+        this.evt.emit(this.evt.key.onSaveGameData, this._data);
+    }
+
+    /**
+     *  获取内部父节点
+     * @returns 
+     */
+    getInterParent() {
+        return this._secondParent;
+    }
+
+    /**
+     * 获取游戏数据
+     */
+    getGameData() {
+        return this._data;
+    }
+
+    /**
+     * 清空数据
+     */
+    protected clear() {
+        this._data = {
+            taskIds: [],
+            preTaskIds: [],
+            taskTypeData: {},
+            curTimetatmp: 0
+        };
+    }
+
+
+    /**
+     * 加载分包
+     * @param preogerss 
+     * @param onComplete 
+     */
+    loadBundle(preogerss?: (progress: number) => void, onComplete?: (err: Error | null, bundle: AssetManager.Bundle | null) => void): void {
+        const onProgress = (progress: number) => {
+            chsdk.log.debug(`加载进度:${progress}%`);
+            if (preogerss) {
+                preogerss(progress);
+            }
+        };
+        bundleMgr.getBundle(bundleName, onProgress)
+            .then((bundle) => {
+                if (bundle) {
+                    chsdk.log.debug('分包加载成功!');
+                    // 在这里可以执行加载完成后的操作
+                    if (onComplete) {
+                        onComplete(null, bundle);
+                    }
+                } else {
+                    chsdk.log.debug('分包加载失败!');
+                    // 这里可以执行失败后的操作
+                    setTimeout(() => {
+                        // 这里可以尝试重新加载分包
+                        this.loadBundle(preogerss, onComplete);
+                    }, 1000);
+                }
+            })
+    }
+
+    /**
+     * 显示主窗口
+     * @param parent 父节点
+     * @param interParent 界面父节点
+     */
+    show(parent?: Node, interParent?: Node) {
+        this._firstParent = parent ? parent : null;
+        this._secondParent = interParent ? interParent : null;
+        resMgr.loadRes<Prefab>(mainPopup).then((prefab: Prefab) => {
+            let node = instantiate(prefab);
+            if (parent) {
+                if (isValid(parent)) {
+                    node.parent = parent;
+                } else {
+                    node.destroy();
+                }
+            }
+            else {
+                let canvas = director.getScene().getChildByName("Canvas");
+                if (canvas) {
+                    node.parent = canvas;
+                }
+            }
+        });
+    }
+}
+
+/** 导出单例模式的实例 */
+export const ch_task: Ch_task = Ch_task.instance;
+
+

+ 1 - 0
Flowers/assets/ch/ch_module/Module/Ch_task.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"1cc5b2de-6999-40ee-892d-bd4ff6206354","files":[],"subMetas":{},"userData":{}}

+ 160 - 0
Flowers/assets/ch/ch_module/Module/Ch_turn.ts

@@ -0,0 +1,160 @@
+import { AssetManager, director, instantiate, isValid, Node, Prefab } from "cc";
+import { bundleMgr } from "../Mgr/BundleMgr";
+import { resMgr } from "../Mgr/ResMgr";
+
+export interface evt {
+    onComClick: () => void;
+    onGetReward: (itemId: number, count: number) => void;
+    onViedoReward: (success: boolean) => void;
+    onSaveGameData: (save_data: number) => void;
+}
+
+export interface TurnObj {
+    itemArr: Array<{ itemId: number, count: number, weigth: number }>;
+    total: number;
+}
+
+/** 主窗口 */
+const mainPopup = "Turn/Prefab/Popup/turnPopup";
+/** 包名 */
+const bundleName = "Turn";
+
+/** 定义一个类 */
+class Ch_turn {
+    /** 私有构造函数 */
+    private constructor() { }
+    /** 单例模式 */
+    public static readonly instance: Ch_turn = new Ch_turn();
+    private _firstParent: Node | null = null;
+    private _secondParent: Node | null = null;
+    private _itemArr: Array<{ itemId: number, count: number, weigth: number }> = [];
+    private _data: TurnObj = { itemArr: this._itemArr, total: 0 };
+    private _evt = chsdk.get_new_event<evt>();
+    get evt() {
+        return this._evt;
+    }
+
+    /**
+     *  初始化数据
+     * @param data 
+     */
+    init(total: number = 0) {
+        // 初始化
+        this._data.total = total ? total : 0;
+    }
+
+
+    /** 公共点击事件 */
+    onComClick() {
+        this.evt.emit(this.evt.key.onComClick);
+    }
+
+    /**
+     * 获得奖励
+     * @param itemId 
+     */
+    onGetReward(itemId: number, count: number) {
+        // console.log("获得奖励", itemId);
+        this.evt.emit(this.evt.key.onGetReward, itemId, count);
+    }
+
+    /**
+     * 看视频广告
+     */
+    onViedoReward(success: boolean) {
+        this.evt.emit(this.evt.key.onViedoReward, success);
+    }
+
+    /**
+     * 保存游戏数据
+     * @param save_data 
+     */
+    onSaveGameData(total: number) {
+        this._data.total = total;
+        this.evt.emit(this.evt.key.onSaveGameData, total);
+    }
+
+    /**
+     *  获取内部父节点
+     * @returns 
+     */
+    getInterParent() {
+        return this._secondParent;
+    }
+
+    /**
+     * 获取游戏数据
+     */
+    getGameData() {
+        return this._data;
+    }
+
+    /**
+     * 清空数据
+     */
+    protected clear() {
+        this._data.total = 0;
+    }
+
+    /**
+     * 加载分包
+     * @param preogerss 
+     * @param onComplete 
+     */
+    loadBundle(preogerss?: (progress: number) => void, onComplete?: (err: Error | null, bundle: AssetManager.Bundle | null) => void): void {
+        const onProgress = (progress: number) => {
+            chsdk.log.debug(`加载进度:${progress}%`);
+            if (preogerss) {
+                preogerss(progress);
+            }
+        };
+        bundleMgr.getBundle(bundleName, onProgress)
+            .then((bundle) => {
+                if (bundle) {
+                    chsdk.log.debug('分包加载成功!');
+                    // 在这里可以执行加载完成后的操作
+                    if (onComplete) {
+                        onComplete(null, bundle);
+                    }
+                } else {
+                    chsdk.log.debug('分包加载失败!');
+                    // 这里可以执行失败后的操作
+                    setTimeout(() => {
+                        // 这里可以尝试重新加载分包
+                        this.loadBundle(preogerss, onComplete);
+                    }, 1000);
+                }
+            })
+    }
+
+    /**
+     * 显示主窗口
+     * @param parent 父节点
+     * @param interParent 界面父节点
+     */
+    show(parent?: Node, interParent?: Node) {
+        this._firstParent = parent ? parent : null;
+        this._secondParent = interParent ? interParent : null;
+        resMgr.loadRes<Prefab>(mainPopup).then((prefab: Prefab) => {
+            let node = instantiate(prefab);
+            if (parent) {
+                if (isValid(parent)) {
+                    node.parent = parent;
+                } else {
+                    node.destroy();
+                }
+            }
+            else {
+                let canvas = director.getScene().getChildByName("Canvas");
+                if (canvas) {
+                    node.parent = canvas;
+                }
+            }
+        });
+    }
+}
+
+/** 导出单例模式的实例 */
+export const ch_turn: Ch_turn = Ch_turn.instance;
+
+

+ 1 - 0
Flowers/assets/ch/ch_module/Module/Ch_turn.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"c76e069a-80ca-4f9e-8e66-1ebe3e9ef0eb","files":[],"subMetas":{},"userData":{}}

+ 351 - 0
Flowers/assets/ch/ch_util.ts

@@ -0,0 +1,351 @@
+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
Flowers/assets/ch/ch_util.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "a9303097-d3ab-4e26-b93f-faf28d0f92a6",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 402 - 0
Flowers/assets/ch/chsdk_inside.d.ts

@@ -0,0 +1,402 @@
+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
+    }
+    /**获取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;
+        }[];
+    }>;
+    /**开始录屏*/
+    export function recorderStart(duration?: number): void;
+    /**结束录屏*/
+    export function recorderStop(): void;
+    /**分享录屏 成功返回videoId,失败返回null*/
+    export function shareRecord(options: {
+        templateId?: string;
+        imageUrl?: string;
+        title?: string;
+        desc?: string;
+        query?: string;
+        path?: string;
+        topics?: string[];
+    }): Promise<string | null>;
+    /**加载关卡配置列表
+     * @param offset 偏移量
+     * @param limit 拉取个数
+     * @returns errCode 错误码 errMsg 错误信息  data 关卡配置列表
+     */
+    export function loadLevelConfigList(offset: number, limit: number): Promise<{
+        code: number;
+        err?: string;
+        data?: {
+            [key: string]: any;
+        };
+    }>;
+    /**上传关卡配置
+    * @param gid 游戏id(为空的话为当前游戏id)
+    * @param levelKey 关卡key
+    * @param config 关卡配置
+    * @param secret 秘钥 可选(默认'ch_sdk')
+    * @returns errCode 错误码 errMsg 错误信息  data 关卡配置列表
+    */
+    export function uploadLevelConfig(levelKey: string, config: {
+        [key: string]: any;
+    }, gid?: string, secret?: string): Promise<{
+        code: number;
+        err?: string;
+    }>;
+    /**读取关卡配置
+     * @param levelKeys 关卡key列表
+     * @param secret 秘钥 可选(默认'ch_sdk')
+     * @param refesh 是否强制刷新,否则从缓存中拿,默认false
+     * @returns errCode 错误码 errMsg 错误信息  data 关卡配置列表
+     */
+    export function preloadLevelConfig(levelKeys: string[], secret?: string, refesh?: boolean): Promise<{
+        code: number;
+        err?: string;
+        data?: {
+            [key: string]: any;
+        };
+    }>;
+    /**获取关卡配置
+    * @param levelKey 关卡key
+    * @param secret 秘钥 可选(默认'ch_sdk')
+    * @param refesh 强制刷新否则从缓存中拿
+    * @returns errCode 错误码 errMsg 错误信息  data 关卡配置列表
+    */
+    export function getLevelConfig(levelKey: string, secret?: string, refesh?: boolean): Promise<{
+        code: number;
+        err?: string;
+        data?: {
+            [key: string]: any;
+        };
+    }>;
+    /**创建反馈按钮
+    * @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;
+        };
+        private _isEmitting;
+        private _pendingOperations;
+        /**
+        * 监听事件
+        * @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;
+        onShowAd(type: 'Rewarded' | 'Interstitial' | 'Banner', 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
Flowers/assets/ch/chsdk_inside.d.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "0ddd0740-37ee-4339-bfc3-33d9302a99a7",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

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

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "8d783bae-c476-461f-b3e6-3cb70421533f",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 112 - 0
Flowers/assets/ch/net/NetBase.ts

@@ -0,0 +1,112 @@
+/**网络变量基类*/
+export class NetBase {
+    private _id: string;
+    public get Id(): string { return this._id };
+    private dataMap: { [key: string]: any } = {};
+    private dirtyMap: { [key: string]: any } = {};
+    private _dirty: boolean = false;
+    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;
+        this._dirty = true;
+    }
+    /**获取数据的值*/
+    protected getValue(key: string): any {
+        return this.dataMap[key];
+    }
+    /**处理脏数据*/
+    private doDirtyData(f: (data: { [key: string]: any }) => void): void {
+        if (!this._dirty) return;
+        f(this.dirtyMap);
+        this.dirtyMap = {};
+        this._dirty = false;
+    }
+    protected dispose(): void {
+        this.dataMap = {};
+        this.dirtyMap = {};
+        this._dirty = false;
+    }
+}
+export function transUserDataform(data: any): UserData {
+    data.hid = Number.parseInt(data.hid);
+    data.province = chsdk.provinceCode2Name(data.hid) ?? '其它';
+    data.userId = Number.parseInt(data.userId);
+    data.loginTime = Number.parseInt(data.loginTime);
+    data.registerTime = Number.parseInt(data.registerTime);
+    data.nickName = data.nickName || '玩家' + data.userId;
+    return data;
+}
+export function transUserExtraDataform<T>(data: string): T | null {
+    return data ? JSON.parse(data) as T : null
+}
+/**玩家信息*/
+export interface 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
+}
+export interface Elements {
+    /**排名*/Rank: number,
+    /**段位信息*/Level: Level,
+    /**玩家基本信息*/UserData: UserData
+}
+/**段位信息*/
+export interface Level {
+    /**段位*/ LowerRank: number,
+    /**该段位下的级别*/  Rank: number,
+    /**该段位下星星数*/ Star: number
+}
+export interface results {
+    /**玩家Id*/Id: string,
+    /**变动星星数*/AddScore: number,
+    /**是否完成*/IsFinish: boolean,
+    /**本局排名*/Rank: number,
+    /**最终星星数*/Score: number,
+    /**段位排名*/TotalRank: number,
+    /**段位信息*/Level: Level,
+    /**玩家基本信息*/UserData: UserData,
+    /**相近排名玩家信息*/Elements: Elements[],
+}
+
+export interface RankHistory {
+     /**是否处理过奖励 */IsReceive: boolean,
+     /**排名*/Rank: number,
+     /**分数*/Score: number,
+     /**所在赛季*/Season: number
+}
+export interface SeasonInfo {
+     /**当前赛季的编号 */Season: number,
+     /**赛季结束时间的时间戳 */SeasonEndTime: number,
+     /**赛季结束时间的时间戳 */SeasonStartTime: number,
+     /**历史赛季数组 */RankHistory: RankHistory[]
+}
+export interface UserRank<T> {
+    /**玩家基本信息*/UserData: UserData,
+    /**玩家扩展数据*/UserDataExtra: T,
+    /**段位信息*/ Level: Level,
+    /**排名*/ Rank: number,
+}
+export interface Ranks<T> {
+    /**排名列表*/list: UserRank<T>[],
+    /**玩家自己的排名*/own: UserRank<T>,
+}

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

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"8f540fe1-affb-4680-9926-7807c2d336f1","files":[],"subMetas":{},"userData":{}}

+ 233 - 0
Flowers/assets/ch/net/NetPlayer.ts

@@ -0,0 +1,233 @@
+import { NetBase, transUserDataform, transUserExtraDataform, UserData } 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}`>>;
+type protocol = { extra?: Record<string, any> } & chsdk.OmitIndex<chsdk.EventsMap>;
+/**网络玩家*/
+export class NetPlayer<GD extends protocol> extends NetBase {
+    public readonly evt = chsdk.get_new_event<PickEvent<GD, 'p_', 'online' | 'finish' | 'ready' | 'exit'>>();
+    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 _exit: boolean = false;
+    /**是否退出游戏*/
+    private get isExit(): boolean { return this._exit };
+    private _totalRank: number = 0;
+    /**总排名 */
+    public get totalRank(): number { return this._totalRank };
+    private _ishost: boolean = true;
+    /**活跃可互动状态(在线,还没有离开房间,还没有结算名次)*/
+    public get active(): boolean { return this.online && !this.isExit && !this._rank };
+    /**房间位置0开头*/
+    public get location(): number { return this._location };
+    private _userData: UserData;
+    /**游戏id*/
+    public get gid(): string { return this._userData.gid };
+    /**头像*/
+    public get head(): string { return this._userData.head };
+    /**省id*/
+    public get hid(): number { return this._userData.hid };
+    /**来自省份 */
+    public get province(): string { return this._userData.province };
+    public get ip(): string { return this._userData.ip };
+    /**最近登录时间戳*/
+    public get loginTime(): number { return this._userData.loginTime };
+    /**名称*/
+    public get nickName(): string { return this._userData.nickName };
+    /**平台d */
+    public get openId(): string { return this._userData.openId };
+    /**权限*/
+    public get option(): string { return this._userData.option };
+    /**平台*/
+    public get pf(): string { return this._userData.pf };
+    /**用户Id */
+    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;
+    private _userDataExtra: GD['extra'] = null;
+    /**玩家扩展数据 */
+    public get userDataExtra(): GD['extra'] { return this._userDataExtra; };
+    /**获取玩家扩展数据某个字段*/
+    public getUserDataExtraField<K extends keyof GD['extra']>(key: K): GD['extra'][K] { return this._userDataExtra?.[key]; }
+    /**段位信息*/
+    public get level(): { LowerRank: number, Rank: number, Star: number } | null { return this._rankInfo };
+    /**是否是当前玩家自己*/
+    private _isOwn: boolean = false;
+    public get isOwn(): boolean { return this._isOwn };
+    /**是否是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, userDataExtra?: 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 = transUserDataform(pd.userData);
+        this._rankInfo = pd.userRank;
+        this._userDataExtra = transUserExtraDataform(pd.userDataExtra);
+        if (pd.gameData) this.initValue(pd.gameData);
+    }
+    private set_userExtra(data: any): void {
+        this._userDataExtra = data;
+        (this.evt as any)._emit('p_extra');
+    }
+    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_own(isOwn: boolean): void {
+        this._isOwn = isOwn;
+    }
+    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);
+    }
+    /**创建自定义obj数据*/
+    public creatObj<T extends { [key: string]: any }>(data: T): string {
+        if (!this.isPermission) return;
+        let oid: number = 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('p_obj', key, data);
+        return key;
+    }
+    /**根据key获取obj数据*/
+    public getObj<T extends { [key: string]: any }>(key: string): T {
+        return super.getValue(key);
+    }
+    /**获取当前所有的obj数据*/
+    public getAllObj<T extends { [key: string]: any }>(): { key: string, data: T }[] {
+        const list: { key: string, data: T }[] = [];
+        const oid = super.getValue('oid') ?? 0;
+        for (let i = 1; i <= oid; i++) {
+            const key = 'obj_' + i;
+            const data = super.getValue(key);
+            if (data) list.push({ key: key, data: data === 0 ? null : data });
+        }
+        return list;
+    }
+    /**不建议经常更改obj */
+    public changeObj<T extends { [key: string]: any }>(key: string, data: T): void {
+        if (!this.isPermission) return;
+        const old = super.getValue(key);
+        if (old) {
+            super.setValue(key, data);
+            super.setValueDirty(key, data);
+            (this.evt as any)._emit('p_obj', key, data, old);
+        }
+    }
+    /**删除obj数据*/
+    public deleteObj(key: string) {
+        if (!this.isPermission) return;
+        const old = super.getValue(key);
+        if (old) {
+            super.setValue(key, 0);
+            super.setValueDirty(key, 0);
+            (this.evt as any)._emit('p_obj', key, null, old);
+        }
+    }
+    /**修改某个键的值*/
+    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;
+        const evt: { evt: 0 | 1, key: string, data: any, old: any }[] = [];
+        Object.keys(data).forEach(key => {
+            const old = super.getValue(key);
+            super.setValue(key, data[key]);
+            if (key === 'oid') {
+                return;
+            } else if (key.startsWith('obj_')) {
+                evt.push({ evt: 1, key: key, data: data[key] === 0 ? null : data[key], old: old });
+            } else {
+                evt.push({ evt: 0, key: key, data: data[key], old: old });
+            }
+        });
+        const e = (this.evt as any)
+        for (let i = 0; i < evt.length; i++) {
+            const ee = evt[i];
+            if (!ee) continue;
+            if (ee.evt === 1) {
+                e._emit('p_obj', ee.key, ee.data, ee.old);
+            } else {
+                e._emit(ee.key, ee.data, ee.old);
+            }
+        }
+    }
+    private setFinish(rank: number): void {
+        this._rank = rank;
+        (this.evt as any)._emit('finish', this._rank);
+    }
+    private exit(): void {
+        this._exit = true;
+        (this.evt as any)._emit('exit');
+    }
+    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
Flowers/assets/ch/net/NetPlayer.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"360c11e4-0499-4012-96e3-63d989687a4d","files":[],"subMetas":{},"userData":{}}

+ 475 - 0
Flowers/assets/ch/net/NetRoom.ts

@@ -0,0 +1,475 @@
+import { NetBase, results, transUserDataform } 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}`>>;
+const C2S_TALK = 'c2s_talk';
+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_FINISH = 'c2s_finish';
+const C2S_UPDATE_USER_GAMEDATA = 'c2s_update_user_gameData';
+const C2S_UPDATE_ROOM_GAMEDATA = 'c2s_update_room_gameData';
+/**网络房间*/
+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 };
+    private _own_id: string;
+    public get ownId(): string { return this._own_id };
+    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 }> = [];
+    private _send: (type: string, data?: any) => void;
+    /**自己是否是主机*/
+    public get isHost(): boolean {
+        return this._own_id === this._hostId;
+    }
+    //
+    constructor(ownId: string, roomData: any, playerData: any, send: (type: string, data?: any) => void) {
+        super(roomData.roomId);
+        this._send = send;
+        this._own_id = ownId;
+        this._hostId = roomData.hostInfo;
+        this._status = roomData.status;
+        let ps = Object.keys(playerData).map(key => ({ key, data: playerData[key] }));
+        for (let i = 0; i < ps.length; i++) this.addPlayer(ps[i]);
+        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);
+        (player as any).set_own(player.Id === this._own_id);
+        (player as any).set_host(player.Id === this._hostId, this.isHost)
+    }
+    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 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: number = 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;
+    }
+    /**根据key获取obj数据*/
+    public getObj<T extends { [key: string]: any }>(key: string): T {
+        return super.getValue(key);
+    }
+    /**获取当前所有的obj数据*/
+    public getAllObj<T extends { [key: string]: any }>(): { key: string, data: T }[] {
+        const list: { key: string, data: T }[] = [];
+        const oid = super.getValue('oid') ?? 0;
+        for (let i = 1; i <= oid; i++) {
+            const key = 'obj_' + i;
+            const data = super.getValue(key);
+            if (data) list.push({ key: key, data: data === 0 ? null : data });
+        }
+        return list;
+    }
+    /**不建议经常更改obj */
+    public changeObj<T extends { [key: string]: any }>(key: string, data: T): void {
+        if (!this.isHost) return;
+        const old = super.getValue(key);
+        if (old) {
+            super.setValue(key, data);
+            super.setValueDirty(key, data);
+            (this.evt as any)._emit('r_obj', key, data, old);
+        }
+    }
+    /**删除obj数据*/
+    public deleteObj<T extends { [key: string]: any } | 0>(key: string, data: T | 0 = 0) {
+        if (!this.isHost) return;
+        const old = super.getValue(key);
+        if (old) {
+            super.setValue(key, data);
+            super.setValueDirty(key, data);
+            (this.evt as any)._emit('r_obj', key, null, old);
+        }
+    }
+    /**修改某个键的值*/
+    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;
+        const evt: { evt: 0 | 1, key: string, data: any, old: any }[] = [];
+        Object.keys(data).forEach(key => {
+            evt.push(this.change(key, data[key]));
+        });
+        const e = (this.evt as any)
+        for (let i = 0; i < evt.length; i++) {
+            const ee = evt[i];
+            if (!ee) continue;
+            if (ee.evt === 1) {
+                e._emit('r_obj', ee.key, ee.data, ee.old);
+            } else {
+                e._emit(ee.key, ee.data, ee.old);
+            }
+        }
+    }
+    private change(key: string, data: any): { evt: 0 | 1, key: string, data: any, old: any } | null {
+        const old = super.getValue(key);
+        super.setValue(key, data);
+        if (typeof data !== "object" && data === old) return null;
+        if (key === 'oid') {
+            return null;
+        } else if (key.startsWith('obj_')) {
+            return { evt: 1, key: key, data: data === 0 ? null : data, old: old };
+        } else {
+            return { evt: 0, key: key, data: data, old: 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(): results[] {
+        return this._results;
+    }
+    /**获取自己本局结算*/
+    public get own_results(): results {
+        const rs = this.results;
+        if (!rs) return null;
+        for (let i = 0; i < rs.length; i++) {
+            if (rs[i].Id === this._own_id) {
+                return rs[i];
+            }
+        }
+        return null;
+    }
+    /**获取自己本局获得星星*/
+    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;
+    }
+    /**获取自己本局获得名次*/
+    public get own_results_rank(): 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].Rank;
+            }
+        }
+        return 0;
+    }
+    public sort_results<T extends StartEventNames<GD, "p_">>(sort_name: T): void {
+        if (this._results.length > 1) {
+            this._results.sort((a, b) => {
+                if (a.Rank === 0 && b.Rank === 0) {
+                    if (sort_name) {
+                        return this.getPlayer(b.Id).getValue(sort_name) - this.getPlayer(a.Id).getValue(sort_name);
+                    }
+                    return 0;
+                } else if (a.Rank === 0) {
+                    return 1;
+                } else if (b.Rank === 0) {
+                    return -1;
+                } else {
+                    return a.Rank - b.Rank;
+                }
+            });
+        }
+    }
+    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++) {
+                    elements[i] = transUserDataform(elements[i]);
+                }
+            }
+            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);
+    }
+    private exit(pid: string): void {
+        const player = this.getPlayer(pid);
+        if (!player) return;
+        const location = player.location;
+        const nickName = player.nickName;
+        (player as any).exit();
+        (this.evt as any)._emit('r_exit', pid, location, nickName);
+    }
+    /**向房间所有玩家发消息*/
+    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(): void {
+        if (this._waitSends_mode0.length > 0) {
+            this._send(C2S_MESSAGE, this._waitSends_mode0);
+            this._waitSends_mode0.length = 0;
+        }
+        if (this._waitSends_mode1.length > 0) {
+            this._send(C2S_MESSAGE_TO_HOST, this._waitSends_mode1);
+            this._waitSends_mode1.length = 0;
+        }
+        if (this._waitSends_mode2.length > 0) {
+            this._send(C2S_MESSAGE_TO_OTHER, this._waitSends_mode2);
+            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() {
+        if (this._chat_msg) {
+            this._send(C2S_TALK, this._chat_msg);
+            this._chat_msg = null;
+        }
+        this.doSendMode();
+        const ps = this.all;
+        for (let i = 0; i < ps.length; i++) {
+            const p = ps[i];
+            (p as any).doFinishGame(() => { this._send(C2S_FINISH, p.Id); });
+        }
+    }
+    public doSendDirty() {
+        (this.own as any).doDirtyData((dirty) => {
+            this._send(C2S_UPDATE_USER_GAMEDATA, { userKey: this.own.Id, data: dirty });
+        })
+        if (!this.isHost) return;
+        (this as any).doDirtyData((dirty) => {
+            this._send(C2S_UPDATE_ROOM_GAMEDATA, dirty);
+        })
+        const ais = this.ais;
+        for (let i = 0; i < ais.length; i++) {
+            const ai = ais[i];
+            (ai as any).doDirtyData((dirty) => {
+                this._send(C2S_UPDATE_USER_GAMEDATA, { userKey: ai.Id, data: dirty });
+            })
+        }
+    }
+    //
+    private _onEvt(key: string, data: any): void {
+        (this.evt as any)._emit(key, ...data);
+    }
+    //
+    public dispose(): void {
+        super.dispose();
+        this._send = null;
+        this._players.forEach((v, _) => {
+            v.dispose();
+        });
+        this._players.clear();
+        this._status = false;
+        this.evt.clearAll();
+    }
+}

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

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"be23235f-23df-4ec2-8f2a-41b1b29af740","files":[],"subMetas":{},"userData":{}}

+ 255 - 0
Flowers/assets/ch/net/NetTeam.ts

@@ -0,0 +1,255 @@
+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_READY_CANCEL = 'c2s_ready_cancel';
+const C2S_KICK_OUT = 'c2s_kick_out';
+const C2S_TALK = 'c2s_talk';
+/**网络队伍*/
+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;
+    public get ownId(): string { return this._own_id };
+    constructor(ownId: string, data: any, send: (type: string, data?: any) => void) {
+        this._own_id = ownId;
+        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 {
+        //if (!this.own.ready) 
+        this._send(C2S_READY);
+    }
+    /**取消准备*/
+    public cancelReady(): void {
+        //if (this.own.ready)
+        this._send(C2S_READY_CANCEL);
+    }
+    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_own(player.Id === this._own_id);
+        (player as any).set_host(player.Id === this._hostId);
+        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() {
+        if (this._chat_msg) {
+            this._send(C2S_TALK, 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
Flowers/assets/ch/net/NetTeam.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"dedb90c0-677b-4983-a3b1-b1361ad4cda3","files":[],"subMetas":{},"userData":{}}

+ 114 - 0
Flowers/assets/ch/net/TTWsClient.ts

@@ -0,0 +1,114 @@
+
+/**tt scoket 客户端*/
+export class TTWsClient {
+    public onConnected(): void { };
+    public onError(err: string): void { };
+    public onClosing(): void { };
+    public onClosed(code: number, reason: string): void { };
+    public onMessage(msg: string | ArrayBuffer): void { };
+    //
+    private _ws: any | 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();
+    }
+    /**
+     * 获取当前连接状态
+     * @returns 是否处于活动状态
+     */
+    get isActive(): boolean {
+        return this._ws != null;
+    }
+    /**
+     *是否正在关闭
+     */
+    private get isClosing(): boolean {
+        return this._closing;
+    }
+    /**是否已经关闭 */
+    private get isClosed(): boolean {
+        return this._ws === null;
+    }
+    /**手动连接*/
+    connect() {
+        if (this.isActive) return false;
+        const url = this._url;
+        if (!url) return false;
+        try {
+            this.close();
+            //eslint-disable-next-line @typescript-eslint/ban-ts-comment
+            //@ts-ignore
+            //eslint-disable-next-line @typescript-eslint/no-unsafe-call
+            const ws = tt.connectSocket({
+                url: url,
+                success: (res) => {
+                    chsdk.log.info("WebSocket创建成功", res);
+                },
+                fail: (res) => {
+                    chsdk.log.info("WebSocket创建失败", res);
+                },
+            });
+            ws.onOpen(() => {
+                chsdk.log.info("WebSocket 已连接");
+                this.onConnected();
+            });
+            ws.onClose((res) => {
+                this._ws = null;
+                this.onClosed(res?.code, res?.reason);
+                chsdk.log.info("WebSocket 已断开");
+            });
+            ws.onError((error) => {
+                chsdk.log.info("WebSocket 发生错误:", error);
+                this.onError(error);
+            });
+            ws.onMessage((message) => {
+                chsdk.log.info("socket message:", message);
+                this.onMessage(message.data);
+            });
+            this._ws = ws;
+        } catch (error) {
+            this.onError(error instanceof Error ? error.message : 'Unknown error');
+        }
+        return true;
+    }
+    private _closing: boolean = false;
+    /**
+     * 主动关闭WebSocket连接
+     */
+    close(code?: number, reason?: string): void {
+        if (this.isClosed || this.isClosing) return;
+        this._closing = true;
+        this.onClosing();
+        this._ws.close({
+            code: code,
+            reason: reason,
+            success: (res) => {
+                // 关闭成功的回调
+                chsdk.log.info("close success", res);
+            },
+            fail: (res) => {
+                // 关闭失败的回调
+                chsdk.log.info("close fail", res);
+            },
+        });
+    }
+    /**
+    * 发送数据
+    * @param data 指定格式数据
+    */
+    send(data: string | ArrayBuffer): void {
+        this._ws.send({
+            data: data,
+            success: (ret) => { chsdk.log.info('成功', ret) },
+            fail: (ret) => { chsdk.log.info('失败', ret) },
+            complete: (ret) => { chsdk.log.info('完成', ret) },
+        })
+    }
+}
+

+ 1 - 0
Flowers/assets/ch/net/TTWsClient.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"409f1698-0486-46ca-8a3d-b279fdde18e7","files":[],"subMetas":{},"userData":{}}

+ 109 - 0
Flowers/assets/ch/net/WXWsClient.ts

@@ -0,0 +1,109 @@
+
+/**Wxweb scoket 客户端*/
+export class WXWsClient {
+    public onConnected(): void { };
+    public onError(err: string): void { };
+    public onClosing(): void { };
+    public onClosed(code: number, reason: string): void { };
+    public onMessage(msg: string | ArrayBuffer): void { };
+    //
+    private _ws: any | 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();
+    }
+    /**
+     * 获取当前连接状态
+     * @returns 是否处于活动状态
+     */
+    get isActive(): boolean {
+        return this._ws != null;
+    }
+    /**
+     *是否正在关闭
+     */
+    private get isClosing(): boolean {
+        return this._closing;
+    }
+    /**是否已经关闭 */
+    private get isClosed(): boolean {
+        return this._ws === null;
+    }
+    /**手动连接*/
+    connect() {
+        if (this.isActive) return false;
+        const url = this._url;
+        if (!url) return false;
+        try {
+            this.close();
+            //eslint-disable-next-line @typescript-eslint/ban-ts-comment
+            //@ts-ignore
+            //eslint-disable-next-line @typescript-eslint/no-unsafe-call
+            const ws = wx.connectSocket({
+                url: url,
+                header: {
+                    'content-type': 'application/json'
+                }
+            })
+            ws.onClose((res) => {
+                this._ws = null;
+                this.onClosed(res.code, res.reason);
+            });
+            ws.onError((res) => {
+                this.onError(res.errMsg);
+            });
+            ws.onMessage((res) => {
+                this.onMessage(res.data);
+            })
+            ws.onOpen((res) => {
+                console.log(res.header);
+                console.log(res.profile);
+                this.onConnected();
+            })
+            this._ws = ws;
+        } catch (error) {
+            this.onError(error instanceof Error ? error.message : 'Unknown error');
+        }
+        return true;
+    }
+    private _closing: boolean = false;
+    /**
+     * 主动关闭WebSocket连接
+     */
+    close(code?: number, reason?: string): void {
+        if (this.isClosed || this.isClosing) return;
+        this._closing = true;
+        this.onClosing();
+        this._ws.close({
+            code: code,
+            reason: reason,
+            success: (res) => {
+                // 关闭成功的回调
+                chsdk.log.log("close success", res);
+            },
+            fail: (res) => {
+                // 关闭失败的回调
+                chsdk.log.log("close fail", res);
+            },
+        });
+    }
+    /**
+    * 发送数据
+    * @param data 指定格式数据
+    */
+    send(data: string | ArrayBuffer): void {
+        this._ws.send({
+            data: data,
+            success: (ret) => { console.log('成功', ret) },
+            fail: (ret) => { console.log('失败', ret) },
+            complete: (ret) => { console.log('完成', ret) },
+        })
+    }
+}
+

+ 1 - 0
Flowers/assets/ch/net/WXWsClient.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"cf87de3a-3a1f-45e3-aa55-8cb737253c16","files":[],"subMetas":{},"userData":{}}

+ 105 - 0
Flowers/assets/ch/net/WsClient.ts

@@ -0,0 +1,105 @@
+
+/**web scoket 客户端*/
+export class WsClient {
+    public onConnected(evt: any): void { };
+    public onError(err: any): void { };
+    public onClosing(): void { };
+    public onClosed(code: number, reason: string): void { };
+    public onMessage(msg: string | ArrayBuffer): 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.data);
+    }
+    /** 错误处理回调 */
+    private _onError(err: any) {
+        this.onError(err);
+    }
+    /**连接关闭时的回调 */
+    private _onClosed(event: CloseEvent) {
+        this.onClosed(event?.code, event?.reason);
+    }
+    /**
+     * 获取当前连接状态
+     * @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;
+        if (this._ws?.readyState === WebSocket.OPEN) {
+            try {
+                this.onClosing();
+                this._ws.close(code, reason);
+            } catch (e) {
+                this.onError(e instanceof Error ? e : new Error('Close failed'));
+            }
+        }
+    }
+    /**
+     * 发送数据
+     * @param data 指定格式数据
+     */
+    send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
+        this._ws.send(data);
+    }
+}
+

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

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "454c6766-6024-4725-85e7-3633e92cde70",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

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

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "6e7a6c1c-a45d-4e10-80ea-98f4d3ede7a7",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

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

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

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

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "1dae93a8-c33f-4a7a-94f2-80c65d0c3ae3",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
Flowers/assets/ch/net/modules/msgpack.min.js


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

@@ -0,0 +1,17 @@
+{
+  "ver": "4.0.24",
+  "importer": "javascript",
+  "imported": true,
+  "uuid": "a7ab22e2-afa2-4b2b-acb5-06dfaf0421ad",
+  "files": [
+    ".js"
+  ],
+  "subMetas": {},
+  "userData": {
+    "loadPluginInEditor": false,
+    "loadPluginInWeb": true,
+    "loadPluginInNative": true,
+    "loadPluginInMiniGame": true,
+    "isPlugin": true
+  }
+}

+ 723 - 0
Flowers/assets/ch/net/net.ts

@@ -0,0 +1,723 @@
+import { Level, Ranks, SeasonInfo, transUserDataform, transUserExtraDataform, UserRank } from "./NetBase";
+import { NetRoom } from "./NetRoom";
+import { NetTeam } from "./NetTeam";
+import { TTWsClient } from "./TTWsClient";
+import { WsClient } from "./WsClient";
+import { WXWsClient } from "./WXWsClient";
+/**自定义同步游戏变量(只能以r_或p_开头,按帧率同步,重新进入会收到最新的数据)
+ * 只有第一个参数有效,可加上第一个参数跟第一个参数同类型,用于接收改变前的数据
+ * t_开头为队伍事件
+ * r_开头 主机游戏数据 主机权限
+ * revt_ 开头房间事件
+ * p_ 开头玩家数据 玩家权限和主机ai权限
+ * online 玩家在线下线状态
+ * 尽量拆分数据类型,不要设置过于复杂的数据类型*/
+type protocol = { extra?: Record<string, any> } & chsdk.OmitIndex<chsdk.EventsMap>
+export interface game_protocol extends protocol {
+    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_exit(id: string, location: number, nickName: string): void;//房间某个玩家离开
+    r_state(state: number, old_state?: number): void;//房间游戏状态
+    //
+    p_obj(key: string, data: any, old: any): void;
+    p_state(state: number, old_state?: number): void;//玩家游戏状态
+    p_extra(): void;//扩展数据改变
+    exit(): void;//玩家离开房间
+    finish(rank: number): void;//玩家完成游戏获得名次
+    online(online: boolean): void;//玩家断线重连消息
+    extra: Record<string, any>;
+}
+/**网络状态*/
+interface ws_event_protocol {
+    Connecting(): void;//连接中
+    Reconnecting(reconnectCount: number): void;//重新连接中
+    Connected(): void;//进入大厅连接成功
+    Reconnected(): void;//重连成功
+    Error(err: any | string): void;//错误消息
+    Closing(): void;//关闭连接中
+    Closed(event: string): void;//连接已关闭
+    NextSeason(): void;//进入下一个赛季
+}
+interface options {
+    /**(可选)query连接token等数据*/query?: string,
+    /**(可选) CA 证书字符串,用于 SSL 连接的验证*/ca?: string,
+    /**(可选) 指示是否启用重连机制,默认为 `true`*/ reconnection?: boolean,
+    /**(可选) 在连接失败后重连的最大次数,默认值为 `10`*/ reconnectCount?: number,
+    /**(可选) 每次重连之间的间隔时间,以毫秒为单位,默认值为 `2000`*/  reconnectInterval?: number,
+    /**(可选) 心跳断线时间,以毫秒为单位,默认值为 `30000`*/  timeoutInterval?: number,
+    /**(可选) 游戏事件同步间隔,默认值为 `66` (0.06秒 1秒钟15次)*/  gameEvtInterval?: number,
+    /**(可选) 游戏变量同步间隔,默认值为 `200` (0.2秒 1秒钟5次)*/   gameDataInterval?: number,
+    /**(可选) 是否用json序列化否则用messagepck 默认为 `false`*/    json?: boolean
+}
+function isArrayBuffer(it: unknown): it is ArrayBuffer {
+    try {
+        return it instanceof ArrayBuffer
+    } catch (_) {
+        return false
+    }
+}
+type JsonPrimitive = string | number | boolean | null;
+type Serializable<T> = T extends JsonPrimitive ? T : T extends Date | Function | Symbol | undefined ? never :
+    T extends Array<infer U> ? SerializableArray<U> : T extends object ? SerializableObject<T> : never;
+type SerializableArray<T> = Array<Serializable<T>>;
+type SerializableObject<T extends object> = { [K in keyof T]: Serializable<T[K]>; };
+//
+const HEARTBEAT_INTERVAL = 1000; //每秒发送一次 Ping  
+const HEARTBEAT_TIMEOUT = 80000; //80 秒的断线超时
+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_EXIT_ROOM = 's2c_exit_room';
+const S2C_FINISH = 's2c_finish';
+const S2C_READY = 's2c_ready';
+const S2C_NOT_READY = 's2c_not_ready';
+const S2C_RECEIVE_REWARD = 's2c_receive_reward';
+const S2C_SEASON = 's2c_season';
+const S2C_SET_USER_DATA_EXTRA = 's2c_set_user_data_extra';
+const S2C_RANK_DATA = 's2c_rank_data';
+//
+const C2S_MESSAGE = 'c2s_message';
+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_BACKED_RETURN = 'c2s_backed_return';
+const C2S_BACKED = 'c2s_to_backed';
+const C2S_RECEIVE_REWARD = 'c2s_receive_reward';
+const C2S_SEASON = 'c2s_season';
+const C2S_SET_USER_DATA_EXTRA = 'c2s_set_user_data_extra';
+const C2S_RANK_DATA = 'c2s_rank_data';
+//
+const Ver: string = '0.6.2';
+/**客户端*/
+export class ch_net<GD extends game_protocol = game_protocol> {
+    /**连接状态事件*/
+    public state = chsdk.get_new_event<ws_event_protocol>();
+    private _ws: WsClient | TTWsClient | WXWsClient | 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 season_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 | ArrayBuffer> = [];//| ArrayBufferLike | Blob | ArrayBufferView
+    private _new_resolve<T>(id: string): Promise<T> { return new Promise(resolve => this._callbacks[id] = resolve); }
+    private _lv: Level = { LowerRank: 0, Rank: 0, Star: 0 };
+    private _userKey: string;
+    private _userDataExtra: GD['extra'] = null;
+    /**玩家扩展数据 */
+    public get userDataExtra(): GD['extra'] { return this._userDataExtra; };
+    /**获取玩家扩展数据某个字段*/
+    public getUserDataExtraField<K extends keyof GD['extra']>(key: K): GD['extra'][K] { return this._userDataExtra?.[key]; }
+    /**自己的段位信息
+    * @returns {Object}段位信息对象
+    */
+    public get ownLevel(): Level { return this._lv };
+    private _seasonInfo: any;
+    /** 
+     * 赛季信息 包含当前赛季的基本信息以及排名历史。  
+     * @returns {SeasonInfo} 赛季信息对象
+     */
+    public get seasonInfo(): SeasonInfo { return this._seasonInfo };
+    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 - 可选的配置选项对象,支持以下属性:
+     * @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?: options) {
+        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);
+        //if (chsdk.get_pf() === chsdk.pf.wx) this._ws = new WXWsClient(this._url, this._ca);
+        //if (chsdk.get_pf() === chsdk.pf.tt) this._ws = new TTWsClient(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 SimulatedDisconnection(): void {
+        this._ws.close();
+    }
+    /**主动断开连接*/
+    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): string | ArrayBuffer {
+        return this.json ? JSON.stringify(msg) : msgpack.encode(msg).buffer;
+    }
+    private decode(medata: any): any {
+        return this.json ? JSON.parse(medata) : msgpack.decode(new Uint8Array(medata));
+    }
+    private send_data(type: string, data?: any): void {
+        //console.log(type, data);
+        const d = this.encode(data ? { type: type, data: data } : { type: type });
+        //console.log(d);
+        this._send(d);
+    }
+    /**开始连接,返回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 {
+        this._userKey = null;
+        this.state.emit(this.state.key.Reconnecting, this._currentReconnectCount);
+        this._ws.connect();
+    }
+    /**玩家创建队伍,返回null为成功,否则为错误提示*/
+    public async creat_team(player_count: number = 4): Promise<string | null> {
+        if (!this._ws) return 'No network connection';
+        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 network connection';
+        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> {
+        try {
+            if (!this._ws) return 'No network connection';
+            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');
+        } catch (error) {
+            return 'Unexpected error occurred';
+        }
+    }
+    /**主机结束游戏关闭房间,成功后收到房间关闭事件*/
+    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> {
+        try {
+            if (!this._ws) return 'No network connection';
+            if (!this.inTeam) return 'Not in team';
+            if (!this.inRoom) return 'Not in room';
+            this.send_data(C2S_ROOM_EXIT);
+            return this._new_resolve('exit_room');
+        } catch (error) {
+            return 'Unexpected error occurred';
+        }
+    }
+    private _cache_rank: Ranks<GD['extra']> | null = null;
+    private _cache_rank_time: number = 0;
+    /**获取排行榜行信息 如果是string,就是错误信息,否则为排行榜信息*/
+    public async getRankData(): Promise<string | Ranks<GD['extra']>> {
+        if (chsdk.date.now() - this._cache_rank_time >= 30000) {
+            this._cache_rank = null;
+        }
+        if (this._cache_rank) return this._cache_rank;
+        if (!this._ws) return 'No network connection';
+        this.send_data(C2S_RANK_DATA);
+        return this._new_resolve('get_rank_data');
+    }
+    /**强制转化成用护排行类型*/
+    public asUserRankType<T extends UserRank<GD['extra']> = UserRank<GD['extra']>>(data: any): T {
+        return data as T;
+    }
+    /**领取赛季奖励 ,返回null为成功,否则为错误提示*/
+    public async receive_reward(season: number): Promise<string | null> {
+        if (!this._ws) return 'No network connection';
+        this.send_data(C2S_RECEIVE_REWARD, season);
+        return this._new_resolve('receive_reward');
+    }
+    /**主动刷新赛季信息,返回null为成功,否则为错误提示*/
+    public async update_season(): Promise<string | null> {
+        if (!this._ws) return 'No network connection';
+        this.send_data(C2S_SEASON);
+        return this._new_resolve('update_season');
+    }
+    /**更新设置玩家整个扩展数据,返回null为成功,否则为错误提示*/
+    public async setUserExtra(data: GD['extra'] & SerializableObject<GD['extra']>): Promise<string | null> {
+        if (!this._ws) return 'No network connection';
+        this._userDataExtra = data;
+        this.send_data(C2S_SET_USER_DATA_EXTRA, JSON.stringify(data));
+        if (this.inTeam) (this.team.own as any).set_userExtra(this._userDataExtra);
+        if (this.inRoom) (this.room.own as any).set_userExtra(this._userDataExtra);
+        return this._new_resolve('set_user_extra');
+    }
+
+    /**更新设置玩家扩展数据某个字段的值  */
+    public async setUserExtraField<K extends keyof GD['extra']>(key: K, value: GD['extra'][K] & SerializableObject<GD['extra'][K]>): Promise<string | null> {
+        this._userDataExtra ??= {};
+        this._userDataExtra[key] = value;
+        return this.setUserExtra(this._userDataExtra);
+    }
+    //
+    private _send(data: string | ArrayBuffer) {
+        if (!this._ws) return;
+        if (this._ws.isActive) {
+            this._ws.send(data);
+        } else {
+            this._waitSends.push(data);
+        }
+    }
+    //
+    private _clear_team(): void {
+        this._team?.dispose();
+        this._team = null;
+    }
+    private _clear_room(): void {
+        this._room?.dispose();
+        this._room = null;
+    }
+    private _temp_team: any | null;
+    private _team_info(data?: any | null) {
+        this._clear_team();
+        if (!this._userKey) {
+            this._temp_team = data;
+        } else if (this._temp_team || data) {
+            data ??= this._temp_team;
+            this._team = new NetTeam<GD>(this._userKey, data, this.send_data.bind(this));
+            if (!this._do_resolve('creat_team', null)) this._do_resolve('join_team', null);
+            this._temp_team = null;
+        }
+    }
+    private _temp_match_data: any | null;
+    private _match_success(data?: any | null): void {
+        this._clear_room();
+        if (!this._userKey) {
+            this._temp_match_data = data;
+        } else if (this._temp_match_data || data) {
+            data ??= this._temp_match_data;
+            const roomData = data.roomData;
+            const pData = data.playerData;
+            this._room = new NetRoom<GD>(this._userKey, roomData, pData, this.send_data.bind(this));
+            if (this._team) (this._team as any).match_success();
+            this._temp_match_data = null;
+        }
+    }
+    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: string | ArrayBuffer): void {
+        this.resetHeartbeat();
+        try {
+            const data = this.decode(msg);
+            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.level;
+                    this._userKey = data.data.userKey;
+                    this._seasonInfo = data.data.seasonInfo;
+                    this._userDataExtra = transUserExtraDataform(data.data.userDataExtra);
+                    this._team_info();
+                    this._match_success();
+                    //
+                    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:
+                    this._team_info(data.data);
+                    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:
+                    if (this.inTeam) (this._team as any).changeHost(data.data);
+                    break;
+                case S2C_ROOM_CHANGE_HOST:
+                    if (this.inRoom) (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._match_success(data.data);
+                    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);
+                    const list = this.room.all;
+                    for (let i = 0; i < list.length; i++) {
+                        const rp = list[i];
+                        if (rp.isAI) continue;
+                        if (rp.isOwn) {
+                            this._lv.LowerRank = rp.level.LowerRank;
+                            this._lv.Rank = rp.level.Rank;
+                            this._lv.Star = rp.level.Star;
+                        }
+                        if (this.inTeam) {
+                            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_EXIT_ROOM:
+                    (this._room as any).exit(data.data);
+                    break;
+                case S2C_RECEIVE_REWARD:
+                    this._do_resolve('receive_reward', null);
+                    break;
+                case S2C_SEASON:
+                    this._seasonInfo = data.data;
+                    this._do_resolve('update_season', null);
+                    this.state.emit('NextSeason');
+                    break;
+                case S2C_SET_USER_DATA_EXTRA:
+                    this._do_resolve('set_user_extra', null);
+                    break;
+                case S2C_RANK_DATA:
+                    this._cache_rank_time = chsdk.date.now();
+                    const r = data.data;
+                    const rr = r.ownRank;
+                    const ownRank: UserRank<GD['extra']> = rr;
+                    ownRank.UserData = transUserDataform(rr.UserData);
+                    ownRank.UserDataExtra = transUserExtraDataform(rr.UserDataExtra);
+                    const elementsRank: UserRank<GD['extra']>[] = [];
+                    for (let i = 0; i < r.elementsRank.length; i++) {
+                        const rr = r.elementsRank[i];
+                        rr.UserData = transUserDataform(rr.UserData);
+                        rr.UserDataExtra = transUserExtraDataform(rr.UserDataExtra);
+                        elementsRank.push(rr);
+                    }
+                    this._cache_rank = { list: elementsRank, own: ownRank };
+                    this._do_resolve('get_rank_data', this._cache_rank);
+                    break;
+                case C2S_MESSAGE:
+                    if (Array.isArray(data.data)) {
+                        if (this.inRoom) {
+                            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(() => {
+            chsdk.log.warn("Closing connection whit no replay logoin!");
+            this._ws.close();
+        }, 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.Closing);
+    }
+    private _onClosed(code: number, reason: string): void {
+        code ??= 0;
+        reason ??= '';
+        chsdk.log.log("WebSocket connection closed", code, reason);
+        this.stopHeartbeat();
+        if (!this.reconnection || (reason && code > 4000)) {
+            this.state.emit(this.state.key.Closed, reason);
+            this._do_resolve('connect', reason);
+            chsdk.log.log("WebSocket connection closed", reason);
+            return;
+        }
+        if (this._currentReconnectCount < this.reconnectCount) {
+            this._currentReconnectCount += 1;
+            chsdk.log.log(`Reconnecting... Attempt ${this._currentReconnectCount}`);
+            setTimeout(() => { this.do_reconnect() }, this.reconnectInterval)
+        } else {
+            this._clear_team();
+            this._clear_room();
+            chsdk.log.error("Max reconnect attempts reached. Giving up.");
+            this.state.emit(this.state.key.Closed, reason);
+            this._do_resolve('connect', reason);
+        }
+    }
+    private startHeartbeat() {
+        this.heartbeatInterval = setInterval(() => {
+            this.sendHeartbeat();
+        }, HEARTBEAT_INTERVAL);
+        this.dirtyDataCheckInterval = setInterval(() => {
+            this.dirtyDataCheck();
+        }, this.gameDataInterval);
+        this.evtCheckInterval = setInterval(() => {
+            this.evtCheck();
+        }, this.gameEvtInterval);
+        if (this.season_timeout) clearTimeout(this.season_timeout);
+        this.season_timeout = setTimeout(() => {
+            this.update_season();
+        }, this.seasonInfo.SeasonEndTime - chsdk.date.now() + Math.floor(Math.random() * 2000));
+    }
+    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);
+        if (this.season_timeout) clearTimeout(this.season_timeout);
+    }
+    private evtCheck() {
+        if (this.inRoom) {
+            (this.room as any).doSendChat();
+        } else if (this.inTeam) {
+            (this.team as any).doSendChat();
+        }
+    }
+    private dirtyDataCheck() {
+        if (!this.inRoom) return;
+        this.room.doSendDirty();
+    }
+    /**
+     * 分享队伍信息,邀请进队
+     * @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
Flowers/assets/ch/net/net.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"c337540d-025b-49a0-8359-a36c22ebf236","files":[],"subMetas":{},"userData":{}}

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

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "b2b776da-1e87-4d83-8a5b-f0d602602fb4",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 241 - 0
Flowers/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
Flowers/assets/ch/pvp/ch_pvp.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"ee924f9b-e0e1-4454-b62d-1bc8e9662dfb","files":[],"subMetas":{},"userData":{}}

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

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "eecdcdcc-74f7-4167-83af-96831a005042",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 22 - 0
Flowers/assets/ch/start/ch_net_help.ts

@@ -0,0 +1,22 @@
+import { ch } from "../ch";
+import { ch_net, game_protocol } from "../net/net";
+export default class NetMgr<gd extends game_protocol> {
+    private _net: ch_net<gd> = null!;
+    public get net(): Readonly<ch_net<gd>> {
+        if (!this._net) throw new Error("Network not initialized");
+        return this._net;
+    }
+    constructor() {
+        this._net = ch.get_new_net<gd>();
+    }
+    public init(json: boolean = false) {
+        this._net.init(ch.wsurl,
+            {
+                query: `token=${chsdk.getToken()}`,
+                json: json,
+            });
+    }
+    public connect(): Promise<string | null> {
+        return this._net.connect();
+    }
+}

+ 1 - 0
Flowers/assets/ch/start/ch_net_help.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"a51de21e-2c9b-420e-a12b-8d1a663f32db","files":[],"subMetas":{},"userData":{}}

+ 22 - 0
Flowers/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
Flowers/assets/ch/start/ch_sdk_comp.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "8aeee3c1-44ef-4120-b113-31b31cbb0a64",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 214 - 0
Flowers/assets/ch/start/ch_start.ts

@@ -0,0 +1,214 @@
+import { _decorator, CCInteger, CCString, 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('BannerAdConf')
+class BannerAdConf {
+    @property({ displayName: "广告位id" })
+    adUnitId: string = '';
+    @property({ type: CCInteger, displayName: "刷新的间隔(S)", visible: function () { return this.adUnitId; } })
+    adIntervals: number = 120;
+    @property({ type: CCInteger, displayName: "左上角横坐标", visible: function () { return this.adUnitId; } })
+    left: number = 10;
+    @property({ type: CCInteger, displayName: "左上角纵坐标", visible: function () { return this.adUnitId; } })
+    top: number = 80;
+    @property({ type: CCInteger, displayName: "宽度", visible: function () { return this.adUnitId; } })
+    width: number = 500;
+    @property({ type: CCInteger, displayName: "高度", visible: function () { return this.adUnitId; } })
+    height: number = 60;
+}
+@ccclass('PfConf')
+class PfConf {
+    @property({ displayName: "奖励广告位id" })
+    r_adUnitId: string = '';
+    @property({ displayName: "插屏广告位id" })
+    i_adUnitId: string = '';
+    @property({ type: [BannerAdConf], displayName: "Banner广告位" })
+    banners: BannerAdConf[] = [];
+    @property({ type: [CCString], displayName: "订阅id" })
+    subscriptionIds: string[] = [];
+}
+@ccclass('PackConf')
+class PackConf {
+    //
+    @property({ displayName: '游戏名', group: { name: '基本信息', id: 'base', displayOrder: 0 } })
+    gname: string = '';
+    @property({ displayName: '游戏id', group: { name: '基本信息', id: 'base', displayOrder: 0 } })
+    gid: string = '';
+    @property({ type: Enum(chsdk.reportType), displayName: '上报类型', group: { name: '基础配置', id: 'sdk', displayOrder: 0 }, tooltip: "off 不上报 \n ch 使用ch服务器(需要在ch后台配置事件) \n platform 使用微信/抖音平台(需要在平台后台设置事件)\nch__platflorm所有平台自定义事件都上报" })
+    report: chsdk.reportType = chsdk.reportType.off;
+    @property({ type: Enum(chsdk.loglevel), displayName: '日志等级', group: { name: '基础配置', id: 'sdk', displayOrder: 0 } })
+    log: chsdk.loglevel = chsdk.loglevel.ALL;
+    //
+    @property({ type: PfConf, displayName: '抖音相关配置', group: { name: '平台配置', id: 'sdk', displayOrder: 0 } })
+    tt_conf: PfConf = new PfConf();
+    @property({ type: PfConf, displayName: '微信相关配置', group: { name: '平台配置', id: 'sdk', displayOrder: 0 } })
+    wx_conf: PfConf = new PfConf();
+    //
+    @property({ displayName: '是否单机', group: { name: '网络配置', id: 'web', displayOrder: 1 } })
+    is_local: boolean = false;
+    @property({ type: Enum(chsdk.serverType), displayName: '服务器内置地址类型', visible: function () { return !this.is_local }, tooltip: "local 局域网 \n dev 测试服 \n online 正式服" })
+    server: chsdk.serverType = chsdk.serverType.dev;
+    @property({ displayName: '配置本地测试服务器地址(,隔开)', tooltip: ",隔开,前面的为sdk基础地址(带上V1),后面的为sdk上报地址\n为空使用sdk内置地址没", visible: function () { return !this.is_local && this.server == chsdk.serverType.test } })
+    serverIP: string = 'http://192.168.1.120:8787/v1';
+    @property({ displayName: '配置测试服务器地址(,隔开)', tooltip: ",隔开,前面的为sdk基础地址(带上V1),后面的为sdk上报地址\n为空使用sdk内置地址", visible: function () { return !this.is_local && this.server == chsdk.serverType.dev } })
+    serverIP1: string = '';
+    @property({ displayName: '配置正试服务器地址(,隔开)', tooltip: ",隔开,前面的为sdk基础地址(带上V1),后面的为sdk上报地址\n为空使用sdk内置地址", visible: function () { return !this.is_local && this.server == chsdk.serverType.online } })
+    serverIP2: string = '';
+}
+@ccclass('ch_start')
+export class ch_start extends Component {
+    @property({ type: [PackConf], visible: false })
+    private _confs: PackConf[] = [];
+    @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; }
+    @property({ displayName: '包配置', group: { name: '选包', id: 'pack', displayOrder: 0 } })
+    get conf() { return this._confs[this._pid] ?? new PackConf(); }
+    set conf(val) { this._confs[this._pid] = val; }
+    //联机游戏模块
+    @property({ visible: false })
+    private _use_ch_net: boolean = false;
+    @property({ displayName: '是否使用ch.net模块', tooltip: "联机游戏基于chsdk的登录,如果sdk选单机将无法使用,不同的包共享此配置", visible: function () { return !this.conf.is_local }, group: { name: 'ch.net模块', id: 'net', displayOrder: 1 } })
+    get use_ch_net() { return this._use_ch_net; }
+    set use_ch_net(val) { this._use_ch_net = val; }
+    @property({ displayName: '本地webscoket完整地址', tooltip: "为空使用配置测试地址转换", visible: function () { return !this.conf.is_local && this._use_ch_net && this.conf.server == chsdk.serverType.test; }, group: { name: 'ch.net模块', id: 'net', displayOrder: 1 } })
+    wsIP: string = '';
+    @property({ displayName: '测试服webscoket完整地址', tooltip: "为空使用sdk内置地址转换", visible: function () { return !this.conf.is_local && this._use_ch_net && this.conf.server == chsdk.serverType.dev; }, group: { name: 'ch.net模块', id: 'net', displayOrder: 1 } })
+    wsIP1: string = '';
+    @property({ displayName: '正式服webscoket完整地址', tooltip: "为空使用sdk内置地址转换", visible: function () { return !this.conf.is_local && this._use_ch_net && this.conf.server == chsdk.serverType.online; }, group: { name: 'ch.net模块', id: 'net', displayOrder: 1 } })
+    wsIP2: string = '';
+    //音频模块
+    @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模块', tooltip: "不同的包共享此配置", group: { name: 'ch.audio模块', id: 'audio', displayOrder: 2 } })
+    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: 2 }, 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: 2 }, 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: 2 }, visible: function () { return this._use_ch_audio && this._ch_audio_type == loadType.remote } })
+    private sound_url: string = '';
+    //-------------------------------------------
+    private _real_server: string = '';
+    private _real_ws: string = '';
+    private _ready: boolean = false;
+    protected onLoad(): void {
+
+    }
+    private ready(): void {
+        if (this.conf.server === chsdk.serverType.test) {
+            this._real_server = this.conf.serverIP;
+            this._real_ws = this.wsIP;
+        } else if (this.conf.server === chsdk.serverType.dev) {
+            this._real_server = this.conf.serverIP1;
+            this._real_ws = this.wsIP1;
+        } else if (this.conf.server === chsdk.serverType.online) {
+            this._real_server = this.conf.serverIP2;
+            this._real_ws = this.wsIP2;
+        }
+        console.log('包Id:' + this.pid,
+            '游戏名:' + this.conf.gname,
+            '游戏Id:' + this.conf.gid,
+            '单机:' + this.conf.is_local,
+            '服务器:' + chsdk.serverType[this.conf.server] + '-- ' + this._real_server + '-- ' + this._real_ws
+        );
+    }
+    private getPfConf(conf: PfConf): chsdk.pf_conf {
+        const c = {
+            adUnitId: conf.r_adUnitId, multiton: false,
+            inster_unitId: conf.i_adUnitId,
+            tmplIds: conf.subscriptionIds.length > 0 ? conf.subscriptionIds : null,
+            bannerAds: []
+        }
+        for (let i = 0; i < conf.banners.length; i++) {
+            const banner = conf.banners[i];
+            c.bannerAds.push({
+                adUnitId: banner.adUnitId,
+                adIntervals: banner.adIntervals,
+                style: {
+                    left: banner.left,
+                    top: banner.top,
+                    width: banner.width,
+                    height: banner.height,
+                }
+            });
+        }
+        return c;
+    }
+    /**初始化*/
+    async init(): Promise<boolean> {
+        if (!this._ready) {
+            this._ready = true;
+            this.ready();
+        }
+        chsdk.setConf(chsdk.pf.tt, this.getPfConf(this.conf.tt_conf));
+        chsdk.setConf(chsdk.pf.wx, this.getPfConf(this.conf.wx_conf));
+        let ret = await chsdk.init_inside(this.conf.gid, this.conf.log, this._real_server ? this._real_server : this.conf.server, this.conf.is_local, this.conf.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 (this.use_ch_net) {
+                if (!this._real_ws) {
+                    const base = chsdk.getUrl('/handle');
+                    if (this.conf.server === chsdk.serverType.test) {
+                        this._real_ws = base;
+                    } else {
+                        const url = base.replace(base.split(':')[0], 'wss');
+                        this._real_ws = url;
+                    }
+                }
+                ch.wsurl = this._real_ws;
+            }
+            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.conf.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
Flowers/assets/ch/start/ch_start.ts.meta

@@ -0,0 +1 @@
+{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"f76017d5-74f5-46b5-baf8-195b19fafb2f","files":[],"subMetas":{},"userData":{}}

+ 9 - 0
Flowers/assets/core.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "7693f00c-eb3b-4577-8e36-900f942e1c79",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
Flowers/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
Flowers/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
Flowers/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": 667,
+      "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": 7,
+    "_fov": 45,
+    "_fovAxis": 0,
+    "_orthoHeight": 667,
+    "_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": 1334
+    },
+    "_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
Flowers/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"
+  }
+}

+ 321 - 0
Flowers/assets/core/ui/ui.ts

@@ -0,0 +1,321 @@
+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
Flowers/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
Flowers/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
Flowers/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": {}
+}

+ 379 - 0
Flowers/assets/core/ui/ui_base.ts

@@ -0,0 +1,379 @@
+import { _decorator, assetManager, Button, Component, EventHandler, EventTouch, find, isValid, Node, Prefab, Toggle, ToggleContainer } from 'cc';
+import ch_audio from '../../ch/audio/audio';
+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 new_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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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
Flowers/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": {}
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.