From ad1ba08bc33b432ead2c30c934670003260cee7a Mon Sep 17 00:00:00 2001 From: keke Date: Tue, 15 Sep 2026 20:06:12 +0800 Subject: [PATCH 1/2] feat: add level gameplay introduction panel and preview tools --- assets/GameplayIntro.meta | 13 + assets/GameplayIntro/README.md | 176 ++ assets/GameplayIntro/README.md.meta | 6 + .../cat.meta => GameplayIntro/Texture.meta} | 2 +- assets/GameplayIntro/Texture/close.png | Bin 0 -> 2784 bytes assets/GameplayIntro/Texture/close.png.meta | 38 + assets/GameplayIntro/Texture/mianba.png | Bin 0 -> 4062 bytes assets/GameplayIntro/Texture/mianba.png.meta | 38 + assets/GameplayIntro/Texture/wenhao.png | Bin 0 -> 6362 bytes assets/GameplayIntro/Texture/wenhao.png.meta | 38 + assets/GameplayIntro/Texture/yuandian.png | Bin 0 -> 1187 bytes .../GameplayIntro/Texture/yuandian.png.meta | 38 + assets/GameplayIntro/Texture/yuandian00.png | Bin 0 -> 1820 bytes .../GameplayIntro/Texture/yuandian00.png.meta | 38 + assets/GameplayIntro/Texture/玩法指南字.png | Bin 0 -> 10516 bytes .../GameplayIntro/Texture/玩法指南字.png.meta | 38 + .../prefab.meta} | 2 +- .../GameplayIntro/prefab/GameplayIntro.prefab | 2362 +++++++++++++++++ .../prefab/GameplayIntro.prefab.meta | 9 + .../{passCheck => GameplayIntro}/script.meta | 2 +- assets/GameplayIntro/script/GameplayIntro.ts | 237 ++ .../script/GameplayIntro.ts.meta | 10 + assets/GameplayIntro/script/GuideCarousel.ts | 275 ++ .../script/GuideCarousel.ts.meta | 10 + assets/Scene/GameScene.fire | 466 +++- assets/Script/SceneManager.ts | 83 +- assets/Script/module/Tool/LevelGameplay.ts | 74 + .../Script/module/Tool/LevelGameplay.ts.meta | 10 + assets/test.meta | 13 + assets/test/README.md | 48 + assets/test/README.md.meta | 6 + .../{pause/texture.meta => test/scene.meta} | 2 +- assets/test/scene/TestLayout.fire | 258 ++ assets/test/scene/TestLayout.fire.meta | 8 + assets/test/script.meta | 13 + assets/test/script/TestLayoutPanel.ts | 219 ++ assets/test/script/TestLayoutPanel.ts.meta | 10 + assets/test/tests.meta | 13 + assets/test/tests/guide-carousel.cjs | 407 +++ assets/test/tests/guide-carousel.cjs.meta | 6 + assets/test/tests/home-prop-panel.cjs | 32 + assets/test/tests/home-prop-panel.cjs.meta | 6 + assets/test/tests/preview.cjs | 94 + assets/test/tests/preview.cjs.meta | 6 + .../wechatgame/project.config.json | 9 +- settings/project.json | 4 +- 46 files changed, 5032 insertions(+), 87 deletions(-) create mode 100644 assets/GameplayIntro.meta create mode 100644 assets/GameplayIntro/README.md create mode 100644 assets/GameplayIntro/README.md.meta rename assets/{gacha_bundle/img/cat.meta => GameplayIntro/Texture.meta} (81%) create mode 100644 assets/GameplayIntro/Texture/close.png create mode 100644 assets/GameplayIntro/Texture/close.png.meta create mode 100644 assets/GameplayIntro/Texture/mianba.png create mode 100644 assets/GameplayIntro/Texture/mianba.png.meta create mode 100644 assets/GameplayIntro/Texture/wenhao.png create mode 100644 assets/GameplayIntro/Texture/wenhao.png.meta create mode 100644 assets/GameplayIntro/Texture/yuandian.png create mode 100644 assets/GameplayIntro/Texture/yuandian.png.meta create mode 100644 assets/GameplayIntro/Texture/yuandian00.png create mode 100644 assets/GameplayIntro/Texture/yuandian00.png.meta create mode 100644 assets/GameplayIntro/Texture/玩法指南字.png create mode 100644 assets/GameplayIntro/Texture/玩法指南字.png.meta rename assets/{libs/dn-sdk-minigame.meta => GameplayIntro/prefab.meta} (81%) create mode 100644 assets/GameplayIntro/prefab/GameplayIntro.prefab create mode 100644 assets/GameplayIntro/prefab/GameplayIntro.prefab.meta rename assets/{passCheck => GameplayIntro}/script.meta (81%) create mode 100644 assets/GameplayIntro/script/GameplayIntro.ts create mode 100644 assets/GameplayIntro/script/GameplayIntro.ts.meta create mode 100644 assets/GameplayIntro/script/GuideCarousel.ts create mode 100644 assets/GameplayIntro/script/GuideCarousel.ts.meta create mode 100644 assets/Script/module/Tool/LevelGameplay.ts create mode 100644 assets/Script/module/Tool/LevelGameplay.ts.meta create mode 100644 assets/test.meta create mode 100644 assets/test/README.md create mode 100644 assets/test/README.md.meta rename assets/{pause/texture.meta => test/scene.meta} (81%) create mode 100644 assets/test/scene/TestLayout.fire create mode 100644 assets/test/scene/TestLayout.fire.meta create mode 100644 assets/test/script.meta create mode 100644 assets/test/script/TestLayoutPanel.ts create mode 100644 assets/test/script/TestLayoutPanel.ts.meta create mode 100644 assets/test/tests.meta create mode 100644 assets/test/tests/guide-carousel.cjs create mode 100644 assets/test/tests/guide-carousel.cjs.meta create mode 100644 assets/test/tests/home-prop-panel.cjs create mode 100644 assets/test/tests/home-prop-panel.cjs.meta create mode 100644 assets/test/tests/preview.cjs create mode 100644 assets/test/tests/preview.cjs.meta diff --git a/assets/GameplayIntro.meta b/assets/GameplayIntro.meta new file mode 100644 index 0000000..0fc1bae --- /dev/null +++ b/assets/GameplayIntro.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.3", + "uuid": "b2ad39e9-35c9-4657-ba4b-621b6b6fb499", + "importer": "folder", + "isBundle": true, + "bundleName": "gameplay_intro", + "priority": 1, + "compressionType": {}, + "optimizeHotUpdate": {}, + "inlineSpriteFrames": {}, + "isRemoteBundle": {}, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/GameplayIntro/README.md b/assets/GameplayIntro/README.md new file mode 100644 index 0000000..07b9856 --- /dev/null +++ b/assets/GameplayIntro/README.md @@ -0,0 +1,176 @@ +# 玩法介绍正式功能包 + +Asset Bundle 名称:`gameplay_intro`。 + +- `prefab/GameplayIntro.prefab`:本关介绍面板固定布局,可在 Creator 中编辑背景、标题、滑动区域和分页器位置。 +- `assets/Script/module/Tool/LevelGameplay.ts`:主脚本目录中的筛选工具,从实际关卡的方块、门、地块及生成队列识别玩法,并按类型去重。主场景仅以类型引用 GameplayIntro,在分包加载完成后用组件名获取实例,避免启动时依赖尚未加载的分包脚本。 +- `script/GameplayIntro.ts`:绑定预制体节点,关闭按钮或外部点击关闭,相邻三页图片加载与释放。 +- `script/GuideCarousel.ts`:鼠标/触摸翻页、缓动回弹、最多三点分页及白点替换动画;单页隐藏分页且不绑定拖动,双页显示两点。 +- `Texture/`:面板背景、玩法指南标题、问号、关闭按钮及分页圆点等 UI 素材;原有中文玩法图片已清理,玩法图片统一加载公共目录。 +- `assets/resources/Window_Prop/`:34 组公共玩法图片,按 `titleN`、`iconN`、`IntroductionN` 加载。daoju26 使用素材 27,daoju27 使用素材 26。 + +游戏入口由 `assets/Script/SceneManager.ts` 控制:进入场景时按实际加载的关卡生成介绍列表,没有特殊道具就隐藏问号。首次点击 `openGameplayIntro` 加载本包的 `prefab/GameplayIntro`,实例挂到 Inspector 绑定的 `tipPanel` 节点;关闭后保留固定布局,释放所有玩法页图片,再次打开复用实例,按解锁关卡倒序排列后定位最左端的最新玩法;三个分页点时高亮左边第 1 点。 + +`GameplayIntro.open(entries)` 接收已经筛选的介绍列表。`NEW_LEVEL.json` 仅提供介绍编号和排列顺序,不再用解锁关卡或玩家进度筛选。开局方块和 `spawnGates[].spawnQueue[].members` 都纳入本关范围,消除后仍可查看;同类型只展示一页。木箱/快递盒/彩色木箱、普通/定次连接等按实际变体匹配,花盆借用的不可见盖板不增加快递盒介绍。 + +场景接线:在 GameScene 的 Top/tip 下放置问号 Sprite + Button,图片使用本包 `wenhao`;Button 点击事件绑定 Canvas 上的 SceneManager.openGameplayIntro。SceneManager 的 Tip Panel 属性绑定场景现有的 tipPanel。tipPanel 应作为位于游戏内容之上的居中容器,不需要添加覆盖整个屏幕的输入拦截组件。 + +预制体直接引用背景、标题、关闭按钮和两种分页圆点;34 种公共玩法图片由脚本按当前页及相邻页动态加载,不能把所有玩法图片拖到预制体中,以免一次加载全部图片。 + +测试工具位于 `assets/test`。当前 `settings/project.json` 的启动场景是 `TestLayout`;不需要测试时,先在 Creator 中切回正式启动场景并保存设置,再删除 `assets/test` 和同级 `assets/test.meta`。当前 HomeScene 没有 HomePropTestPanel 节点。 + + + +## 与已拉取 main 的改动对比 + +核对日期:2026-09-15。最新 `origin/main`(`e992a8f`)已快进合入当前 `layout-optimization` 分支。以下以 `e992a8f` 为基准,包含当前工作区的暂存、未暂存和未跟踪文件;本地 `main`(`122b504`)未移动,不作为本清单的对比基准。此清单是本次核对快照,后续代码变更需同步更新。 + +新增业务代码并非全部放在新包:面板与滑动脚本位于 `gameplay_intro`,测试代码位于 `test`,`LevelGameplay.ts` 位于原公共脚本目录。 + +``` +GameplayIntro/ +├─ assets/ +│ ├─ GameplayIntro/ 【新增资源包:gameplay_intro】 +│ │ ├─ script/ +│ │ │ ├─ GameplayIntro.ts 面板展示、图片加载与释放 +│ │ │ └─ GuideCarousel.ts 滑动翻页、分页指示 +│ │ ├─ prefab/ +│ │ │ └─ GameplayIntro.prefab 玩法介绍面板 +│ │ ├─ Texture/ 玩法介绍美术资源 +│ │ └─ README.md +│ │ +│ ├─ test/ 【新增资源包:test】 +│ │ ├─ script/ +│ │ │ └─ TestLayoutPanel.ts 独立测试面板 +│ │ ├─ scene/ +│ │ │ └─ TestLayout.fire 测试场景 +│ │ ├─ tests/ +│ │ │ ├─ guide-carousel.cjs +│ │ │ ├─ home-prop-panel.cjs 残留测试,依赖的脚本已不存在 +│ │ │ └─ preview.cjs +│ │ └─ README.md +│ │ +│ ├─ Script/ 【原有脚本目录】 +│ │ ├─ module/Tool/ +│ │ │ └─ LevelGameplay.ts 【新增】当前关卡玩法筛选 +│ │ ├─ SceneManager.ts 【修改】入口、加载、暂停与恢复 +│ │ └─ ItemGuide.ts 无实际内容差异 +│ │ +│ ├─ NewMode/script/ +│ │ └─ NewMode.ts 无实际内容差异 +│ │ +│ ├─ Scene/ +│ │ ├─ GameScene.fire 【修改】问号按钮及面板接线 +│ │ └─ HomeScene.fire 无实际内容差异 +│ │ +│ ├─ gacha_bundle/img/cat.meta 【删除】 +│ ├─ libs/dn-sdk-minigame.meta 【删除】 +│ ├─ passCheck/script.meta 【删除】 +│ └─ pause/texture.meta 【删除】 +│ +├─ build-templates/wechatgame/ +│ └─ project.config.json 【修改】上传时忽略 cocos 目录 +│ +└─ settings/ + └─ project.json 【修改】启动场景、记录时间 +``` + +新增资源对应的 `.meta` 文件未逐一展开。**原有脚本实际只改了 `SceneManager.ts`;新增的 `LevelGameplay.ts` 位于公共脚本目录。两个新资源包尚未配置为微信独立分包。** + +`assets/custom/Json/level1961.json.meta` 至 `level1980.json.meta` 共 20 个关卡元数据文件现已包含在 main 基准中,合并时采用 main 的 UUID;未发现资源对旧本地 UUID 的引用。`.codex_tmp/prop-compare.jpg` 和 `.codex_tmp/texture-compare.jpg` 是本地对比图片,不是业务代码。`project.json`、`settings/services.json`、`settings/wechatgame.json` 相对上述基准没有实际文本内容差异。 + +本次合入 main 的 3 个提交包含:GameScene 点击拦截组件、门生方块介绍关卡改为 1943、AB 测试 B 方案第 15 关不返回首页及版本配置更新、战令图片更新。场景合并保留了 main 的拦截组件及本地问号按钮、坐标和面板接线;这些 main 自带改动不计入本地新增功能差异。 + +### 新增正式脚本及方法职责 + +整体调用关系:`SceneManager` 控制入口和暂停恢复 → `LevelGameplay` 筛选本关玩法 → `GameplayIntro` 管理内容与图片 → `GuideCarousel` 处理滑动分页。 + +#### LevelGameplay.ts(公共目录) + +| 方法 | 作用 | +| --- | --- | +| `getLevelGameplay(config, data)` | 输入全部介绍配置和当前关卡数据,扫描方块、门、特殊地块及生成队列,返回本关介绍列表;按名称去重,保留配置顺序 | +| 内部 `add(id)` | 将编号转换为 `daojuN` 存入 Set,重复玩法只记录一次 | + +该工具只读取数据。方块列表使用浅复制后再补充待生成方块,不向原数组追加内容。根据方块类型及附加属性区分普通/定次、木板/快递盒/彩色箱子等变体;花盆接收块借用的不可见盖板不额外计入箱子介绍。它不使用玩家进度或配置中的 `level` 筛选,也不负责显示、加载图片或暂停游戏;最终只返回在介绍配置中存在的条目。 + +#### GameplayIntro.ts(gameplay_intro 包) + +挂在正式预制体根节点,负责内容、开关动画和图片资源。 + +| 方法 | 作用 | +| --- | --- | +| `open(entries)` | 复制列表并按 `level` 倒序排列,绑定固定节点、初始化分页,播放打开动画;空列表直接关闭 | +| `syncPages()` | 只保留当前页和前后相邻页的图片,优先加载当前页 | +| `picture(...)` | 排队加载公共标题、道具或说明图;标题和道具图等比适配,说明图保持预制体设置;丢弃失效页面的加载结果 | +| `nextImage()` | 依次启动图片加载任务,避免集中加载 | +| `releasePage(index)` / `releasePages()` | 清空图片显示引用、归还资源引用、隐藏页面;批量清理时同时清空待加载队列,固定节点保留复用 | +| `close()` / `finishClose()` | 播放关闭动画后释放图片、隐藏面板并发出 `intro-closed`,由场景判断是否恢复游戏 | +| `stopPanelAnimation()` | 停止开关动画并重置状态 | +| `onDisable()` / `onDestroy()` | 隐藏或销毁时清理图片和动画 | +| `previewNavigate(action)` / `getPreviewState()` | 提供测试跳页入口,以及当前页、总页数和当前配置 | +| `showMessage(value)` | 使用预制体的 Status 节点显示提示 | + +#### GuideCarousel.ts(gameplay_intro 包) + +挂在 `IntroViewport`,只负责手势、页面位置和圆点动画。逻辑页循环使用预制体中的三个固定页面节点。 + +| 方法 | 作用 | +| --- | --- | +| `initialize(...)` / `clear()` | 初始化页数、默认页、圆点及输入监听;重复打开前清理旧状态 | +| `goTo(index, animated)` | 限制目标页范围,更新分页高亮,发出 `page-changed` 通知内容加载 | +| `begin(...)` / `move(...)` / `end(...)` | 开始拖动、跟手移动、根据距离和速度决定翻页或回弹;首尾越界有阻力 | +| `x(event)` | 将屏幕坐标转为视口局部坐标,适配面板缩放 | +| `touchStart/Move/End/Cancel` | 将带手指 ID 的触摸事件转给拖动逻辑 | +| `mouseDown/Move/Up/Leave` | 处理鼠标左键拖动与离开视口结束拖动 | +| `createDot()` / `styleDot()` | 创建圆点并设置选中样式 | +| `animateDots()` / `resetDotAnimation()` | 推进或重置圆点替换动画 | +| `render()` / `update(dt)` | 应用页面位移、缩放和透明度,每帧推进吸附、回弹及圆点动画 | +| `onDisable()` | 结束手势和动画,避免重开后残留中间状态 | + +### 原有 SceneManager.ts 的具体改动 + +| 改动位置 | 改动及作用 | +| --- | --- | +| 文件顶部 | 引入 `getLevelGameplay`;以 `import type` 引用 GameplayIntro,不产生启动时的运行时导入 | +| 新增字段 | `tipPanel` 绑定场景容器;`gameplayIntro` 缓存面板控制器;`levelGameplay` 保存本关介绍列表 | +| `isPauseOpen()` | 将介绍面板加载中、显示中计入暂停判断,防止重复打开及原有流程提前恢复游戏 | +| `onLoad()` | 增加 `refreshGameplayInfo()` 调用,进场景时初始化列表和问号显隐 | +| 新增 `refreshGameplayInfo()` | 使用已有 NEW_LEVEL 配置,未就绪则异步加载;按实际关卡筛选并缓存列表,没有介绍时隐藏问号 | +| 新增 `openGameplayIntro()` | 检查场景、暂停、结束、商城和容器状态;暂停游戏并停止炸弹计时;通过原有动态弹窗加载器加载或复用正式面板,再传入列表 | +| 内部 `resume()` 回调 | 关闭或加载失败时,仅在无冰冻、无其他暂停、无商城暂停且游戏未结束时恢复游戏;游戏已开始才恢复炸弹计时 | +| 加载完成回调 | 结束加载标记;失败时尝试恢复;成功时按组件名取得面板,同一实例只绑定一次关闭回调 | +| 原空 `start()` | 删除无内容的方法,没有移除实际业务逻辑 | + +`loadDynamicPopup()` 等通用加载方法沿用原有实现。`MapConroler`、`ItemGuide.ts`、`NewMode.ts` 等其他原有业务脚本没有内容修改。 + +配套非脚本改动:`GameScene.fire` 新增问号按钮、tipPanel 容器及点击/属性绑定;微信构建模板增加上传忽略 `cocos` 目录;`settings/project.json` 将启动场景改为 `TestLayout`(UUID `8e21ec7e-3913-44ec-8f99-53ff52fe81ed`)并更新记录时间。 + +### 新增测试脚本 + +| 脚本 | 作用 | +| --- | --- | +| `assets/test/script/TestLayoutPanel.ts` | 独立测试界面,支持指定本地关卡、全部玩法、重新打开和跳页;直接复用正式面板,不写玩家进度 | +| `assets/test/tests/guide-carousel.cjs` | 使用模拟 Cocos 环境检查筛选、分页、资源回收及场景入口的暂停恢复逻辑 | +| `assets/test/tests/preview.cjs` | 本地预览服务,使用 Creator 编译器及运行中的编辑器引擎/资源展示测试场景 | +| `assets/test/tests/home-prop-panel.cjs` | 残留道具测试;依赖的 HomePropTestPanel.ts 和 HomeScene 节点已不存在,当前不可正常运行 | + +运行方式与移除步骤见 `assets/test/README.md`。本次合并后执行 `node assets/test/tests/guide-carousel.cjs`,全部回归项通过;同时验证了场景引用索引和双方场景改动的保留情况。未进行 Creator 真机视觉及性能验证。 + + +## 可编辑的固定玩法页结构 + +直接打开 `prefab/GameplayIntro.prefab`: + +``` +IntroViewport +└─ Pages + ├─ Page1 + │ ├─ TitleImage + │ ├─ IconImage + │ └─ DescriptionImage + ├─ Page2(相同结构) + └─ Page3(相同结构) +``` + +所有图片节点已经放在面板预制体内,SpriteFrame 留空。最下方 DescriptionImage 的大小由 Sprite 的 Size Mode、Trim 及节点尺寸/缩放控制,代码不覆盖;TitleImage 和 IconImage 仍以预制体 Width/Height 为展示区域等比适配;三页需要保持相同布局。代码仅复用三个页面、改变分页位置并动态加载图片。说明图不做代码缩放,标题和道具图保留等比适配;没有单图比例修正。翻远或关闭面板时清空图片并归还资源引用,不销毁这三个页面;面板整体销毁时由引擎销毁子节点。 diff --git a/assets/GameplayIntro/README.md.meta b/assets/GameplayIntro/README.md.meta new file mode 100644 index 0000000..a1d7667 --- /dev/null +++ b/assets/GameplayIntro/README.md.meta @@ -0,0 +1,6 @@ +{ + "ver": "2.0.2", + "uuid": "7a8b8085-ff6e-436f-8671-a99801b33134", + "importer": "asset", + "subMetas": {} +} \ No newline at end of file diff --git a/assets/gacha_bundle/img/cat.meta b/assets/GameplayIntro/Texture.meta similarity index 81% rename from assets/gacha_bundle/img/cat.meta rename to assets/GameplayIntro/Texture.meta index 95837c3..4f8e596 100644 --- a/assets/gacha_bundle/img/cat.meta +++ b/assets/GameplayIntro/Texture.meta @@ -1,6 +1,6 @@ { "ver": "1.1.3", - "uuid": "aef15769-3ab2-4a0e-8b96-022a6d62696d", + "uuid": "260eaaff-84cb-4758-95c7-be73182532c5", "importer": "folder", "isBundle": false, "bundleName": "", diff --git a/assets/GameplayIntro/Texture/close.png b/assets/GameplayIntro/Texture/close.png new file mode 100644 index 0000000000000000000000000000000000000000..34b6a6ff5284df7ec054b98bd569da7d7cbccb1a GIT binary patch literal 2784 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 b#daZQ1I|9$PRWG*^A$8VwJ~{W?3(x=?}<5@ literal 0 HcmV?d00001 diff --git a/assets/GameplayIntro/Texture/close.png.meta b/assets/GameplayIntro/Texture/close.png.meta new file mode 100644 index 0000000..09f2f23 --- /dev/null +++ b/assets/GameplayIntro/Texture/close.png.meta @@ -0,0 +1,38 @@ +{ + "ver": "2.3.7", + "uuid": "7c6fbbbe-7e71-4e67-bff0-78ed794c6edd", + "importer": "texture", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "premultiplyAlpha": false, + "genMipmaps": false, + "packable": false, + "width": 103, + "height": 112, + "platformSettings": {}, + "subMetas": { + "close": { + "ver": "1.0.6", + "uuid": "b920b9e1-7d5d-4218-bc40-797cb0d9319d", + "importer": "sprite-frame", + "rawTextureUuid": "7c6fbbbe-7e71-4e67-bff0-78ed794c6edd", + "trimType": "custom", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 103, + "height": 112, + "rawWidth": 103, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/GameplayIntro/Texture/mianba.png b/assets/GameplayIntro/Texture/mianba.png new file mode 100644 index 0000000000000000000000000000000000000000..1644c85adb6affdc9a1540b001720965092bb293 GIT binary patch literal 4062 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 literal 0 HcmV?d00001 diff --git a/assets/GameplayIntro/Texture/wenhao.png.meta b/assets/GameplayIntro/Texture/wenhao.png.meta new file mode 100644 index 0000000..1507e0f --- /dev/null +++ b/assets/GameplayIntro/Texture/wenhao.png.meta @@ -0,0 +1,38 @@ +{ + "ver": "2.3.7", + "uuid": "2dbc727b-eb2b-48f4-9e1a-c76fbe480b78", + "importer": "texture", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "premultiplyAlpha": false, + "genMipmaps": false, + "packable": false, + "width": 86, + "height": 90, + "platformSettings": {}, + "subMetas": { + "wenhao": { + "ver": "1.0.6", + "uuid": "d9333256-302a-403b-93bf-e25c42829476", + "importer": "sprite-frame", + "rawTextureUuid": "2dbc727b-eb2b-48f4-9e1a-c76fbe480b78", + "trimType": "custom", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 86, + "height": 90, + "rawWidth": 86, + "rawHeight": 90, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/GameplayIntro/Texture/yuandian.png b/assets/GameplayIntro/Texture/yuandian.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e8762b9941429d28980fbac3c39a1d065c5d68 GIT binary patch literal 1187 zcmaJ>U1%It6rQv+E`gO$1684TnQCZAcjnIQPLdh2X_B2?vzQ6XW@(mEX(ux`yF+$< zoVm$PA|l<;hl-V`)MBZchf2hs8W2jsLO0POmHsF`Sc=&7Aul4N{t#-Z;+-^``rtY+ zbMHNz?>pbQ=VOj%MxG6~JlR4~R5(2(XUWN77~LA?~Uy~D`mab6jgG}~mJPOp7_ zi3Wxqr^f^ZDoz3wjiFf=jn0nbwAo2b)am_wpx2iOfr+pRe6wVEk{_oxcqOtA!YmCo zQt)J)ejqBZWI)1p5#S&LBN~Jdbc>A6Llo=kio$(>hrGaYJPXAL-;T!&&rD?|%||xfuts zS>)MMu11D8u_q|yNC_9I*miTaU8>(?rf6f^E7}g=yP0mVS5Y;?3L+gF5Ji#FmWNeK zLuolq6B5QSbV(e9Jsgzz0U?p(xD+?Q!z-#qh!ar)2M@7@+Q|J}vP!Go)hzjB3HN#EV}R4x2V_4hYc z4^_MqpmmA=y7l^pH*PJP{RQ?YmCW38p8Ko)p1-Ts8JAyT|32Dr`^@@h2|RZB=7FDb ziBqH6w^5^M-I1QY6Dq`B-*Mz@+oA6-3TO45OCWUhG0235i+xSwbS*t!rfUaUFVG8} zAuPN%KS#aVb^a5va(Y{}IbZlWqxL`l+F&(x^}?sWC3gGwA1SSPx0_2l&>d8r-mCVt wpI>WP_-5gc>%T6)eE+U+xM}^P&RTnwI;@UeTbudf{ouEePL9au29H$!0Z?I;tN;K2 literal 0 HcmV?d00001 diff --git a/assets/GameplayIntro/Texture/yuandian.png.meta b/assets/GameplayIntro/Texture/yuandian.png.meta new file mode 100644 index 0000000..6e28239 --- /dev/null +++ b/assets/GameplayIntro/Texture/yuandian.png.meta @@ -0,0 +1,38 @@ +{ + "ver": "2.3.7", + "uuid": "b2388707-a337-4107-85c8-3938cbd8d4c5", + "importer": "texture", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "premultiplyAlpha": false, + "genMipmaps": false, + "packable": false, + "width": 20, + "height": 20, + "platformSettings": {}, + "subMetas": { + "yuandian": { + "ver": "1.0.6", + "uuid": "93a7437b-9792-42c0-b0f5-73724314cbd8", + "importer": "sprite-frame", + "rawTextureUuid": "b2388707-a337-4107-85c8-3938cbd8d4c5", + "trimType": "custom", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 20, + "height": 20, + "rawWidth": 20, + "rawHeight": 20, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/GameplayIntro/Texture/yuandian00.png b/assets/GameplayIntro/Texture/yuandian00.png new file mode 100644 index 0000000000000000000000000000000000000000..84dd2593d84062000110c491e542089513fc9f11 GIT binary patch literal 1820 zcmaJ?dsGrx9H#P>nwg$_wdDk5SOEiw$QZQ-NR}ibmOU&9Mj?YCn1XcMN{dkISZQW- zYRk;rEN#u~wB`2Hwk<1Pwb0sb<)bpp-m7KpU~2ovx^vFVz4v#%@B2OPnGE54{wQZZ zXB-YUDq;>-gslO#*U16<7PFt0Vv8@zi$i0OL{ulyz_>6OvH}Jol#(P^1WRP=R$qr_ z;BX@X72-HFP7n!65hYn-!;tk#HO9u_X3Wy7CDLRV1y;aG3Kbjw;@ky1ppdcg3mF1X zpyt4G#herk9FsC%EKNz4vSj#KGl3a;2oq4ks07d}SE;m+o{jI*g|NNNOu++v5Hy*M z{}NQ3KnQRU4Gd60GDwnwAP6v7WEl;FgMx$U;B zh010L?4t+-P=rd0N>oxfg3HEZB4mX^2C-&?AykkX9LC`AsMK(37!3?!F&Q)lm(C3d zX9Nv!xrlU)5>}xDT-jGHb4adjB9v;(GZ)q<*21!I4Wb14vW65x#}YK8UO!hhbS#V^ zxfCoIimkW*>h*vL8z9?uaC))B;QYfXZ0I%EbRV3Q_7`?(G9tKPV!h>Nz97;w+Wp#A z`$M9_jRhhP-oXHe6?U9zyUs?Zj12!NRYpglCrjV1!Uy0@MsK#STR_5eoyj=aNOd3G zI*-7sp5s#CW%ul6M@LUjT#o8ob5~TrzMJ=d>29@lqKV}Nb;Ku49i1+)Uh$}nTf&|8 z=*F;$ML`tP*5}{7FX2^D%Xisxd|Z}6b>zeQ>wMeCTW}V_=50O>2B9t_cSL>8=o6zK zPAIfDe9P~u&${bev(m}xHT?U!Q3m0{m#^GwD{uwwtv|J7xy!}IE_zo~N=07T^(lLB z24Snd+qj_M!9k+1FI>Pq2pS!$mwX)mOa6%cTQ-2=(#8V&%q@z|M6c@HMs7kaE#Ex#L}^ZZt=$4jit~+Q zk&dT!$NpBDGCY)=;{5Ku!0}YxbK_HC%d?5sb|FxHc;vp}=fuusSz=}QmVy_4X3sl&&OVb6!GqZm6;q)k6FCw z$kUN#YxK#LZ(g_MDpHd@lix6lHq~aIT@)F%Ot#o~G;7%EEq+_y3h(4L6vXXXmI+6d z{k=xSq_=k74)JOFBx-MY+`8U%+Zk(ib!hRvLzUB{+3xS}KFL3DmG88&jqmt&XZoY~ zm@B`ZOx=Oz&N`%}9YrjB`&4OLw(e$Dh@rSAr;=dFj` pN`_Tzy&^rYdD-<@8s9VRA}*3>!1vaMM%(^Z5xn`_(%A`V{{VzW(cu69 literal 0 HcmV?d00001 diff --git a/assets/GameplayIntro/Texture/yuandian00.png.meta b/assets/GameplayIntro/Texture/yuandian00.png.meta new file mode 100644 index 0000000..855ed1b --- /dev/null +++ b/assets/GameplayIntro/Texture/yuandian00.png.meta @@ -0,0 +1,38 @@ +{ + "ver": "2.3.7", + "uuid": "94a173e9-d339-4583-8962-0a18e0df52d5", + "importer": "texture", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "premultiplyAlpha": false, + "genMipmaps": false, + "packable": false, + "width": 38, + "height": 38, + "platformSettings": {}, + "subMetas": { + "yuandian00": { + "ver": "1.0.6", + "uuid": "af06b902-6722-4e79-871c-706dfd075327", + "importer": "sprite-frame", + "rawTextureUuid": "94a173e9-d339-4583-8962-0a18e0df52d5", + "trimType": "custom", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 38, + "height": 38, + "rawWidth": 38, + "rawHeight": 38, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/GameplayIntro/Texture/玩法指南字.png b/assets/GameplayIntro/Texture/玩法指南字.png new file mode 100644 index 0000000000000000000000000000000000000000..3d037cd26a8a8d22ee448dd71752d49d1d5bd126 GIT binary patch literal 10516 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 literal 0 HcmV?d00001 diff --git a/assets/GameplayIntro/Texture/玩法指南字.png.meta b/assets/GameplayIntro/Texture/玩法指南字.png.meta new file mode 100644 index 0000000..eff9d80 --- /dev/null +++ b/assets/GameplayIntro/Texture/玩法指南字.png.meta @@ -0,0 +1,38 @@ +{ + "ver": "2.3.7", + "uuid": "e0f7e25d-ab0d-4c0b-ba3b-0a7560ded387", + "importer": "texture", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "premultiplyAlpha": false, + "genMipmaps": false, + "packable": false, + "width": 310, + "height": 85, + "platformSettings": {}, + "subMetas": { + "玩法指南字": { + "ver": "1.0.6", + "uuid": "1b908cf0-2bab-4a0e-a4a8-6b1bc8692909", + "importer": "sprite-frame", + "rawTextureUuid": "e0f7e25d-ab0d-4c0b-ba3b-0a7560ded387", + "trimType": "custom", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 310, + "height": 85, + "rawWidth": 310, + "rawHeight": 85, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/libs/dn-sdk-minigame.meta b/assets/GameplayIntro/prefab.meta similarity index 81% rename from assets/libs/dn-sdk-minigame.meta rename to assets/GameplayIntro/prefab.meta index 2fe8053..e605f08 100644 --- a/assets/libs/dn-sdk-minigame.meta +++ b/assets/GameplayIntro/prefab.meta @@ -1,6 +1,6 @@ { "ver": "1.1.3", - "uuid": "97538b78-7842-4668-9318-e04946661548", + "uuid": "768035be-c53f-4e17-b438-a862006a56db", "importer": "folder", "isBundle": false, "bundleName": "", diff --git a/assets/GameplayIntro/prefab/GameplayIntro.prefab b/assets/GameplayIntro/prefab/GameplayIntro.prefab new file mode 100644 index 0000000..32ced8b --- /dev/null +++ b/assets/GameplayIntro/prefab/GameplayIntro.prefab @@ -0,0 +1,2362 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "", + "_objFlags": 0, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "asyncLoadAssets": false, + "readonly": false + }, + { + "__type__": "cc.Node", + "_name": "GameplayIntro", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 7 + } + ], + "_active": true, + "_components": [ + { + "__id__": 64 + } + ], + "_prefab": { + "__id__": 65 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1080, + "height": 1920 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Backdrop", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 3 + }, + { + "__id__": 4 + }, + { + "__id__": 5 + } + ], + "_prefab": { + "__id__": 6 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 4000, + "height": 4000 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Graphics", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "a153945d-2511-4c14-be7b-05d242f47d57" + } + ], + "_lineWidth": 2, + "_strokeColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_lineJoin": 2, + "_lineCap": 0, + "_fillColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_miterLimit": 10, + "_id": "" + }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_normalMaterial": null, + "_grayMaterial": null, + "duration": 0.1, + "zoomScale": 1.1, + "clickEvents": [], + "_N$interactable": true, + "_N$enableAutoGrayEffect": false, + "_N$transition": 0, + "transition": 0, + "_N$normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_N$pressedColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "pressedColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_N$hoverColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "hoverColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_N$disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_N$normalSprite": null, + "_N$pressedSprite": null, + "pressedSprite": null, + "_N$hoverSprite": null, + "hoverSprite": null, + "_N$disabledSprite": null, + "_N$target": { + "__id__": 2 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "RLuNcFtmjzPsfxQx+giP6w", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "IntroPanel", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 8 + }, + { + "__id__": 11 + }, + { + "__id__": 14 + }, + { + "__id__": 17 + }, + { + "__id__": 56 + }, + { + "__id__": 58 + } + ], + "_active": true, + "_components": [ + { + "__id__": 62 + } + ], + "_prefab": { + "__id__": 63 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 920, + "height": 1480 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "BackgroundImage", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 9 + } + ], + "_prefab": { + "__id__": 10 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 920, + "height": 1480 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 8 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "506252be-e576-479a-8475-649113ffa72f" + }, + "_type": 1, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "4JqtbQPABf5EV2rXTzvvqA", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "Title", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 12 + } + ], + "_prefab": { + "__id__": 13 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 310, + "height": 85 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 600, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 11 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "1b908cf0-2bab-4a0e-a4a8-6b1bc8692909" + }, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "i2/0JpaqcuctRPU1ClWYVw", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "Status", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 15 + } + ], + "_prefab": { + "__id__": 16 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 151, + "g": 111, + "b": 62, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 760, + "height": 100 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 14 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_string": "加载介绍中…", + "_N$string": "加载介绍中…", + "_fontSize": 32, + "_lineHeight": 40, + "_enableWrapText": true, + "_N$file": null, + "_isSystemFontUsed": true, + "_spacingX": 0, + "_batchAsBitmap": false, + "_styleFlags": 0, + "_underlineHeight": 0, + "_N$horizontalAlign": 1, + "_N$verticalAlign": 1, + "_N$fontFamily": "Arial", + "_N$overflow": 2, + "_N$cacheMode": 0, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d8lgNfQ+Vvz+0A8epBzunQ", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "IntroViewport", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [ + { + "__id__": 18 + } + ], + "_active": true, + "_components": [ + { + "__id__": 53 + }, + { + "__id__": 54 + } + ], + "_prefab": { + "__id__": 55 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 850, + "height": 1020 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Pages", + "_objFlags": 0, + "_parent": { + "__id__": 17 + }, + "_children": [ + { + "__id__": 19 + }, + { + "__id__": 30 + }, + { + "__id__": 41 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 52 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 850, + "height": 1020 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Page1", + "_objFlags": 0, + "_parent": { + "__id__": 18 + }, + "_children": [ + { + "__id__": 20 + }, + { + "__id__": 23 + }, + { + "__id__": 26 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 29 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 850, + "height": 1020 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "TitleImage", + "_objFlags": 0, + "_parent": { + "__id__": 19 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 21 + } + ], + "_prefab": { + "__id__": 22 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 730, + "height": 125 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 425, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 20 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "fbf81871-1c4d-463d-b388-2287474d2a33", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "IconImage", + "_objFlags": 0, + "_parent": { + "__id__": 19 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 24 + } + ], + "_prefab": { + "__id__": 25 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 660, + "height": 425 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 75, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 23 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "6a13c691-afab-4e00-b2f9-d60c0d707bfe", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "DescriptionImage", + "_objFlags": 0, + "_parent": { + "__id__": 19 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 27 + } + ], + "_prefab": { + "__id__": 28 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 735, + "height": 205 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + -325, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 26 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 1, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "7153a826-10e3-4bfb-8ab8-98e002e6b025", + "sync": false + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "736c29a2-96dc-4509-b40c-07d74d7f81ce", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "Page2", + "_objFlags": 0, + "_parent": { + "__id__": 18 + }, + "_children": [ + { + "__id__": 31 + }, + { + "__id__": 34 + }, + { + "__id__": 37 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 40 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 850, + "height": 1020 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 850, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "TitleImage", + "_objFlags": 0, + "_parent": { + "__id__": 30 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 32 + } + ], + "_prefab": { + "__id__": 33 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 730, + "height": 125 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 425, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 31 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9be1c97e-e0cb-4957-aee2-4ded08c94222", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "IconImage", + "_objFlags": 0, + "_parent": { + "__id__": 30 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 35 + } + ], + "_prefab": { + "__id__": 36 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 660, + "height": 425 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 75, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 34 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "19bad985-5823-4879-9cbf-7dd8f4dc4212", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "DescriptionImage", + "_objFlags": 0, + "_parent": { + "__id__": 30 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 38 + } + ], + "_prefab": { + "__id__": 39 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 735, + "height": 205 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + -325, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 37 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 1, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "b293a531-49fc-4357-9edd-4e7b89d7364d", + "sync": false + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "db760f39-54a3-42b7-84a3-a8945e408a1b", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "Page3", + "_objFlags": 0, + "_parent": { + "__id__": 18 + }, + "_children": [ + { + "__id__": 42 + }, + { + "__id__": 45 + }, + { + "__id__": 48 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 51 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 850, + "height": 1020 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 1700, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "TitleImage", + "_objFlags": 0, + "_parent": { + "__id__": 41 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 43 + } + ], + "_prefab": { + "__id__": 44 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 730, + "height": 125 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 425, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 42 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "864c0f29-e875-40d6-9583-d5869845a462", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "IconImage", + "_objFlags": 0, + "_parent": { + "__id__": 41 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 46 + } + ], + "_prefab": { + "__id__": 47 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 660, + "height": 425 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 75, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 45 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "40a3f9ed-8676-4eed-ab77-5070d6a1e4f9", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "DescriptionImage", + "_objFlags": 0, + "_parent": { + "__id__": 41 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 49 + } + ], + "_prefab": { + "__id__": 50 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 735, + "height": 205 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + -325, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 48 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": null, + "_type": 0, + "_sizeMode": 1, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "38f4fe50-3b53-460e-993f-e7213162b477", + "sync": false + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "249c882f-5b3c-4d05-8170-44174198eab4", + "sync": false + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "066a0ec9-5460-4a1a-af70-a9593ced2b37", + "sync": false + }, + { + "__type__": "cc.Mask", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 17 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_spriteFrame": null, + "_type": 0, + "_segments": 64, + "_N$alphaThreshold": 0.1, + "_N$inverted": false, + "_id": "" + }, + { + "__type__": "8168bhI+uRMW5/TmxovW2EE", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 17 + }, + "_enabled": true, + "dotFrame": { + "__uuid__": "93a7437b-9792-42c0-b0f5-73724314cbd8" + }, + "selectedDotFrame": { + "__uuid__": "af06b902-6722-4e79-871c-706dfd075327" + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "yraV/Owl8H/VN6T1NEj1Cw", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "Pagination", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 57 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 160, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + -615, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "/mpSXwHdYoE0dZohjCJEFQ", + "sync": false + }, + { + "__type__": "cc.Node", + "_name": "CloseButton", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 59 + }, + { + "__id__": 60 + } + ], + "_prefab": { + "__id__": 61 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 103, + "height": 110 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 365, + 615, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 58 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "b920b9e1-7d5d-4218-bc40-797cb0d9319d" + }, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 58 + }, + "_enabled": true, + "_normalMaterial": null, + "_grayMaterial": null, + "duration": 0.1, + "zoomScale": 1.1, + "clickEvents": [], + "_N$interactable": true, + "_N$enableAutoGrayEffect": false, + "_N$transition": 0, + "transition": 0, + "_N$normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_N$pressedColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "pressedColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_N$hoverColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "hoverColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_N$disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_N$normalSprite": null, + "_N$pressedSprite": null, + "pressedSprite": null, + "_N$hoverSprite": null, + "hoverSprite": null, + "_N$disabledSprite": null, + "_N$target": { + "__id__": 58 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d335EWZtVCrYv8+999km8J", + "sync": false + }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 7 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "SjhtNLAlHLqHHyNMZb8DSA", + "sync": false + }, + { + "__type__": "b6043noSMtByaaImD9068l0", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 1 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "", + "sync": false + } +] \ No newline at end of file diff --git a/assets/GameplayIntro/prefab/GameplayIntro.prefab.meta b/assets/GameplayIntro/prefab/GameplayIntro.prefab.meta new file mode 100644 index 0000000..2cf8f01 --- /dev/null +++ b/assets/GameplayIntro/prefab/GameplayIntro.prefab.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.3.2", + "uuid": "23d68b2f-0074-44dd-bbb2-5b064ee575a6", + "importer": "prefab", + "optimizationPolicy": "AUTO", + "asyncLoadAssets": false, + "readonly": false, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/passCheck/script.meta b/assets/GameplayIntro/script.meta similarity index 81% rename from assets/passCheck/script.meta rename to assets/GameplayIntro/script.meta index 8227b45..3115793 100644 --- a/assets/passCheck/script.meta +++ b/assets/GameplayIntro/script.meta @@ -1,6 +1,6 @@ { "ver": "1.1.3", - "uuid": "da60b47d-5d32-4804-8b5f-3411aff909a3", + "uuid": "281f1ce9-089b-4ef5-983d-42deec7d4e25", "importer": "folder", "isBundle": false, "bundleName": "", diff --git a/assets/GameplayIntro/script/GameplayIntro.ts b/assets/GameplayIntro/script/GameplayIntro.ts new file mode 100644 index 0000000..9277ad1 --- /dev/null +++ b/assets/GameplayIntro/script/GameplayIntro.ts @@ -0,0 +1,237 @@ +import GuideCarousel from "./GuideCarousel"; +// 一页图片的资源记录:node 是图片容器,frames 是本页持有引用的资源,sprites 是显示图片的组件。 +// live=false 表示本页已移出保留范围;晚到的加载结果必须丢弃,不能再显示。 +type IntroPage = { node: cc.Node; frames: cc.SpriteFrame[]; sprites: cc.Sprite[]; live: boolean }; +const { ccclass } = cc._decorator; + +// daoju26 是变色跳跃门、daoju27 是障碍杆;公共素材的这两个编号相反。 +const PROP_ART_IDS = Array.from({ length: 34 }, (_, index) => index === 26 ? 27 : index === 27 ? 26 : index); + +/** + * 本关玩法面板控制器,挂在 GameplayIntro 预制体根节点。 + * 阅读顺序:open 接收本关列表 → syncPages 确定需要的三页 → picture 排队加载图片。 + * 关闭时 releasePages 释放玩法图;背景、标题等固定布局留在预制体中供下次复用。 + */ +@ccclass +export default class GameplayIntro extends cc.Component { + private imageLayouts = new Map(); + + // 调用方已按本关配置筛选并去重;按解锁关卡倒序排列,每次打开最左端的最新玩法。 + private introductions: any[] = []; + private carousel: GuideCarousel = null; + private board: cc.Node = null; + private status: cc.Label = null; + // 每次打开/关闭递增的请求编号,用来识别已过期的异步加载回调。 + private request = 0; + private panelTween: cc.Tween = null; + private closing = false; + // 只记录当前页及相邻页的图片,不为所有历史页面常驻图片资源。 + private loadedPages: { [index: number]: IntroPage } = {}; + // 图片逐张加载:队列存“加载任务”,imageLoading 防止同时启动多个任务。 + private imageQueue: Array<() => void> = []; + private imageLoading = false; + + /** entries 是本关实际用到的玩法介绍,空列表直接关闭。 */ + open(entries: any[]) { + this.stopPanelAnimation(); + this.releasePages(); + this.introductions = entries.slice().sort((a, b) => b.level - a.level); + if (!this.introductions.length) { + if (this.carousel) this.carousel.clear(); + this.finishClose(); + return; + } + this.node.active = true; + // 固定节点只查找和绑定一次,避免重复打开时叠加关闭/翻页监听。 + if (!this.board) { + this.board = this.node.getChildByName("IntroPanel"); + this.status = this.board.getChildByName("Status").getComponent(cc.Label); + // Backdrop 是面板外的遮罩,点击它关闭;面板自身通过 BlockInputEvents 阻止穿透。 + const backdrop = this.node.getChildByName("Backdrop"); + const graphics = backdrop.getComponent(cc.Graphics); + graphics.fillColor = new cc.Color(0, 0, 0, 170); + graphics.rect(-backdrop.width / 2, -backdrop.height / 2, backdrop.width, backdrop.height); + graphics.fill(); + backdrop.on("click", this.close, this); + this.board.getChildByName("CloseButton").on("click", this.close, this); + const viewport = this.board.getChildByName("IntroViewport"); + this.carousel = viewport.getComponent(GuideCarousel); + viewport.on("page-changed", () => { + // GuideCarousel 通知目标页已变化,此时及时加载新邻页、释放远处页。 + this.syncPages(); + this.node.emit("guide-page-changed", this.getPreviewState()); + }, this); + } + // 重置分页状态和滑动监听,保留预制体中的三个固定页面。 + this.carousel.clear(); + this.status.node.targetOff(this); + this.status.node.active = true; + this.status.string = "加载介绍中…"; + this.status.node.active = false; + this.carousel.initialize(this.introductions.length, 0, this.board.getChildByName("Pagination")); + this.board.scale = 0.3; + this.panelTween = cc.tween(this.board) + .to(0.2, { scale: 1.05 }, { easing: "backOut" }) + .to(0.15, { scale: 1 }, { easing: "sineOut" }); + this.panelTween.start(); + } + + /** 性能优化入口:翻页后只保留 current-1、current、current+1 这三页的图片。 */ + private syncPages() { + const current = this.carousel.index; + Object.keys(this.loadedPages).forEach(key => { + const index = Number(key); + // 例如当前为第 5 项,只保留索引 4、5、6;其它已加载页立即释放。 + if (Math.abs(index - current) > 1) this.releasePage(index); + }); + // 优先加载当前页,再加载前一页和后一页;越界或已加载的页面直接跳过。 + // 在真实列表的两端,通常只需保留两页。 + [current, current - 1, current + 1].forEach(index => { + if (index < 0 || index >= this.introductions.length || this.loadedPages[index]) return; + const node = this.carousel.pages[index]; + node.active = true; + const page: IntroPage = { node, frames: [], sprites: [], live: true }; + this.loadedPages[index] = page; + const entry = this.introductions[index]; + const art = PROP_ART_IDS[Number(entry.name.replace("daoju", ""))]; + // 公共玩法图:上方标题、中间道具大图、下方说明。 + if (art !== undefined) { + this.picture(page, "TitleImage", "title" + art); + this.picture(page, "IconImage", "icon" + art); + this.picture(page, "DescriptionImage", "Introduction" + art); + } else this.showMessage(entry.tips); + }); + } + + /** 替换预制体中的图片;说明图由组件控制大小;标题和道具图按预制体展示区域等比适配。 */ + private picture(page: IntroPage, nodeName: string, path: string) { + const node = page.node.getChildByName(nodeName); + const fitImage = nodeName !== "DescriptionImage"; + if (fitImage && !this.imageLayouts.has(node)) { + this.imageLayouts.set(node, { width: node.width, height: node.height, scale: node.scale }); + } + const sprite = node.getComponent(cc.Sprite); + page.sprites.push(sprite); + this.imageQueue.push(() => { + // 排队期间用户可能已翻走或关闭面板,失效任务无需再发起资源加载。 + if (!page.live) { this.nextImage(); return; } + this.imageLoading = true; + cc.resources.load("Window_Prop/" + path, cc.SpriteFrame, (error, frame: cc.SpriteFrame) => { + // addRef 表示本页持有这份资源;释放页时配对 decRef。 + // decRef 减少引用,资源没有其它引用时由引擎回收。 + if (!error && frame) frame.addRef(); + // 已经发出的请求不能靠清空队列取消;因此结果回来时还要再检查页面是否有效。 + if (!page.live || !cc.isValid(node, true)) { + if (!error && frame) frame.decRef(); + } else if (error) { + this.showMessage("图片加载失败"); + cc.error("Gameplay intro image", path, error); + } else { + page.frames.push(frame); + // 禁止加入动态合图,避免图片被复制到长期存在的图集中,影响关闭后的回收。 + frame.getTexture().packable = false; + sprite.spriteFrame = frame; + if (fitImage) { + const layout = this.imageLayouts.get(node); + const size = frame.getOriginalSize(); + node.setContentSize(size.width, size.height); + node.scale = Math.min(layout.width / size.width, layout.height / size.height) * layout.scale; + } + + } + this.imageLoading = false; + this.nextImage(); + }); + }); + this.nextImage(); + } + + /** 一次只启动一张图,当前加载回调结束后才取下一项,减少集中加载造成的卡顿。 */ + private nextImage() { + if (this.imageLoading || !this.imageQueue.length) return; + this.imageQueue.shift()(); + } + + /** 先断开显示引用,再归还资源引用,最后隐藏固定页面,供后续复用。 */ + private releasePage(index: number) { + const page = this.loadedPages[index]; + page.live = false; + page.sprites.forEach(sprite => { + if (cc.isValid(sprite, true)) sprite.spriteFrame = null; + }); + page.frames.forEach(frame => frame.decRef()); + if (cc.isValid(page.node, true)) { + page.node.active = false; + // 固定页面节点保留,下一次仅替换图片。 + } + delete this.loadedPages[index]; + } + + /** 清空待加载任务和已加载图片;在途请求返回后由 picture 中的 live 检查处理。 */ + private releasePages() { + this.imageQueue = []; + Object.keys(this.loadedPages).forEach(key => this.releasePage(Number(key))); + } + + // 即使外部直接隐藏面板而没有调用 close,也要释放图片并让旧请求失效。 + onDisable() { + this.stopPanelAnimation(); + ++this.request; + this.releasePages(); + } + + // 场景退出或节点被销毁时的兜底清理。 + onDestroy() { + this.stopPanelAnimation(); + this.releasePages(); + } + + /** 隐藏面板并通知打开它的场景;是否恢复游戏由 SceneManager 判断。 */ + close() { + if (this.closing || !this.node.active) return; + if (!this.board) { this.finishClose(); return; } + if (this.panelTween) this.panelTween.stop(); + this.closing = true; + this.panelTween = cc.tween(this.board) + .to(0.15, { scale: 0.3 }, { easing: "sineIn" }) + .call(() => this.finishClose()); + this.panelTween.start(); + } + + // 退场完成后再释放图片和通知场景恢复游戏,避免动画期间内容消失。 + private finishClose() { + this.stopPanelAnimation(); + ++this.request; + this.releasePages(); + this.node.active = false; + this.node.emit("intro-closed"); + } + + private stopPanelAnimation() { + if (this.panelTween) this.panelTween.stop(); + this.panelTween = null; + this.closing = false; + // 销毁回调中子节点可能已释放;这里只停止动画,不再写入节点属性。 + // 每次打开都会重新设置起始缩放。 + } + + /** 给测试面板使用的跳页入口,复用正式滑动组件的翻页逻辑。 */ + previewNavigate(action: string) { + if (!this.carousel || !this.carousel.pages.length) return; + const next = action === "first" ? 0 : action === "last" ? this.introductions.length - 1 : + this.carousel.index + (action === "previous" ? -1 : 1); + this.carousel.goTo(next); + } + + /** 返回当前索引、总页数和玩法配置,供测试面板显示状态。 */ + getPreviewState() { + const index = this.carousel && this.carousel.pages.length ? this.carousel.index : 0; + return { index, total: this.introductions.length, entry: this.introductions[index] || null }; + } + + // 提示也复用主预制体的 Status 节点,避免向固定玩法页追加节点。 + private showMessage(value: string) { + this.status.string = value; + this.status.node.active = true; + } +} diff --git a/assets/GameplayIntro/script/GameplayIntro.ts.meta b/assets/GameplayIntro/script/GameplayIntro.ts.meta new file mode 100644 index 0000000..0aee8f6 --- /dev/null +++ b/assets/GameplayIntro/script/GameplayIntro.ts.meta @@ -0,0 +1,10 @@ +{ + "ver": "1.1.0", + "uuid": "b60439e8-48cb-41c9-a688-983f74ebc974", + "importer": "typescript", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/GameplayIntro/script/GuideCarousel.ts b/assets/GameplayIntro/script/GuideCarousel.ts new file mode 100644 index 0000000..9e14978 --- /dev/null +++ b/assets/GameplayIntro/script/GuideCarousel.ts @@ -0,0 +1,275 @@ +const { ccclass, property } = cc._decorator; + +/** + * 通用横向滑动组件,挂在预制体的 IntroViewport 上。 + * 只负责位移、手势和白点动画,不负责解锁配置及图片加载。 + * 手势流程:begin 按下 → move 跟手 → end 松开选页 → update 每帧完成缓动。 + * 实际页码 index 与白点位置 activeDot 是两套状态,介绍页数可以超过 3。 + */ +@ccclass +export default class GuideCarousel extends cc.Component { + @property(cc.SpriteFrame) + dotFrame: cc.SpriteFrame = null; + @property(cc.SpriteFrame) + selectedDotFrame: cc.SpriteFrame = null; + // 当前目标页索引,从 0 开始;pages 包含所有介绍页的占位节点。 + index = 0; + pages: cc.Node[] = []; + private pageSlots: cc.Node[] = []; + // 所有页面都挂到 track 下;移动这个父节点即可让整排页面一起移动。 + private track: cc.Node = null; + private dots: cc.Node[] = []; + // 最多三个白点;打开时根据默认页定位。 + private activeDot = 0; + // 仅替换动画期间显示的辅助白点,平时隐藏,不表示额外页面。 + private incomingDot: cc.Node = null; + // 替换动画的移动方向:1 向右移动,-1 向左移动,0 不播放。 + private dotDirection = 0; + private dotElapsed = 0; + // width 是一页宽度;offset 是整排页面的横向位移;origin 是按下时的位移基准。 + private width = 880; + private offset = 0; + private origin = 0; + // 拖动起点、最近位置和最近时间,用于计算拖动距离及甩动速度。 + private startX = 0; + private lastX = 0; + private lastTime = 0; + private speed = 0; + // 记录本次拖动属于鼠标还是哪根手指,避免多个输入同时控制页面。 + private pointer: string = null; + // 松手缓动的起点、终点、已过时间;settling=true 时 update 会继续移动。 + private from = 0; + private destination = 0; + private elapsed = 0; + private settling = false; + + /** 传入所有页面、默认页索引和分页器容器;重复初始化会先清理旧内容。 */ + initialize(count: number, selected: number, indicator: cc.Node) { + this.clear(); + this.width = this.node.width; + this.track = this.node.getChildByName("Pages"); + this.pageSlots = this.track.children.slice(); + // 逻辑页共用预制体内的三个固定槽位,相邻三页对应不同槽位。 + const pages = this.pages = Array.from({ length: count }, (_, index) => this.pageSlots[index % 3]); + const dotCount = pages.length > 1 ? Math.min(3, pages.length) : 0; + indicator.active = dotCount > 0; + for (let i = 0; i < dotCount; i++) { + const dot = this.createDot(indicator, "Dot" + i); + dot.x = (i - (dotCount - 1) / 2) * 40; + this.dots.push(dot); + } + if (pages.length > 3) { + this.incomingDot = this.createDot(indicator, "IncomingDot"); + this.incomingDot.active = false; + } + this.index = Math.max(0, Math.min(pages.length - 1, selected)); + this.activeDot = Math.min(Math.max(0, dotCount - 1), this.index); + this.goTo(this.index, false); + if (pages.length < 2) return; + const type = cc.Node.EventType; + this.node.on(type.TOUCH_START, this.touchStart, this); + this.node.on(type.TOUCH_MOVE, this.touchMove, this); + this.node.on(type.TOUCH_END, this.touchEnd, this); + this.node.on(type.TOUCH_CANCEL, this.touchCancel, this); + // 同时监听鼠标与触摸,兼容桌面预览中没有被转换成触摸事件的鼠标拖动。 + this.node.on(type.MOUSE_DOWN, this.mouseDown, this); + this.node.on(type.MOUSE_MOVE, this.mouseMove, this); + this.node.on(type.MOUSE_UP, this.mouseUp, this); + this.node.on(type.MOUSE_LEAVE, this.mouseLeave, this); + } + + /** 清理本组件的监听、页面轨道和白点,防止重复打开后节点/事件越来越多。 */ + clear() { + this.node.targetOff(this); + this.pointer = null; + this.settling = false; + this.dotDirection = 0; + this.pageSlots.forEach(page => { if (cc.isValid(page, true)) page.active = false; }); + this.dots.concat(this.incomingDot ? [this.incomingDot] : []).forEach(dot => { + dot.removeFromParent(); + dot.destroy(); + }); + this.track = null; + this.incomingDot = null; + this.dots = []; + this.pages = []; + this.offset = 0; + } + + /** 切换目标页;animated=false 用于首次打开时直接定位,默认则平滑过渡。 */ + goTo(index: number, animated = true) { + if (!this.track || !this.pages.length) return; + this.pointer = null; + const previous = this.index; + const previousDot = this.activeDot; + this.resetDotAnimation(); + // 限制在真实页码范围内,所以边界拖动不会进入不存在的页面。 + this.index = Math.max(0, Math.min(this.pages.length - 1, index)); + // 查看更早页:左点 → 中点 → 右点 → 一直保持右点;查看更新页则反向移动。 + // 这里只根据实际页码变化量移动白点,并不要求已经到达介绍列表的两端。 + // 如果只是边界回弹,页码未变,白点也不会改变。 + this.activeDot = Math.max(0, Math.min(this.dots.length - 1, this.activeDot + this.index - previous)); + this.from = this.offset; + // 第 n 页原本在 n*width 处,让整排左移 n*width,就能把该页放到视口中心。 + this.destination = -this.index * this.width; + this.elapsed = 0; + this.settling = animated; + if (!animated) this.offset = this.destination; + this.dots.forEach((dot, i) => this.styleDot(dot, i === this.activeDot)); + const delta = this.index - previous; + // 页码确实改变,但高亮点已经停在最左/最右时,播放白点从旁边补进的替换动画。 + // 真正到达列表边界且页码未改变时不播放。 + if (this.incomingDot && animated && delta !== 0 && previousDot === this.activeDot && + ((delta < 0 && this.activeDot === 0) || (delta > 0 && this.activeDot === 2))) { + this.dotDirection = delta < 0 ? 1 : -1; + this.dotElapsed = 0; + this.incomingDot.active = true; + this.animateDots(0); + } + this.render(); + this.node.emit("page-changed", this.index); + } + + // 分页图片由预制体直接持有,重复打开无需异步加载。 + private createDot(parent: cc.Node, name: string): cc.Node { + const dot = new cc.Node(name); + dot.parent = parent; + dot.addComponent(cc.Sprite); + this.styleDot(dot, false); + return dot; + } + + private styleDot(dot: cc.Node, selected: boolean) { + dot.getComponent(cc.Sprite).spriteFrame = selected ? this.selectedDotFrame : this.dotFrame; + dot.scale = 1; + } + + // 动画结束或被打断时恢复三个固定位置,隐藏辅助点,选中点使用白色圆点图片。 + private resetDotAnimation() { + this.dotDirection = 0; + if (this.incomingDot) this.incomingDot.active = false; + this.dots.forEach((dot, i) => { + dot.x = (i - (this.dots.length - 1) / 2) * 40; + dot.opacity = 255; + this.styleDot(dot, i === this.activeDot); + }); + } + + /** dt 是这一帧经过的秒数;旧点移出并淡出,辅助点从另一边进入并放大。 */ + private animateDots(dt: number) { + if (!this.dotDirection) return; + this.dotElapsed += dt; + // 替换动画持续 0.28 秒,三次方缓动使运动先快后慢。 + const t = Math.min(1, this.dotElapsed / 0.28); + const eased = 1 - Math.pow(1 - t, 3); + const direction = this.dotDirection; + this.dots.forEach((dot, i) => { + const destination = i + direction; + this.styleDot(dot, (eased < 0.5 ? i : destination) === this.activeDot); + dot.x = (i - 1) * 40 + direction * 40 * eased; + dot.opacity = destination < 0 || destination > 2 ? Math.round(255 * (1 - eased)) : 255; + }); + this.incomingDot.x = -direction * (80 - 40 * eased); + this.incomingDot.opacity = Math.round(255 * eased); + this.styleDot(this.incomingDot, true); + this.incomingDot.scale = 0.5 + 0.5 * eased; + if (t === 1) this.resetDotAnimation(); + } + + // 把屏幕坐标转成视口局部坐标,面板有缩放时仍能正确计算拖动距离。 + private x(event: cc.Event.EventTouch | cc.Event.EventMouse) { + return this.node.convertToNodeSpaceAR(event.getLocation()).x; + } + + /** 按下时接管当前位置,允许用户在回弹中途再次抓住页面。 */ + private begin(pointer: string, x: number) { + if (this.pointer !== null || this.pages.length < 2) return; + this.pointer = pointer; + this.settling = false; + this.startX = this.lastX = x; + this.lastTime = Date.now(); + this.speed = 0; + this.origin = this.offset; + const min = -(this.pages.length - 1) * this.width; + // 边界位移已经经过阻尼压缩;这里反算原始拖动量。 + // 否则在回弹途中重新按下,第一次 move 会再次压缩位移,导致内容突然跳动。 + if (this.offset > 0) this.origin = 240 * this.offset / (120 - this.offset); + else if (this.offset < min) { + const extra = min - this.offset; + this.origin = min - 240 * extra / (120 - extra); + } + } + + /** 拖动中直接更新位移,实现跟手效果。 */ + private move(pointer: string, x: number) { + if (this.pointer !== pointer) return; + const now = Date.now(); + this.speed = (x - this.lastX) / Math.max(1, now - this.lastTime); + this.lastX = x; + this.lastTime = now; + const raw = this.origin + x - this.startX; + const min = -(this.pages.length - 1) * this.width; + // 合法位移范围是 [min, 0]。超过任一端后使用阻尼:越拉越费劲,位移趋近 120。 + // 240 控制阻力曲线;范围内则直接使用 raw,不额外减慢跟手移动。 + this.offset = raw > 0 ? 120 * raw / (240 + raw) : raw < min ? min - 120 * (min - raw) / (240 + min - raw) : raw; + this.render(); + } + + /** 松手时根据距离/速度决定目标页,再交给 goTo 做吸附或回弹。 */ + private end(pointer: string, cancel = false) { + if (this.pointer !== pointer) return; + const distance = this.lastX - this.startX; + // 停住超过 100 毫秒再松手,就不再把之前的移动速度当作快速甩动。 + const speed = Date.now() - this.lastTime < 100 ? this.speed : 0; + let next = Math.round(-this.offset / this.width); + // 拖过页宽的 18%,或速度超过每毫秒 0.45 个局部坐标单位,即按方向翻一页。 + // 触摸被系统取消时,使用当前位置最近的页面,不额外按甩动方向翻页。 + if (!cancel && (Math.abs(distance) > this.width * 0.18 || Math.abs(speed) > 0.45)) { + next = this.index + ((Math.abs(speed) > 0.45 ? speed : distance) < 0 ? 1 : -1); + } + this.goTo(next); + } + + // 触摸事件带手指 ID;鼠标只响应左键。离开视口也结束拖动,避免内容一直粘着鼠标。 + private touchStart(event: cc.Event.EventTouch) { this.begin("touch" + event.getID(), this.x(event)); event.stopPropagation(); } + private touchMove(event: cc.Event.EventTouch) { this.move("touch" + event.getID(), this.x(event)); event.stopPropagation(); } + private touchEnd(event: cc.Event.EventTouch) { this.end("touch" + event.getID()); event.stopPropagation(); } + private touchCancel(event: cc.Event.EventTouch) { this.end("touch" + event.getID(), true); event.stopPropagation(); } + private mouseDown(event: cc.Event.EventMouse) { if (event.getButton() === 0) this.begin("mouse", this.x(event)); } + private mouseMove(event: cc.Event.EventMouse) { this.move("mouse", this.x(event)); } + private mouseUp(event: cc.Event.EventMouse) { if (event.getButton() === 0) this.end("mouse"); } + private mouseLeave() { this.end("mouse"); } + + /** 应用当前位移;离视口中心越远的页面稍微缩小、变淡,让过渡更自然。 */ + private render() { + this.track.x = this.offset; + this.pageSlots.forEach(page => page.active = false); + for (let i = Math.max(0, this.index - 1); i <= Math.min(this.pages.length - 1, this.index + 1); i++) { + const page = this.pages[i]; + page.active = true; + page.x = i * this.width; + const distance = Math.min(1, Math.abs(i * this.width + this.offset) / this.width); + page.scale = 1 - distance * 0.025; + page.opacity = Math.round(255 - distance * 45); + } + } + + /** Cocos 每帧调用:推进白点动画,以及松手后的 0.32 秒页面缓动。 */ + update(dt: number) { + this.animateDots(dt); + if (!this.settling) return; + this.elapsed += dt; + const t = Math.min(1, this.elapsed / 0.32); + this.offset = this.from + (this.destination - this.from) * (1 - Math.pow(1 - t, 3)); + this.render(); + if (t === 1) this.settling = false; + } + + // 面板隐藏时结束未完成的手势和动画,避免再次显示时残留半途状态。 + onDisable() { + this.resetDotAnimation(); + this.pointer = null; + this.settling = false; + if (this.track && cc.isValid(this.track, true) && this.pageSlots.every(page => cc.isValid(page, true))) { this.offset = -this.index * this.width; this.render(); } + } +} diff --git a/assets/GameplayIntro/script/GuideCarousel.ts.meta b/assets/GameplayIntro/script/GuideCarousel.ts.meta new file mode 100644 index 0000000..fcc0453 --- /dev/null +++ b/assets/GameplayIntro/script/GuideCarousel.ts.meta @@ -0,0 +1,10 @@ +{ + "ver": "1.1.0", + "uuid": "8168b848-fae4-4c5b-9fd3-9b1a2f5b6104", + "importer": "typescript", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/Scene/GameScene.fire b/assets/Scene/GameScene.fire index 776385b..f9f688d 100644 --- a/assets/Scene/GameScene.fire +++ b/assets/Scene/GameScene.fire @@ -17,7 +17,7 @@ "__id__": 2 }, { - "__id__": 297 + "__id__": 296 } ], "_active": false, @@ -84,14 +84,20 @@ "__id__": 258 }, { - "__id__": 262 + "__id__": 261 }, { - "__id__": 287 + "__id__": 286 + }, + { + "__id__": 303 } ], "_active": true, "_components": [ + { + "__id__": 292 + }, { "__id__": 293 }, @@ -100,9 +106,6 @@ }, { "__id__": 295 - }, - { - "__id__": 296 } ], "_prefab": null, @@ -801,6 +804,9 @@ }, { "__id__": 62 + }, + { + "__id__": 298 } ], "_active": true, @@ -12376,7 +12382,7 @@ "__id__": 260 }, { - "__id__": 261 + "__id__": 304 } ], "_prefab": null, @@ -12474,16 +12480,6 @@ "_originalHeight": 0, "_id": "394oVlZVVDDp4PRHaKfK2n" }, - { - "__type__": "cc.BlockInputEvents", - "_name": "", - "_objFlags": 0, - "node": { - "__id__": 258 - }, - "_enabled": true, - "_id": "d9e8Y//+dGO5Tm2vNoQbwO" - }, { "__type__": "cc.Node", "_name": "Loading", @@ -12493,31 +12489,31 @@ }, "_children": [ { - "__id__": 263 + "__id__": 262 }, { - "__id__": 266 + "__id__": 265 }, { - "__id__": 268 + "__id__": 267 }, { - "__id__": 272 + "__id__": 271 }, { - "__id__": 276 + "__id__": 275 }, { - "__id__": 280 + "__id__": 279 }, { - "__id__": 284 + "__id__": 283 } ], "_active": false, "_components": [ { - "__id__": 286 + "__id__": 285 } ], "_prefab": null, @@ -12573,16 +12569,16 @@ "_name": "New Sprite(Splash)", "_objFlags": 0, "_parent": { - "__id__": 262 + "__id__": 261 }, "_children": [], "_active": true, "_components": [ { - "__id__": 264 + "__id__": 263 }, { - "__id__": 265 + "__id__": 264 } ], "_prefab": null, @@ -12638,7 +12634,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 263 + "__id__": 262 }, "_enabled": true, "_materials": [ @@ -12670,7 +12666,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 263 + "__id__": 262 }, "_enabled": true, "_id": "0fS5t0zP5AdI0CPkmzwBv/" @@ -12680,13 +12676,13 @@ "_name": "New Label", "_objFlags": 0, "_parent": { - "__id__": 262 + "__id__": 261 }, "_children": [], "_active": true, "_components": [ { - "__id__": 267 + "__id__": 266 } ], "_prefab": null, @@ -12742,7 +12738,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 266 + "__id__": 265 }, "_enabled": true, "_materials": [ @@ -12775,17 +12771,17 @@ "_name": "block1", "_objFlags": 0, "_parent": { - "__id__": 262 + "__id__": 261 }, "_children": [ { - "__id__": 269 + "__id__": 268 } ], "_active": true, "_components": [ { - "__id__": 271 + "__id__": 270 } ], "_prefab": null, @@ -12841,13 +12837,13 @@ "_name": "light", "_objFlags": 0, "_parent": { - "__id__": 268 + "__id__": 267 }, "_children": [], "_active": true, "_components": [ { - "__id__": 270 + "__id__": 269 } ], "_prefab": null, @@ -12903,7 +12899,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 269 + "__id__": 268 }, "_enabled": true, "_materials": [ @@ -12935,7 +12931,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 268 + "__id__": 267 }, "_enabled": true, "_materials": [ @@ -12967,17 +12963,17 @@ "_name": "block2", "_objFlags": 0, "_parent": { - "__id__": 262 + "__id__": 261 }, "_children": [ { - "__id__": 273 + "__id__": 272 } ], "_active": true, "_components": [ { - "__id__": 275 + "__id__": 274 } ], "_prefab": null, @@ -13033,13 +13029,13 @@ "_name": "light", "_objFlags": 0, "_parent": { - "__id__": 272 + "__id__": 271 }, "_children": [], "_active": true, "_components": [ { - "__id__": 274 + "__id__": 273 } ], "_prefab": null, @@ -13095,7 +13091,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 273 + "__id__": 272 }, "_enabled": true, "_materials": [ @@ -13127,7 +13123,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 272 + "__id__": 271 }, "_enabled": true, "_materials": [ @@ -13159,17 +13155,17 @@ "_name": "block3", "_objFlags": 0, "_parent": { - "__id__": 262 + "__id__": 261 }, "_children": [ { - "__id__": 277 + "__id__": 276 } ], "_active": true, "_components": [ { - "__id__": 279 + "__id__": 278 } ], "_prefab": null, @@ -13225,13 +13221,13 @@ "_name": "light", "_objFlags": 0, "_parent": { - "__id__": 276 + "__id__": 275 }, "_children": [], "_active": true, "_components": [ { - "__id__": 278 + "__id__": 277 } ], "_prefab": null, @@ -13287,7 +13283,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 277 + "__id__": 276 }, "_enabled": true, "_materials": [ @@ -13319,7 +13315,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 276 + "__id__": 275 }, "_enabled": true, "_materials": [ @@ -13351,17 +13347,17 @@ "_name": "block4", "_objFlags": 0, "_parent": { - "__id__": 262 + "__id__": 261 }, "_children": [ { - "__id__": 281 + "__id__": 280 } ], "_active": true, "_components": [ { - "__id__": 283 + "__id__": 282 } ], "_prefab": null, @@ -13417,13 +13413,13 @@ "_name": "light", "_objFlags": 0, "_parent": { - "__id__": 280 + "__id__": 279 }, "_children": [], "_active": true, "_components": [ { - "__id__": 282 + "__id__": 281 } ], "_prefab": null, @@ -13479,7 +13475,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 281 + "__id__": 280 }, "_enabled": true, "_materials": [ @@ -13511,7 +13507,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 280 + "__id__": 279 }, "_enabled": true, "_materials": [ @@ -13543,13 +13539,13 @@ "_name": "cat", "_objFlags": 0, "_parent": { - "__id__": 262 + "__id__": 261 }, "_children": [], "_active": true, "_components": [ { - "__id__": 285 + "__id__": 284 } ], "_prefab": null, @@ -13605,7 +13601,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 284 + "__id__": 283 }, "_enabled": true, "_materials": [ @@ -13637,7 +13633,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 262 + "__id__": 261 }, "_enabled": true, "alignMode": 1, @@ -13668,16 +13664,16 @@ }, "_children": [ { - "__id__": 288 + "__id__": 287 } ], "_active": false, "_components": [ { - "__id__": 291 + "__id__": 290 }, { - "__id__": 292 + "__id__": 291 } ], "_prefab": null, @@ -13733,16 +13729,16 @@ "_name": "New Sprite(Splash)", "_objFlags": 0, "_parent": { - "__id__": 287 + "__id__": 286 }, "_children": [], "_active": true, "_components": [ { - "__id__": 289 + "__id__": 288 }, { - "__id__": 290 + "__id__": 289 } ], "_prefab": null, @@ -13798,7 +13794,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 288 + "__id__": 287 }, "_enabled": true, "_materials": [ @@ -13830,7 +13826,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 288 + "__id__": 287 }, "_enabled": true, "_id": "4fwjz1lnFLD45L1lqUY038" @@ -13840,7 +13836,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 287 + "__id__": 286 }, "_enabled": true, "_materials": [ @@ -13882,7 +13878,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 287 + "__id__": 286 }, "_enabled": false, "_id": "00qpMLeBFNhYRX9r3QZvyP" @@ -13972,7 +13968,10 @@ "__uuid__": "fd85df88-648f-4407-bab2-da4f15478fb9" }, "messageNode": null, - "_id": "4cjBjPtcBO05zIQupw5luh" + "_id": "4cjBjPtcBO05zIQupw5luh", + "tipPanel": { + "__id__": 303 + } }, { "__type__": "f1883pXzMVIp5/ARDAWfZ7j", @@ -13995,7 +13994,7 @@ "_active": true, "_components": [ { - "__id__": 298 + "__id__": 297 } ], "_prefab": null, @@ -14051,7 +14050,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 297 + "__id__": 296 }, "_enabled": true, "_materials": [ @@ -14075,5 +14074,316 @@ "_isTrimmedMode": true, "_atlas": null, "_id": "95joh5iDZB1IGmJkAeXXrL" + }, + { + "__type__": "cc.Node", + "_name": "tip", + "_objFlags": 0, + "_parent": { + "__id__": 15 + }, + "_children": [ + { + "__id__": 299 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 0 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "e4LIw406xPc6DLoPDZ9wmy" + }, + { + "__type__": "cc.Node", + "_name": "GameplayInfoButton", + "_objFlags": 0, + "_parent": { + "__id__": 298 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 300 + }, + { + "__id__": 301 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 82.66666666666667, + "height": 88 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 282, + -60.562, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "Vg/+kIt8y9qkYZSB/Xx8wA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 299 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "d9333256-302a-403b-93bf-e25c42829476" + }, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "7urvt+4r1Eol9iPd9AAi5Q" + }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 299 + }, + "_enabled": true, + "_normalMaterial": null, + "_grayMaterial": null, + "duration": 0.1, + "zoomScale": 1.2, + "clickEvents": [ + { + "__id__": 302 + } + ], + "_N$interactable": true, + "_N$enableAutoGrayEffect": false, + "_N$transition": 0, + "transition": 0, + "_N$normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_N$pressedColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "pressedColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_N$hoverColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "hoverColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_N$disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_N$normalSprite": null, + "_N$pressedSprite": null, + "pressedSprite": null, + "_N$hoverSprite": null, + "hoverSprite": null, + "_N$disabledSprite": null, + "_N$target": { + "__id__": 299 + }, + "_id": "4y6hgv/WvY4E/88bmCerjg" + }, + { + "__type__": "cc.ClickEvent", + "target": { + "__id__": 2 + }, + "component": "", + "_componentId": "80998T1AYNNRZdRxsVvXR65", + "handler": "openGameplayIntro", + "customEventData": "" + }, + { + "__type__": "cc.Node", + "_name": "tipPanel", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 0 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "ffogY+noNEpL/dzRMkLQ4G" + }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 258 + }, + "_enabled": true, + "_id": "d9e8Y//+dGO5Tm2vNoQbwO" } ] \ No newline at end of file diff --git a/assets/Script/SceneManager.ts b/assets/Script/SceneManager.ts index b3144cc..234d747 100644 --- a/assets/Script/SceneManager.ts +++ b/assets/Script/SceneManager.ts @@ -1,3 +1,5 @@ +import type GameplayIntro from "../GameplayIntro/script/GameplayIntro"; +import { getLevelGameplay } from "./module/Tool/LevelGameplay"; // Learn TypeScript: // - https://docs.cocos.com/creator/manual/en/scripting/typescript.html // Learn Attribute: @@ -698,7 +700,18 @@ export default class SceneManager extends cc.Component { return true; } + // 在 Creator 的 Inspector 中绑定场景现有 tipPanel;打开后的预制体挂到这里。 + @property(cc.Node) + tipPanel: cc.Node = null; + + // 保存已创建的面板控制器,重复打开复用实例;玩法图片仍由面板按需加载/释放。 + private gameplayIntro: GameplayIntro = null; + private levelGameplay: any[] = []; + isPauseOpen(): boolean { + // 加载期间及面板显示期间都视为暂停,避免重复打开或其它流程提前恢复游戏。 + if (this.dynamicPopupLoads && this.dynamicPopupLoads.gameplayIntro) return true; + if (this.gameplayIntro && cc.isValid(this.gameplayIntro.node) && this.gameplayIntro.node.active) return true; const pauseRoot = this.getDynamicPopup("pause"); return !!(this.dynamicPopupLoads && this.dynamicPopupLoads.pause) || (!!pauseRoot && pauseRoot.active); @@ -792,6 +805,7 @@ export default class SceneManager extends cc.Component { this.dynamicPopupRetries = {}; this.dynamicPopupStates = {}; this.popupBundleWarmupStarted = false; + this.refreshGameplayInfo(); this.ensureLoadingOnTop(); const intercept = this.ensureDynamicPopupIntercept(); if (intercept) intercept.active = false; @@ -1158,8 +1172,75 @@ export default class SceneManager extends cc.Component { }); } - start() { + /** 关卡数据在进入场景前已加载;按完整关卡保存列表,消除道具后仍可查阅。 */ + private refreshGameplayInfo() { + const button = cc.find("Game/GameNode/Top/tip/GameplayInfoButton", this.node); + const apply = (config: any[]) => { + this.levelGameplay = getLevelGameplay(config, cc.fx.GameConfig); + if (button && cc.isValid(button)) button.active = this.levelGameplay.length > 0; + }; + if (cc.fx.GameConfig.NEW_LEVEL) { + apply(cc.fx.GameConfig.NEW_LEVEL); + } else { + // 全局介绍配置是异步读取的,首次进场景时可能仍在加载。 + if (button) button.active = false; + cc.resources.load("Json/NEW_LEVEL", cc.JsonAsset, (error, asset: cc.JsonAsset) => { + if (!this.isPopupRuntimeAlive()) return; + if (error) { cc.error("Gameplay introductions failed to load", error); return; } + apply(asset.json.NEW_LEVEL); + }); + } + } + /** + * 问号 Button 的点击入口。场景按钮需绑定这个方法,Tip Panel 属性需绑定容器节点。 + * 顺序:检查能否打开 → 暂停游戏 → 加载/复用预制体 → 交给 GameplayIntro 显示内容。 + */ + openGameplayIntro() { + if (!this.isPopupRuntimeAlive() || this.isPauseOpen()) return; + if (!this.levelGameplay.length) return; + const map = MapConroler._instance; + if (!map || map.gameOver || map.shopPause) return; + if (!this.tipPanel || !cc.isValid(this.tipPanel)) { + cc.error("GameplayIntro: SceneManager.tipPanel is not assigned"); + return; + } + // 关闭或加载失败时尝试恢复;仍有冰冻、其它暂停面板、商城或已结束时不能强行恢复。 + const resume = () => { + if (!this.isPopupRuntimeAlive()) return; + if (!map.iceTrue() && !this.isPauseOpen() && !map.shopPause && !map.gameOver) { + map.pause = false; + if (map.gameStart) map.startBoom(); + } + }; + // 暂停游戏,并停止场上炸弹计时,避免阅读玩法期间仍消耗倒计时。 + map.pause = true; + map.stopBoom(); + this.tipPanel.active = true; + this.tipPanel.zIndex = cc.macro.MAX_ZINDEX - 2; + // 参数依次为:弹窗缓存名、资源包名、包内预制体路径、挂载父节点、完成回调。 + // 通用加载器首次会 instantiate 并挂载到 tipPanel,以后返回已经创建的根节点。 + this.loadDynamicPopup("gameplayIntro", "gameplay_intro", "prefab/GameplayIntro", this.tipPanel, (error, root) => { + // 无论成功失败都结束加载状态,防止暂停检测一直误认为还在加载。 + this.finishDynamicPopupLoad("gameplayIntro"); + if (error || !root) { + cc.error("GameplayIntro prefab failed to load", error); + resume(); + return; + } + const panel = root.getComponent("GameplayIntro") as GameplayIntro; + if (!panel) { + cc.error("GameplayIntro prefab is missing its controller"); + resume(); + return; + } + // 同一实例只绑定一次关闭回调,避免重开多次后一次关闭触发多次恢复。 + if (this.gameplayIntro !== panel) { + this.gameplayIntro = panel; + root.on("intro-closed", resume); + } + panel.open(this.levelGameplay); + }); } startGame() { diff --git a/assets/Script/module/Tool/LevelGameplay.ts b/assets/Script/module/Tool/LevelGameplay.ts new file mode 100644 index 0000000..46cd9d2 --- /dev/null +++ b/assets/Script/module/Tool/LevelGameplay.ts @@ -0,0 +1,74 @@ +/** 从实际加载的关卡筛选介绍,不使用玩家进度或首次解锁关卡。 */ +export function getLevelGameplay(config: any[], data: any): any[] { + const level = data.LEVEL_INFO && data.LEVEL_INFO[0] || {}; + const blocks = (data.BLOCK_INFO && data.BLOCK_INFO[0] || []).slice(); + const walls = data.WALL_INFO && data.WALL_INFO[0] || []; + const names = new Set(); + const add = (id: number) => names.add("daoju" + id); + + // 生成区域里尚未出场的方块也属于本关;不扫描会随消除而变化的场景节点。 + const gates = level.spawnGates || []; + if (gates.length) add(33); + gates.forEach(gate => (gate.spawnQueue || []).forEach(item => { + blocks.push(...(item.members || [])); + })); + + blocks.forEach(block => { + if (!block || block.block == null || Number(block.block) === 23) return; + const type = Number(block.type); + switch (type) { + case 1: case 10: add(block.colorChange != null ? 21 : 1); break; + case 2: case 3: case 12: case 13: add(4); break; + case 4: add(2); break; + case 5: add(3); break; + case 6: add(7); break; + case 7: case 8: add(Number(block.lockTime) > 0 ? 31 : 0); break; + case 9: case 19: add(Number(block.lockTime) > 0 ? 17 : 5); break; + case 11: add(11); break; + case 14: add(14); break; + case 16: add(16); break; + case 17: add(18); break; + case 20: add(25); break; + case 21: add(22); break; + case 22: add(28); break; + } + if (block.vertical || block.horizontal) add(0); + if (Number(block.color) === 11 || ((type === 1 || type === 10) && Number(block.stacking) === 11)) add(11); + if (block.colorArray != null) add(25); + if (block.lock != null) add(19); // 铁皮的开/合两种状态都需要介绍。 + if (block.flowerSend || block.flowerReceive) add(24); + // 花盆接收块借用 floor 实现碰撞,它的盖板不可见,不额外展示快递盒。 + if (block.floor && block.floorTime && !block.flowerReceive) { + add(block.floorMove === true ? 20 : block.floorMove === false && block.floorColor != null ? 29 : 12); + } + }); + + walls.forEach(wall => { + switch (Number(wall.special)) { + case 1: add(3); break; + case 2: add(6); break; + case 3: add(9); break; + case 4: case 5: add(10); break; // 果酱沿门移动。 + case 6: add(14); break; + case 7: add(23); break; // 箭头挡板。 + case 9: add(27); break; // 伸缩障碍杆。 + case 10: case 11: add(26); break; // 带色挡板移动时覆盖其它门。 + } + if (wall.colorArray) add(13); + if (wall.longAndShort) add(15); + if (wall.toggle) add(30); + }); + (level.risefall || []).forEach(tile => { + if (Object.prototype.hasOwnProperty.call(tile, "vine")) { + if (Number(tile.vine) > 0) add(32); + } else add(tile.unLockFall ? 30 : 8); + }); + + // 沿用介绍的配置顺序,每种玩法只留一页,不因解锁关卡而漏掉实际出现的道具。 + const seen = new Set(); + return (config || []).filter(entry => { + if (!names.has(entry.name) || seen.has(entry.name)) return false; + seen.add(entry.name); + return true; + }); +} diff --git a/assets/Script/module/Tool/LevelGameplay.ts.meta b/assets/Script/module/Tool/LevelGameplay.ts.meta new file mode 100644 index 0000000..40a9525 --- /dev/null +++ b/assets/Script/module/Tool/LevelGameplay.ts.meta @@ -0,0 +1,10 @@ +{ + "ver": "1.1.0", + "uuid": "e1c45ec8-4986-46b4-a622-609cb5e6d1d8", + "importer": "typescript", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/test.meta b/assets/test.meta new file mode 100644 index 0000000..d28cb2c --- /dev/null +++ b/assets/test.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.3", + "uuid": "4d8515b3-3552-48d4-8fdd-3032e2852e88", + "importer": "folder", + "isBundle": true, + "bundleName": "test", + "priority": 1, + "compressionType": {}, + "optimizeHotUpdate": {}, + "inlineSpriteFrames": {}, + "isRemoteBundle": {}, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/test/README.md b/assets/test/README.md new file mode 100644 index 0000000..2da4b4a --- /dev/null +++ b/assets/test/README.md @@ -0,0 +1,48 @@ +# 本关玩法面板测试包(可整体移除) + +本目录只包含测试场景、测试面板、预览服务、回归脚本及对应 .meta,不存放正式美术资源。 + +核对日期:2026-09-15。测试面板直接加载正式 GameplayIntro 预制体。正式组件和素材位于 `assets/GameplayIntro`,资源包名 `gameplay_intro`;玩法筛选工具位于公共目录 `assets/Script/module/Tool/LevelGameplay.ts`。当前 HomeScene 没有 HomePropTestPanel 节点,测试目录也没有 HomePropTestPanel.ts。 + +当前 `settings/project.json` 的启动场景是 TestLayout(UUID `8e21ec7e-3913-44ec-8f99-53ff52fe81ed`),相对已合入当前分支的 `origin/main`(`e992a8f`)已发生变更。不需要测试时,先在 Creator 中切回正式启动场景并保存设置,再删除 `assets/test` 和同级 `assets/test.meta`。 + +正式脚本的方法职责、原有 SceneManager.ts 的逐项改动及完整 main 对比清单见 `assets/GameplayIntro/README.md`。 + +## 使用 + +保持当前项目在 Cocos Creator 2.4.15 中打开,在项目根目录执行: + +```powershell +node assets/test/tests/preview.cjs +``` + +打开 `http://127.0.0.1:7460/`。默认读取本项目 settings/project.json 中的编辑器预览端口;7460 是独立预览入口。也可以传入两个参数指定编辑器端口和测试端口。Node.js 需要 18 或以上。Ctrl+C 停止服务。 + +也可以在 Creator 中打开 `assets/test/scene/TestLayout.fire`,选择预览当前场景。 + +测试面板读取本地 `custom/Json/levelN`,支持右上角问号打开、点击面板外空白关闭、关卡输入、前后翻页、边界回弹、白点替换动画。预设第 1/7/44/511 关分别覆盖无道具、1/2/5 页;没有特殊道具时隐藏问号。只读本地关卡,不修改游戏进度,线上关卡更新后可能与本地样本不同。 + +## 回归测试 + +```powershell +node assets/test/tests/guide-carousel.cjs +``` + +覆盖本关类型筛选、变体匹配、生成队列、去重、零/单/双/多页、分页高亮、滑动回弹、相邻三页内存控制、过期加载回收、关闭重开及素材映射;同时用正式场景节点验证问号显隐、实际关卡输入、加载暂停和关闭恢复。 + +编译器默认从 `C:/ProgramData/cocos/editors/Creator/2.4.15` 读取;其他目录可通过 `COCOS_EDITOR_HOME` 指定。 + +## 残留的 HomeScene 道具测试 + +`tests/home-prop-panel.cjs` 仍保留,但它读取的 `script/HomePropTestPanel.ts` 已不存在,HomeScene 中对应节点也已不存在。因此当前没有该测试入口,这个脚本不能作为可运行的测试命令使用。本次仅更新文档,未删除残留测试文件。 + +## 测试脚本职责 + +| 脚本 | 作用 | +| --- | --- | +| `script/TestLayoutPanel.ts` | 创建测试控件,读取指定本地关卡并筛选介绍;支持全部玩法、问号重开和首尾/前后跳页,不修改玩家进度 | +| `tests/guide-carousel.cjs` | 在模拟 Cocos 环境中验证正式逻辑;不等同于真机视觉及性能验证 | +| `tests/preview.cjs` | 加载 Creator TypeScript 编译器,编译测试所需脚本,并通过本地服务读取编辑器预览引擎和资源 | +| `tests/home-prop-panel.cjs` | 已失去依赖的残留测试,当前不可正常运行 | + +2026-09-15 合入 main(`e992a8f`)后,已执行 `node assets/test/tests/guide-carousel.cjs`,全部回归项通过。本次未启动预览服务或进行真机验证。 diff --git a/assets/test/README.md.meta b/assets/test/README.md.meta new file mode 100644 index 0000000..3950d41 --- /dev/null +++ b/assets/test/README.md.meta @@ -0,0 +1,6 @@ +{ + "ver": "2.0.2", + "uuid": "7ac21c73-64e5-4490-8e88-b7a391bc0158", + "importer": "markdown", + "subMetas": {} +} \ No newline at end of file diff --git a/assets/pause/texture.meta b/assets/test/scene.meta similarity index 81% rename from assets/pause/texture.meta rename to assets/test/scene.meta index 6208662..6d5e7b0 100644 --- a/assets/pause/texture.meta +++ b/assets/test/scene.meta @@ -1,6 +1,6 @@ { "ver": "1.1.3", - "uuid": "b9306325-5a69-49d8-8065-461b7b0e87a8", + "uuid": "dd583467-1f59-4677-b71d-dc0d97d24a55", "importer": "folder", "isBundle": false, "bundleName": "", diff --git a/assets/test/scene/TestLayout.fire b/assets/test/scene/TestLayout.fire new file mode 100644 index 0000000..36a7d3e --- /dev/null +++ b/assets/test/scene/TestLayout.fire @@ -0,0 +1,258 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "TestLayout", + "_objFlags": 0, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": false, + "_components": [], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 0 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_is3DNode": true, + "_groupIndex": 0, + "groupIndex": 0, + "autoReleaseAssets": true, + "_id": "8e21ec7e-3913-44ec-8f99-53ff52fe81ed", + "_name": "TestLayout" + }, + { + "__type__": "cc.Node", + "_name": "Canvas", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + } + ], + "_active": true, + "_components": [ + { + "__id__": 5 + }, + { + "__id__": 6 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1080, + "height": 1920 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 540, + 960, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "a5esZu+45LA5mBpvttspPD" + }, + { + "__type__": "cc.Node", + "_name": "Main Camera", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1080, + "height": 1920 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "e1WoFrQ79G7r4ZuQE3HlNb" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_cullingMask": 4294967295, + "_clearFlags": 7, + "_backgroundColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_depth": -1, + "_zoomRatio": 1, + "_targetTexture": null, + "_fov": 60, + "_orthoSize": 10, + "_nearClip": 1, + "_farClip": 4096, + "_ortho": true, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_renderStages": 1, + "_alignWithScreen": true, + "_id": "81GN3uXINKVLeW4+iKSlim" + }, + { + "__type__": "cc.Canvas", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_designResolution": { + "__type__": "cc.Size", + "width": 1080, + "height": 1920 + }, + "_fitWidth": true, + "_fitHeight": true, + "_id": "59Cd0ovbdF4byw5sbjJDx7" + }, + { + "__type__": "47a56/pSx1KrYO40VL/jOFP", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + } +] diff --git a/assets/test/scene/TestLayout.fire.meta b/assets/test/scene/TestLayout.fire.meta new file mode 100644 index 0000000..bbff55f --- /dev/null +++ b/assets/test/scene/TestLayout.fire.meta @@ -0,0 +1,8 @@ +{ + "ver": "1.3.2", + "uuid": "8e21ec7e-3913-44ec-8f99-53ff52fe81ed", + "importer": "scene", + "asyncLoadAssets": false, + "autoReleaseAssets": true, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/test/script.meta b/assets/test/script.meta new file mode 100644 index 0000000..55b4a66 --- /dev/null +++ b/assets/test/script.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.3", + "uuid": "426e063c-84c4-48c4-b73a-d999e2643c13", + "importer": "folder", + "isBundle": false, + "bundleName": "", + "priority": 1, + "compressionType": {}, + "optimizeHotUpdate": {}, + "inlineSpriteFrames": {}, + "isRemoteBundle": {}, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/test/script/TestLayoutPanel.ts b/assets/test/script/TestLayoutPanel.ts new file mode 100644 index 0000000..8d157a2 --- /dev/null +++ b/assets/test/script/TestLayoutPanel.ts @@ -0,0 +1,219 @@ +import type GameplayIntro from "../../GameplayIntro/script/GameplayIntro"; +import { getLevelGameplay } from "../../Script/module/Tool/LevelGameplay"; +const { ccclass } = cc._decorator; + +/** 独立测试面板:直接加载正式 GameplayIntro 预制体,不运行登录/对局,也不写入存档。 */ +@ccclass +export default class TestLayoutPanel extends cc.Component { + private config: any[] = []; + // 配置准备好才能打开;loading 防止资源未返回时重复创建多个面板。 + private ready = false; + private loading = false; + private popup: cc.Node = null; + private controller: any = null; + private stage: cc.Node = null; + private status: cc.Label = null; + private levelInput: cc.EditBox = null; + private level = 511; + private allGameplay = false; + private entries: any[] = []; + private levelRequest = 0; + private infoButton: cc.Node = null; + + /** 创建测试按钮、关卡输入框和预览区域;这些辅助控件都属于可删除的 test 包。 */ + onLoad() { + this.node.color = cc.Color.WHITE; + this.box(this.node, "Background", 0, 0, 1080, 2400, new cc.Color(27, 32, 43)); + this.label(this.node, "本关玩法 · 问号按钮测试", 0, 867, 42); + this.label(this.node, "按关卡检查,或查看全部玩法 / 标题、道具、说明、分页", 0, 811, 25); + this.stage = new cc.Node("PreviewStage"); + this.stage.parent = this.node; + this.stage.setPosition(0, 25); + this.stage.setContentSize(1080, 1600); + this.stage.scale = 0.78; + // 测试控件显示在遮罩上方,方便面板打开时继续切换测试关卡。 + this.infoButton = this.createInfoButton(this.node, 440, 840, () => this.reopen()); + this.infoButton.active = false; + const controls = new cc.Node("Controls"); + controls.parent = this.node; + controls.zIndex = 10; + // 读取真实关卡,分别验证零项、单项、双项和多项。 + [1, 7, 44, 511].forEach((level, index) => { + this.button(controls, level === 1 ? "无道具 · 1关" : level === 7 ? "1页 · 7关" : level === 44 ? "2页 · 44关" : "5页 · 511关", + -375 + index * 250, 735, 235, () => this.open(level)); + }); + const input = this.box(controls, "LevelInput", -375, 660, 205, 58, new cc.Color(65, 75, 92)); + input.active = false; + this.levelInput = input.addComponent(cc.EditBox); + this.levelInput.textLabel = this.label(input, "511", 0, 0, 28); + this.levelInput.placeholderLabel = this.label(input, "输入关卡", 0, 0, 26); + [this.levelInput.textLabel, this.levelInput.placeholderLabel].forEach(label => { + label.node.setContentSize(185, 58); + label.node.setPosition(-92.5, 29); + }); + this.levelInput.fontSize = 28; + this.levelInput.fontColor = cc.Color.WHITE; + this.levelInput.inputMode = cc.EditBox.InputMode.NUMERIC; + this.levelInput.string = "511"; + input.active = true; + this.button(controls, "按关卡打开", -115, 660, 230, () => { + const level = Number(this.levelInput.string); + if (!Number.isInteger(level) || level < 1) { this.status.string = "请输入大于或等于 1 的整数关卡"; return; } + this.open(level); + }); + this.button(controls, "重新打开", 140, 660, 230, () => this.reopen()); + this.button(controls, "全部玩法", 395, 660, 230, () => this.openAllGameplay()); + this.status = this.label(controls, "正在加载正式弹窗和介绍资源…", 0, -635, 28); + this.status.node.width = 1020; + this.status.overflow = cc.Label.Overflow.RESIZE_HEIGHT; + ["第一页", "上一页", "下一页", "最后页"].forEach((title, index) => { + const actions = ["first", "previous", "next", "last"]; + this.button(controls, title, -375 + index * 250, -730, 230, () => { + if (this.popup && this.popup.active && this.controller) this.controller.previewNavigate(actions[index]); + }); + }); + this.label(controls, "无道具隐藏问号,单页不可滑动,打开先看最左端最新玩法", 0, -812, 25); + this.label(controls, "点击面板外空白处关闭;读取本地关卡,不修改游戏进度", 0, -856, 25); + // 只读取正式解锁配置,输入的测试关卡保存在本组件,不修改玩家实际进度。 + cc.resources.load("Json/NEW_LEVEL", cc.JsonAsset, (error, asset: cc.JsonAsset) => { + if (!cc.isValid(this, true)) return; + if (error) { this.status.string = "解锁配置加载失败:" + error.message; return; } + this.config = asset.json.NEW_LEVEL; + this.ready = true; + this.open(this.level); + + }); + } + + /** 用指定测试关卡打开正式预制体;已有面板直接复用,否则异步加载并实例化。 */ + private open(level: number) { + if (!this.ready) return; + this.allGameplay = false; + this.level = level; + this.levelInput.string = String(level); + this.infoButton.active = false; + if (this.popup && this.popup.active) this.controller.close(); + this.entries = []; + this.status.string = "正在读取第 " + level + " 关…"; + const token = ++this.levelRequest; + cc.assetManager.loadBundle("custom", (error, bundle) => { + if (!cc.isValid(this, true) || token !== this.levelRequest) return; + if (error) { this.status.string = "关卡包加载失败:" + error.message; return; } + bundle.load("Json/level" + level, cc.JsonAsset, (loadError, asset: cc.JsonAsset) => { + if (!cc.isValid(this, true) || token !== this.levelRequest) return; + if (loadError) { this.status.string = "本地关卡加载失败:" + loadError.message; return; } + this.entries = getLevelGameplay(this.config, asset.json); + this.infoButton.active = this.entries.length > 0; + if (!this.entries.length) { this.status.string = "本关没有特殊道具,问号已隐藏"; return; } + this.showEntries(); + }); + }); + } + + /** 策划验图入口:不按关卡过滤,仍使用正式面板按相邻三页加载资源。 */ + private openAllGameplay() { + if (!this.ready) return; + ++this.levelRequest; // 丢弃切换前尚未返回的关卡请求。 + this.allGameplay = true; + const seen = new Set(); + this.entries = this.config.filter(entry => { + if (seen.has(entry.name)) return false; + seen.add(entry.name); + return true; + }); + this.infoButton.active = this.entries.length > 0; + this.showEntries(); + } + + private reopen() { + if (this.allGameplay) this.openAllGameplay(); + else this.open(this.level); + } + + private showEntries() { + if (this.popup) { + this.controller.open(this.entries); + return; + } + if (this.loading) return; + this.loading = true; + cc.assetManager.loadBundle("gameplay_intro", (error, bundle) => { + if (!cc.isValid(this, true)) return; + if (error) { this.loading = false; this.status.string = "功能包加载失败:" + error.message; return; } + bundle.load("prefab/GameplayIntro", cc.Prefab, (prefabError, prefab: cc.Prefab) => { + if (!cc.isValid(this, true)) return; + this.loading = false; + if (prefabError) { this.status.string = "面板预制体加载失败:" + prefabError.message; return; } + // 测试挂在 stage 下;正式游戏由 SceneManager 挂在场景 tipPanel 下。 + this.popup = cc.instantiate(prefab); + this.popup.parent = this.stage; + this.popup.active = false; + this.controller = this.popup.getComponent("GameplayIntro") as GameplayIntro; + this.popup.on("guide-page-changed", state => { + this.status.string = (this.allGameplay ? "全部玩法" : "测试关卡 " + this.level) + " | " + (state.index + 1) + " / " + state.total + " 页\n" + + (state.entry ? state.entry.name + " · " + state.entry.tips : "暂无玩法"); + }); + this.popup.on("intro-closed", () => this.status.string = this.entries.length ? "面板已关闭,点击右上角问号重新查看" : "本关没有特殊道具,问号已隐藏"); + if (this.entries.length) this.controller.open(this.entries); + }); + }); + } + + // 仅测试场景用代码创建问号;正式游戏入口应由 GameScene 中的 Button 节点提供。 + private createInfoButton(parent: cc.Node, x: number, y: number, open: () => void): cc.Node { + const node = new cc.Node("GameplayInfoButton"); + node.parent = parent; + node.setPosition(x, y); + node.setContentSize(88, 88); + const sprite = node.addComponent(cc.Sprite); + sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; + cc.assetManager.loadBundle("gameplay_intro", (error, bundle) => { + if (!cc.isValid(node, true)) return; + if (error) { cc.error("History button bundle", error); return; } + bundle.load("Texture/wenhao", cc.SpriteFrame, (imageError, frame: cc.SpriteFrame) => { + if (!cc.isValid(node, true)) return; + if (imageError) { cc.error("History button image", imageError); return; } + sprite.spriteFrame = frame; + const size = frame.getOriginalSize(); + const scale = Math.min(88 / size.width, 88 / size.height); + node.setContentSize(size.width * scale, size.height * scale); + }); + }); + node.addComponent(cc.Button); + node.on("click", open); + return node; + } + + + // 以下三个工具方法用于绘制测试控件的底色、文字和按钮。 + private box(parent: cc.Node, name: string, x: number, y: number, width: number, height: number, color: cc.Color) { + const node = new cc.Node(name); + node.parent = parent; + node.setPosition(x, y); + node.setContentSize(width, height); + const graphics = node.addComponent(cc.Graphics); + graphics.fillColor = color; + graphics.roundRect(-width / 2, -height / 2, width, height, 12); + graphics.fill(); + return node; + } + + private label(parent: cc.Node, text: string, x: number, y: number, size: number) { + const node = new cc.Node("Label"); + node.parent = parent; + node.setPosition(x, y); + const label = node.addComponent(cc.Label); + label.string = text; + label.fontSize = size; + label.lineHeight = size + 10; + label.horizontalAlign = cc.Label.HorizontalAlign.CENTER; + return label; + } + + private button(parent: cc.Node, text: string, x: number, y: number, width: number, action: () => void) { + const node = this.box(parent, text, x, y, width, 58, new cc.Color(55, 91, 143)); + this.label(node, text, 0, 0, 26); + node.addComponent(cc.Button); + node.on("click", action, this); + } +} diff --git a/assets/test/script/TestLayoutPanel.ts.meta b/assets/test/script/TestLayoutPanel.ts.meta new file mode 100644 index 0000000..bf29f41 --- /dev/null +++ b/assets/test/script/TestLayoutPanel.ts.meta @@ -0,0 +1,10 @@ +{ + "ver": "1.1.0", + "uuid": "47a56fe9-4b1d-4aad-83b8-d152ff8ce14f", + "importer": "typescript", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/test/tests.meta b/assets/test/tests.meta new file mode 100644 index 0000000..aba7049 --- /dev/null +++ b/assets/test/tests.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.3", + "uuid": "e3931be8-ad66-48ed-8ba8-66ae216a6bf6", + "importer": "folder", + "isBundle": false, + "bundleName": "", + "priority": 1, + "compressionType": {}, + "optimizeHotUpdate": {}, + "inlineSpriteFrames": {}, + "isRemoteBundle": {}, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/test/tests/guide-carousel.cjs b/assets/test/tests/guide-carousel.cjs new file mode 100644 index 0000000..1acdde8 --- /dev/null +++ b/assets/test/tests/guide-carousel.cjs @@ -0,0 +1,407 @@ +// Usage (project root): node assets/test/tests/guide-carousel.cjs +const fs = require('fs'); +const vm = require('vm'); +const path = require('path'); +const assert = require('assert'); +const ts = process.argv[2] ? require(path.resolve(process.argv[2])) : require("./preview.cjs").loadTypeScript(); +class Vec { constructor(x = 0, y = 0) { this.x = x; this.y = y; } clone() { return new Vec(this.x, this.y); } } +class Node { + constructor(name) { this.name = name; this.children = []; this.position = new Vec(); this.scale = 1; this.active = true; this.components = new Map(); this.events = {}; } + set parent(node) { this._parent = node; if (node) node.children.push(this); } + get parent() { return this._parent; } + set x(x) { this.position.x = x; } get x() { return this.position.x; } + set y(y) { this.position.y = y; } get y() { return this.position.y; } + setPosition(x, y) { this.position = new Vec(x, y); } + setContentSize(w, h) { this.width = w; this.height = h; } + addComponent(Type) { const component = new Type(); component.node = this; this.components.set(Type, component); return component; } + getComponent(Type) { return typeof Type === "string" ? Array.from(this.components.values()).find(c => c.constructor.name === Type) : this.components.get(Type); } + getChildByName(name) { return this.children.find(node => node.name === name); } + on(event, handler, target) { this.events[event] = handler.bind(target); this.events[event].target = target; } + emit(event, arg) { if (this.events[event]) this.events[event](arg); } + removeAllChildren() { this.children = []; } + removeFromParent() { if (this.parent) this.parent.children = this.parent.children.filter(n => n !== this); this._parent = null; } + targetOff(target) { Object.keys(this.events).forEach(e => { if (this.events[e].target === target) delete this.events[e]; }); } + destroy() {} + convertToNodeSpaceAR(pos) { return pos.clone(); } +} +Node.EventType = Object.fromEntries(['TOUCH_START','TOUCH_MOVE','TOUCH_END','TOUCH_CANCEL','MOUSE_DOWN','MOUSE_MOVE','MOUSE_UP','MOUSE_LEAVE'].map(x => [x, x])); +class Mask {} Mask.Type = { RECT: 0 }; +class Graphics { rect() {} circle() {} fill() {} stroke() {} roundRect() {} } +class Sprite {} Sprite.Type = { SLICED: 1 }; Sprite.SizeMode = { CUSTOM: 0 }; class Label {} Label.HorizontalAlign = { CENTER: 0 }; class Color {} Color.WHITE = {}; +const config = JSON.parse(fs.readFileSync('assets/resources/Json/NEW_LEVEL.json', 'utf8')).NEW_LEVEL; +let now = 1000; +let deferImages = false; +const pendingImages = []; +const imageLoads = []; +const frames = []; +const cc = { Node, Mask, Graphics, Sprite, Label, Color, Button: class {}, BlockInputEvents: class {}, SpriteFrame: class {}, Component: class {}, + _decorator: { ccclass: Type => Type, property: (...args) => args.length > 1 ? undefined : () => {} }, + fx: { GameConfig: { NEW_LEVEL: config, GM_INFO: { level: 510 } } }, isValid: () => true, + assetManager: { loadBundle(_name, cb) { cb(null, { load(_path, _type, callback) { const frame = { refs: 0, texture: {}, getTexture() { return this.texture; }, addRef() { this.refs++; return this; }, decRef() { this.refs--; return this; }, getOriginalSize: () => ({width:400,height:200}) }; + frames.push(frame); imageLoads.push(_path); + if (deferImages && !_path.endsWith('/mianba')) pendingImages.push(() => callback(null, frame)); else callback(null, frame); } }); } }, + instantiate(asset) { return instantiatePrefab(asset); } +}; +cc.assetManager.loadBundle("resources", (_error, bundle) => cc.resources = bundle); +let deferTweens = false; +const tweens = []; +cc.tween = target => { + const tween = { stopped: false, steps: [], + to(duration, props, options) { this.steps.push(() => Object.assign(target, props)); return this; }, + call(callback) { this.steps.push(callback); return this; }, + stop() { this.stopped = true; }, + finish() { for (const step of this.steps) { if (this.stopped) break; step(); } }, + start() { tweens.push(this); if (!deferTweens) this.finish(); return this; } + }; + return tween; +}; +const modules = {}; +for (const name of ['LevelGameplay', 'GuideCarousel', 'GameplayIntro']) { + const source = fs.readFileSync(name === 'LevelGameplay' ? 'assets/Script/module/Tool/LevelGameplay.ts' : 'assets/GameplayIntro/script/' + name + '.ts', 'utf8'); + const result = ts.transpileModule(source, { compilerOptions: { experimentalDecorators: true, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 } }); + const exports = {}; + vm.runInNewContext(result.outputText, { cc, exports, require: request => modules[request.slice(2)], Date: { now: () => now } }); + modules[name] = exports; +} +function entriesForLevel(level) { + const data = JSON.parse(fs.readFileSync('assets/custom/Json/level' + level + '.json', 'utf8')); + return modules.LevelGameplay.getLevelGameplay(config, data); +} +function instantiatePrefab(data) { + const objects = data.map(o => o.__type__ === 'cc.Node' ? new Node(o._name) : null); + data.forEach((o, i) => { + const node = objects[i]; + if (!node) return; + node.setContentSize(o._contentSize.width, o._contentSize.height); + node.setPosition(o._trs.array[0], o._trs.array[1]); + node.scale = o._trs.array[7]; + if (o._parent) node.parent = objects[o._parent.__id__]; + o._components.forEach(ref => { + const c = data[ref.__id__]; + const Type = c.__type__.startsWith('cc.') ? cc[c.__type__.slice(3)] : + c.__type__.startsWith('b6043') ? modules.GameplayIntro.default : modules.GuideCarousel.default; + const instance = node.addComponent(Type); + if (c.dotFrame) instance.dotFrame = c.dotFrame.__uuid__; + if (c.selectedDotFrame) instance.selectedDotFrame = c.selectedDotFrame.__uuid__; + }); + }); + return objects[data[0].data.__id__]; +} +function setup(entries) { + const popup = instantiatePrefab(JSON.parse(fs.readFileSync('assets/GameplayIntro/prefab/GameplayIntro.prefab', 'utf8'))); + const component = popup.getComponent(modules.GameplayIntro.default); + component.open(typeof entries === 'number' ? entriesForLevel(entries) : entries); + return { component, carousel: component.carousel, popup }; +} +function event(x, id = 1, button = 0) { return { getID: () => id, getButton: () => button, getLocation: () => new Vec(x), stopPropagation() {} }; } +function drag(carousel, delta, touch = false) { + carousel[touch ? 'touchStart' : 'mouseDown'](event(0)); now += 250; + carousel[touch ? 'touchMove' : 'mouseMove'](event(delta)); now += 150; + carousel[touch ? 'touchEnd' : 'mouseUp'](event(delta)); carousel.update(0.4); +} +const names = entries => Array.from(entries, entry => entry.name); +assert.deepEqual(names(entriesForLevel(1)), []); +assert.deepEqual(names(entriesForLevel(7)), ['daoju0']); +assert.deepEqual(names(entriesForLevel(15)), ['daoju1']); +assert.deepEqual(names(entriesForLevel(44)), ['daoju3', 'daoju4']); +assert.deepEqual(names(entriesForLevel(511)), ['daoju0', 'daoju2', 'daoju5', 'daoju11', 'daoju13']); +// These two introduction assets describe different mechanics despite their unlock-table order. +assert(names(entriesForLevel(1354)).includes('daoju27')); // extending rod +assert(!names(entriesForLevel(1354)).includes('daoju26')); +assert(names(entriesForLevel(1398)).includes('daoju26')); // moving colored gate +assert(!names(entriesForLevel(1398)).includes('daoju27')); +assert(names(entriesForLevel(1881)).includes('daoju32')); +assert(!names(entriesForLevel(1923)).includes('daoju33')); // this local fixture has no spawn gates +const filter = modules.LevelGameplay.getLevelGameplay; +const data = (blocks = [], walls = [], level = {}) => ({ BLOCK_INFO: [blocks], WALL_INFO: [walls], LEVEL_INFO: [level] }); +const block = fields => ({ block: 0, color: 1, type: 0, ...fields }); +// Distinct variants must not add an unrelated basic explanation for the same object. +const cases = [ + [block({ type: 7 }), 'daoju0'], [block({ type: 1, stacking: 2 }), 'daoju1'], + [block({ type: 4 }), 'daoju2'], [block({ type: 5 }), 'daoju3'], + [block({ type: 2 }), 'daoju4'], [block({ type: 9 }), 'daoju5'], + [block({ type: 6 }), 'daoju7'], [block({ color: 11 }), 'daoju11'], + [block({ floor: 1, floorTime: 3 }), 'daoju12'], [block({ type: 14 }), 'daoju14'], + [block({ type: 16 }), 'daoju16'], [block({ type: 9, lockTime: 3 }), 'daoju17'], + [block({ type: 17 }), 'daoju18'], [block({ lock: false }), 'daoju19'], + [block({ floor: 1, floorTime: 3, floorMove: true }), 'daoju20'], + [block({ type: 1, colorChange: true }), 'daoju21'], [block({ type: 21 }), 'daoju22'], + [block({ flowerReceive: 33, floor: 300, floorTime: 3000, floorMove: true }), 'daoju24'], + [block({ colorArray: '12' }), 'daoju25'], [block({ type: 22 }), 'daoju28'], + [block({ floor: 1, floorTime: 3, floorMove: false, floorColor: 2 }), 'daoju29'], + [block({ type: 8, lockTime: 3 }), 'daoju31'] +]; +for (const [definition, expected] of cases) { + assert.deepEqual(names(filter(config, data([definition]))), [expected], JSON.stringify(definition)); +} +for (const [definition, expected] of [ + [{ special: 2 }, 'daoju6'], [{ special: 3 }, 'daoju9'], [{ special: 4 }, 'daoju10'], + [{ colorArray: '12' }, 'daoju13'], [{ special: 6 }, 'daoju14'], [{ longAndShort: 12 }, 'daoju15'], + [{ special: 7 }, 'daoju23'], [{ special: 10 }, 'daoju26'], [{ special: 9 }, 'daoju27'], [{ toggle: 2 }, 'daoju30'] +]) assert.deepEqual(names(filter(config, data([], [definition]))), [expected]); +assert.deepEqual(names(filter(config, data([], [], { risefall: [{ color: 2 }] }))), ['daoju8']); +assert.deepEqual(names(filter(config, data([], [], { risefall: [{ unLockFall: true }] }))), ['daoju30']); +assert.deepEqual(names(filter(config, data([], [], { risefall: [{ vine: 0 }] }))), []); +assert.deepEqual(names(filter(config, data([], [], { risefall: [{ vine: 1 }] }))), ['daoju32']); +const queued = data([block({ type: 4 })], [], { spawnGates: [{ spawnQueue: [{ members: [block({ type: 4 }), block({ type: 21 })] }] }] }); +const before = JSON.stringify(queued); +assert.deepEqual(names(filter(config, queued)), ['daoju2', 'daoju22', 'daoju33']); +assert.equal(JSON.stringify(queued), before); +assert.deepEqual(names(filter(config.concat(config), queued)), ['daoju2', 'daoju22', 'daoju33']); +console.log('PASS: actual level filtering, variants, walls/tiles, generated blocks, deduplication and unmodified input'); + +let { component, carousel } = setup(511); +assert.equal(component.getPreviewState().total, 5); +assert.equal(component.getPreviewState().index, 0); +assert.equal(component.getPreviewState().entry.name, 'daoju13'); +assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); +assert.equal(carousel.offset, 0); +assert.deepEqual(names(component.introductions), ['daoju13', 'daoju11', 'daoju5', 'daoju2', 'daoju0']); +drag(carousel, -300); assert.equal(carousel.index, 1); assert.equal(component.getPreviewState().entry.name, 'daoju11'); +assert.equal(carousel.dots[1].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); +drag(carousel, 300, true); assert.equal(carousel.index, 0); +carousel.goTo(4); carousel.update(0.4); +carousel.mouseDown(event(0)); now += 100; carousel.mouseMove(event(-500)); +assert(carousel.offset < -4 * 850 && carousel.offset > -4 * 850 - 120); +carousel.mouseUp(event(-500)); carousel.update(0.1); +const caught = carousel.offset; +carousel.mouseDown(event(0)); carousel.mouseMove(event(0)); assert(Math.abs(carousel.offset - caught) < 0.001); +carousel.mouseLeave(); carousel.update(0.4); assert.equal(carousel.offset, -4 * 850); +component.previewNavigate('first'); carousel.update(0.4); assert.equal(carousel.index, 0); +carousel.touchStart(event(0)); now += 100; carousel.touchMove(event(500)); assert(carousel.offset > 0 && carousel.offset < 120); +const offset = carousel.offset; carousel.touchMove(event(900, 2)); assert.equal(carousel.offset, offset); +carousel.touchCancel(event(500)); carousel.update(0.4); assert.equal(carousel.offset, 0); +({ component, carousel } = setup(7)); +assert.equal(carousel.pages.length, 1); assert.equal(carousel.dots.length, 0); +assert.equal(component.board.getChildByName('Pagination').active, false); +assert.equal(Object.keys(carousel.node.events).filter(e => e !== 'page-changed').length, 0); +carousel.mouseDown(event(0)); carousel.mouseMove(event(400)); assert.equal(carousel.offset, 0); +carousel.touchStart(event(0)); carousel.touchMove(event(-400)); assert.equal(carousel.offset, 0); +({ component, carousel } = setup(44)); +assert.equal(carousel.pages.length, 2); assert.equal(carousel.dots.length, 2); +assert.equal(carousel.incomingDot, null); assert.equal(carousel.index, 0); +assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); +assert.deepEqual(Array.from(carousel.dots, dot => dot.x), [-20, 20]); +drag(carousel, -400); assert.equal(carousel.index, 1); assert.equal(carousel.dots[1].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); +drag(carousel, -400); assert.equal(carousel.index, 1); +drag(carousel, 400); assert.equal(carousel.index, 0); assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); +component.close(); component.open(entriesForLevel(511)); +assert.equal(component.getPreviewState().index, 0); +assert.equal(component.carousel.dots[0].getComponent(Sprite).spriteFrame, component.carousel.selectedDotFrame); +assert.equal(component.board.getChildByName('Pagination').active, true); +assert.equal(component.carousel.node.children.length, 1); +component.open(entriesForLevel(7)); assert.equal(component.carousel.dots.length, 0); +assert.equal(component.board.getChildByName('Pagination').children.length, 0); +let closed = false; component.node.on('intro-closed', () => closed = true); +const emptyLoadStart = imageLoads.length; +component.open([]); assert(closed); assert.equal(component.node.active, false); +assert.equal(component.getPreviewState().total, 0); assert.equal(imageLoads.length, emptyLoadStart); +component.previewNavigate('next'); +({ component, carousel } = setup(1)); assert.equal(component.node.active, false); +const source = fs.readFileSync('assets/GameplayIntro/script/GameplayIntro.ts', 'utf8'); +assert(!source.includes('"Texture/"')); +for (let id = 0; id < 34; id++) { + const before = imageLoads.length; + const { component: panel } = setup([{name: 'daoju' + id, level: id}]); + const artId = id === 26 ? 27 : id === 27 ? 26 : id; + assert.deepEqual(imageLoads.slice(before), ['title', 'icon', 'Introduction'].map(prefix => 'Window_Prop/' + prefix + artId)); + for (const resource of imageLoads.slice(before)) assert(fs.existsSync('assets/resources/' + resource + '.png')); + panel.board.getChildByName('CloseButton').emit('click'); + assert.equal(panel.node.active, false); +} +// Image assignment must not override editor-authored dimensions or scale. +for (const id of [0, 21]) { + const { component: panel } = setup([{ name: 'daoju' + id, level: id }]); + const description = panel.loadedPages[0].node.getChildByName('DescriptionImage'); + const authored = JSON.parse(fs.readFileSync('assets/GameplayIntro/prefab/GameplayIntro.prefab', 'utf8')) + .find(n => n._name === 'DescriptionImage'); + assert.equal(description.y, authored._trs.array[1]); + assert.equal(description.scale, authored._trs.array[7]); + assert.equal(description.width, authored._contentSize.width); + assert.equal(panel.loadedPages[0].node.children.length, 3); + panel.close(); +} +const layoutPanel = setup([{ name: 'daoju0', level: 1 }]).component; +layoutPanel.close(); +const descriptionData = layoutPanel.carousel.pages[0].getChildByName('DescriptionImage'); +descriptionData.setContentSize(500, 100); +descriptionData.scale = 0.8; +descriptionData.y = -280; +layoutPanel.open([{ name: 'daoju0', level: 1 }]); +assert.equal(layoutPanel.loadedPages[0].node.getChildByName('DescriptionImage').y, -280); +assert.equal(layoutPanel.loadedPages[0].node.getChildByName('DescriptionImage').scale, 0.8); +assert.equal(descriptionData.width, 500); +assert.equal(descriptionData.height, 100); +layoutPanel.close(); +console.log('PASS: serialized page layout, editor size/position changes and no per-image correction'); + +// The viewport hierarchy is authored once and survives every page switch and reopen. +const fixedPanel = setup(config).component; +const viewport = fixedPanel.board.getChildByName('IntroViewport'); +const collectNodes = node => [node, ...node.children.flatMap(collectNodes)]; +const fixedNodes = collectNodes(viewport); +const track = viewport.getChildByName('Pages'); +assert.equal(track.children.length, 3); +for (let i = 0; i < 34; i++) { + fixedPanel.carousel.goTo(i, false); + assert.deepEqual(collectNodes(viewport), fixedNodes); + assert(Object.keys(fixedPanel.loadedPages).length <= 3); + assert.equal(fixedPanel.carousel.pages[i].active, true); +} +fixedPanel.close(); +assert.deepEqual(collectNodes(viewport), fixedNodes); +fixedPanel.open(entriesForLevel(511)); +assert.deepEqual(collectNodes(viewport), fixedNodes); +fixedPanel.close(); +console.log('PASS: fixed viewport hierarchy across all 34 pages, close and reopen'); + +// Sorting uses unlock levels, not the input order or the numerical artwork IDs. +const shuffled = [config.find(e => e.name === 'daoju21'), config.find(e => e.name === 'daoju22')]; +({ component, carousel } = setup(shuffled)); +assert.equal(component.getPreviewState().entry.name, 'daoju21'); +assert.equal(carousel.index, 0); +assert.deepEqual(names(shuffled), ['daoju21', 'daoju22']); +component.close(); +console.log('PASS: latest unlocked page at left edge, zero/single/two/many pages, reopen/reset, boundaries and 34 asset mappings'); + +// Window residency and actual texture references shrink on navigation/close. +const loadStart = imageLoads.length; +({ component, carousel } = setup(config)); // Stress the renderer with every existing artwork. +assert.equal(Object.keys(component.loadedPages).length, 2); +assert.equal(imageLoads.length - loadStart, 6); +for (let i = 0; i < 12; i++) { + drag(carousel, -300); + assert(Object.keys(component.loadedPages).length <= 3); + assert(Object.keys(component.loadedPages).every(key => Math.abs(Number(key) - carousel.index) <= 1)); +} +const owned = Object.values(component.loadedPages).flatMap(page => page.frames); +assert.equal(owned.length, 9); +component.node.getChildByName('Backdrop').emit('click'); +assert.equal(component.node.active, false); assert(owned.every(frame => frame.refs === 0)); +deferImages = true; +({ component, carousel } = setup(config)); assert.equal(pendingImages.length, 1); +component.previewNavigate('last'); component.close(); +const loadsAtClose = imageLoads.length; +pendingImages.shift()(); +assert.equal(imageLoads.length, loadsAtClose); assert.equal(pendingImages.length, 0); +assert.equal(Object.keys(component.loadedPages).length, 0); assert.equal(frames[frames.length - 1].refs, 0); +deferImages = false; +console.log('PASS: outside close, <=3 resident pages, sequential loads, released references and stale in-flight completion'); + +({ component, carousel } = setup(511)); +carousel.goTo(0, false); +carousel.goTo(1); carousel.update(0.4); carousel.goTo(2); carousel.update(0.4); carousel.goTo(3); +assert.equal(carousel.activeDot, 2); assert.equal(carousel.incomingDot.active, true); +carousel.update(0.1); assert(carousel.dots[2].x < 40); assert(carousel.incomingDot.opacity > 0); +carousel.update(0.3); assert.equal(carousel.incomingDot.active, false); +carousel.goTo(2); carousel.update(0.4); carousel.goTo(1); carousel.update(0.4); carousel.goTo(0); +assert.equal(carousel.dotDirection, 1); +carousel.onDisable(); assert.equal(carousel.incomingDot.active, false); assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); +carousel.goTo(-1); assert.equal(carousel.incomingDot.active, false); +component.close(); +console.log('PASS: edge dot replacement, symmetric motion, disable cleanup and boundary snap'); + +// Closing retains content and pause until the exit tween completes; reopening cancels it. +deferTweens = true; +({ component } = setup(511)); +assert.equal(component.board.scale, 0.3); +tweens[tweens.length - 1].finish(); +assert.equal(component.board.scale, 1); +let closeEvents = 0; +component.node.on('intro-closed', () => closeEvents++); +component.close(); +const exitTween = tweens[tweens.length - 1]; +component.close(); assert.equal(tweens[tweens.length - 1], exitTween); +assert.equal(closeEvents, 0); assert(component.node.active); +assert(Object.keys(component.loadedPages).length > 0); +component.open(entriesForLevel(511)); exitTween.finish(); +assert(component.node.active); assert.equal(closeEvents, 0); +component.close(); tweens[tweens.length - 1].finish(); +assert.equal(component.node.active, false); assert.equal(closeEvents, 1); +assert.equal(Object.keys(component.loadedPages).length, 0); +deferTweens = false; +console.log('PASS: animated close timing, duplicate close guard, reopen cancellation and resource cleanup'); + +// Scene teardown can destroy child nodes before the panel controller. +({ component } = setup(511)); +component.close(); +Object.defineProperty(component.board, 'scale', { set() { throw new Error('scale accessed after child destruction'); } }); +assert.doesNotThrow(() => component.onDestroy()); +({ component } = setup(511)); +const invalid = new Set(); +for (const page of Object.values(component.loadedPages)) { + invalid.add(page.node); + Object.defineProperty(page.node, 'active', { set() { throw new Error('destroyed page accessed'); } }); + for (const sprite of page.sprites) { + invalid.add(sprite); + Object.defineProperty(sprite, 'spriteFrame', { set() { throw new Error('destroyed sprite accessed'); } }); + } +} +const previousIsValid = cc.isValid; +cc.isValid = object => !invalid.has(object); +const teardownFrames = Object.values(component.loadedPages).flatMap(page => page.frames); +assert.doesNotThrow(() => component.onDestroy()); +assert(teardownFrames.every(frame => frame.refs === 0)); +assert.equal(Object.keys(component.loadedPages).length, 0); +cc.isValid = previousIsValid; +console.log('PASS: teardown after child destruction without node writes or leaked image references'); + +// Exercise the real question-entry controller against the serialized game scene. +const sceneData = JSON.parse(fs.readFileSync('assets/Scene/GameScene.fire', 'utf8')); +const sceneNodes = sceneData.map(o => o.__type__ === 'cc.Node' ? new Node(o._name) : null); +sceneData.forEach((o, i) => { if (sceneNodes[i] && o._parent && sceneNodes[o._parent.__id__]) sceneNodes[i].parent = sceneNodes[o._parent.__id__]; }); +const canvas = sceneNodes.find(n => n && n.name === 'Canvas'); +const infoButton = sceneNodes.find(n => n && n.name === 'GameplayInfoButton'); +cc.find = (relative, root) => relative.split('/').reduce((node, part) => node && node.getChildByName(part), root); +cc.macro = { MAX_ZINDEX: 9999 }; +cc.error = () => {}; +let stops = 0, starts = 0, frozen = false; +const map = { gameOver: false, shopPause: false, gameStart: true, pause: false, + stopBoom() { stops++; }, startBoom() { starts++; }, iceTrue: () => frozen }; +const managerExports = {}; +const managerCode = ts.transpileModule(fs.readFileSync('assets/Script/SceneManager.ts', 'utf8'), { + compilerOptions: { experimentalDecorators: true, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 } +}).outputText; +assert(!managerCode.includes("../GameplayIntro/script/GameplayIntro"), "Startup must not require the unloaded introduction bundle"); +vm.runInNewContext(managerCode, { cc, exports: managerExports, require: request => + request === './Map' ? { default: { _instance: map } } : modules[request.split('/').pop()] || {} }); +const manager = new managerExports.default(); +manager.node = canvas; +manager.tipPanel = new Node('tipPanel'); +manager.isPopupRuntimeAlive = () => true; +manager.getDynamicPopup = () => null; +manager.dynamicPopupLoads = {}; +let popupLoads = 0, completeLoad; +manager.loadDynamicPopup = (key, bundle, file, parent, callback) => { + popupLoads++; manager.dynamicPopupLoads[key] = true; completeLoad = callback; +}; +manager.finishDynamicPopupLoad = key => delete manager.dynamicPopupLoads[key]; +function setLevel(level) { Object.assign(cc.fx.GameConfig, JSON.parse(fs.readFileSync('assets/custom/Json/level' + level + '.json', 'utf8'))); manager.refreshGameplayInfo(); } +setLevel(1); assert.equal(infoButton.active, false); +manager.openGameplayIntro(); assert.equal(popupLoads, 0); assert.equal(stops, 0); assert.equal(map.pause, false); +setLevel(511); assert.equal(infoButton.active, true); +cc.fx.GameConfig.GM_INFO.level = 0; // Display follows the loaded map, not player progress. +manager.openGameplayIntro(); assert.equal(popupLoads, 1); assert.equal(map.pause, true); assert.equal(stops, 1); +assert.equal(manager.isPauseOpen(), true); +manager.openGameplayIntro(); assert.equal(popupLoads, 1); +({ component } = setup(511)); component.close(); +completeLoad(null, component.node); +assert.equal(component.getPreviewState().total, 5); assert.equal(component.getPreviewState().index, 0); +assert.equal(map.pause, true); +component.close(); assert.equal(map.pause, false); assert.equal(starts, 1); +manager.openGameplayIntro(); completeLoad(null, component.node); component.close(); assert.equal(starts, 2); +manager.openGameplayIntro(); completeLoad(new Error('load failure')); assert.equal(map.pause, false); +frozen = true; +manager.openGameplayIntro(); completeLoad(null, component.node); component.close(); assert.equal(map.pause, true); +frozen = false; +console.log('PASS: real scene question visibility, empty-entry no-op, actual-map input, loading pause, close/reopen resume and frozen pause'); +// Introduction metadata may finish loading after the game scene enters. +let finishConfig; +cc.resources = { load(_path, _type, callback) { finishConfig = callback; } }; +cc.fx.GameConfig.NEW_LEVEL = null; +manager.refreshGameplayInfo(); assert.equal(infoButton.active, false); +finishConfig(null, { json: { NEW_LEVEL: config } }); assert.equal(infoButton.active, true); +assert.equal(manager.levelGameplay.length, 5); +manager.refreshGameplayInfo(); manager.isPopupRuntimeAlive = () => false; +finishConfig(null, { json: { NEW_LEVEL: config } }); assert.equal(infoButton.active, false); +console.log('PASS: delayed introduction metadata and discarded callback after scene exit'); diff --git a/assets/test/tests/guide-carousel.cjs.meta b/assets/test/tests/guide-carousel.cjs.meta new file mode 100644 index 0000000..5e3cbd2 --- /dev/null +++ b/assets/test/tests/guide-carousel.cjs.meta @@ -0,0 +1,6 @@ +{ + "ver": "1.0.3", + "uuid": "f9ccaa27-0fd1-41ca-9625-537a4e381fac", + "importer": "asset", + "subMetas": {} +} \ No newline at end of file diff --git a/assets/test/tests/home-prop-panel.cjs b/assets/test/tests/home-prop-panel.cjs new file mode 100644 index 0000000..643a7eb --- /dev/null +++ b/assets/test/tests/home-prop-panel.cjs @@ -0,0 +1,32 @@ +// 在项目根目录运行:node assets/test/tests/home-prop-panel.cjs +const fs = require('fs'), vm = require('vm'), assert = require('assert'); +const ts = require('./preview.cjs').loadTypeScript(); +class Node { + constructor(name) { this.name=name;this.children=[];this.components=[];this.events={};this.active=true; } + set parent(n) { this._parent=n;n.children.push(this); } + get parent() { return this._parent; } + setPosition() {} setContentSize() {} + addComponent(Type) { const c=new Type();c.node=this;this.components.push(c);return c; } + on(name,callback,target) { this.events[name]=callback.bind(target); } +} +class Graphics { roundRect() {} fill() {} } +class Label {} Label.HorizontalAlign={CENTER:1}; +let saved={freezeAmount:90,hammerAmount:90,magicAmount:90,other:'preserved'}, writes=0, fail=false; +const info={freezeAmount:2,hammerAmount:3,magicAmount:4}; +const cc={Node,Graphics,Label,Color:class {},Widget:class {},Button:class {},BlockInputEvents:class {},Component:class {}, + _decorator:{ccclass:C=>C},macro:{MAX_ZINDEX:32767},error(){},fx:{GameConfig:{GM_INFO:info},StorageMessage:{ + getStorage(key){assert.equal(key,'prop');return saved;}, + setStorage(key,value){assert.equal(key,'prop');if(fail)throw Error('full');saved=value;writes++;} + }}}; +const code=ts.transpileModule(fs.readFileSync('assets/test/script/HomePropTestPanel.ts','utf8'),{compilerOptions:{experimentalDecorators:true,module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017}}).outputText; +function setup(debug){const exports={};vm.runInNewContext(code,{cc,exports,CC_DEBUG:debug});const c=new exports.default();c.node=new Node('HomePropTestPanel');c.node.parent=new Node('Canvas');c.onLoad();return c;} +const panel=setup(true);assert.equal(writes,0);assert.equal(panel.panel.active,false);panel.open(); +panel.grant(['freezeAmount'],1);assert.equal(info.freezeAmount,3);assert.equal(info.hammerAmount,3);assert.equal(saved.freezeAmount,3);assert.equal(saved.other,'preserved'); +panel.grant(['freezeAmount'],10);assert.equal(info.freezeAmount,13); +panel.grant(['freezeAmount','hammerAmount','magicAmount'],10);assert.equal(info.freezeAmount,23);assert.equal(info.hammerAmount,13);assert.equal(info.magicAmount,14);assert(saved.timestamp>0); +panel.close();panel.open();assert.equal(writes,3);assert(panel.counts[0].string.includes('23')); +fail=true;panel.grant(['freezeAmount'],10);assert.equal(info.freezeAmount,23);assert.equal(writes,3);fail=false; +cc.fx.GameConfig.GM_INFO=null;panel.grant(['freezeAmount'],1);assert.equal(writes,3);assert(panel.message.string.includes('未准备好'));cc.fx.GameConfig.GM_INFO=info; +assert.equal(setup(false).node.children.length,0); +const scene=JSON.parse(fs.readFileSync('assets/Scene/HomeScene.fire','utf8'));assert.equal(scene.filter(n=>n.__type__==='cc.Node'&&n._name==='HomePropTestPanel').length,1); +console.log('PASS: no automatic grant, single/all/repeated grants, local persistence, preserved fields, reopen, storage failure, unready data, release-build exclusion and scene mount.'); diff --git a/assets/test/tests/home-prop-panel.cjs.meta b/assets/test/tests/home-prop-panel.cjs.meta new file mode 100644 index 0000000..51f2278 --- /dev/null +++ b/assets/test/tests/home-prop-panel.cjs.meta @@ -0,0 +1,6 @@ +{ + "ver": "1.0.3", + "uuid": "a0a6b889-301e-46fa-bdee-0cd0374358ae", + "importer": "asset", + "subMetas": {} +} \ No newline at end of file diff --git a/assets/test/tests/preview.cjs b/assets/test/tests/preview.cjs new file mode 100644 index 0000000..da82e4f --- /dev/null +++ b/assets/test/tests/preview.cjs @@ -0,0 +1,94 @@ +// Run from the project root: node assets/test/tests/preview.cjs [editorPort=7457] [port=7460] +// Uses the installed Cocos 2.4.15 compiler and the running editor's engine/assets. +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const Module = require('module'); +const root = path.resolve(__dirname, '../../..'); + +function loadTypeScript() { + const editor = process.env.COCOS_EDITOR_HOME || path.join(process.env.ProgramData || 'C:/ProgramData', 'cocos/editors/Creator/2.4.15'); + const archive = path.join(editor, 'resources/app.asar'); + const fd = fs.openSync(archive, 'r'); + try { + const header = Buffer.alloc(16); + fs.readSync(fd, header, 0, 16, 0); + const json = Buffer.alloc(header.readUInt32LE(12)); + fs.readSync(fd, json, 0, json.length, 16); + const files = JSON.parse(json).files.node_modules.files.typescript.files.lib.files; + const entry = files['typescript.js']; + const code = Buffer.alloc(entry.size); + fs.readSync(fd, code, 0, code.length, 8 + header.readUInt32LE(4) + Number(entry.offset)); + const compiler = new Module(path.join(__dirname, 'cocos-typescript.js'), module); + compiler.filename = path.join(__dirname, 'cocos-typescript.js'); + compiler.paths = module.paths; + compiler._compile(code.toString(), compiler.filename); + return compiler.exports; + } finally { fs.closeSync(fd); } +} + +function compress(uuid) { + if (uuid.length !== 36) return uuid; + const hex = uuid.replace(/-/g, ''); + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + let result = hex.slice(0, 5); + for (let i = 5; i < 32; i += 3) { + const value = parseInt(hex.slice(i, i + 3), 16); + result += chars[value >> 6] + chars[value & 63]; + } + return result; +} + +function projectScripts(ts) { + let result = 'window.__quick_compile_project__={load:function(done){const modules={};\n'; + for (const relative of ['assets/Script/module/Tool/LevelGameplay.ts', 'assets/GameplayIntro/script/GuideCarousel.ts', 'assets/GameplayIntro/script/GameplayIntro.ts', 'assets/test/script/TestLayoutPanel.ts']) { + const file = path.join(root, relative); + const name = path.basename(file, '.ts'); + const uuid = compress(JSON.parse(fs.readFileSync(file + '.meta')).uuid); + const code = ts.transpileModule(fs.readFileSync(file, 'utf8'), { + compilerOptions: { experimentalDecorators: true, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 } + }).outputText; + result += `(function(){const module={exports:{}};const exports=module.exports;const require=p=>modules[p.split('/').pop()];cc._RF.push(module,${JSON.stringify(uuid)},${JSON.stringify(name)});\n${code}\ncc._RF.pop();modules[${JSON.stringify(name)}]=module.exports;})();\n`; + } + return result + 'done();}};'; +} + +function start() { + const ts = loadTypeScript(); + const projectSettings = JSON.parse(fs.readFileSync(path.join(root, 'settings/project.json'), 'utf8')); + const editorPort = Number(process.argv[2] || projectSettings['preview-port'] || 7456); + const port = Number(process.argv[3] || 7460); + http.createServer(async (request, response) => { + try { + const pathname = request.url.split('?')[0]; + response.setHeader('Cache-Control', 'no-store'); + if (pathname === '/preview-scene.json') { + const scene = JSON.parse(fs.readFileSync(path.join(root, 'assets/test/scene/TestLayout.fire'))); + scene[6].__type__ = compress(scene[6].__type__); + response.setHeader('Content-Type', 'application/json'); + return response.end(JSON.stringify(scene)); + } + if (pathname === '/preview-scripts/__quick_compile__.js') { + response.setHeader('Content-Type', 'application/javascript'); + return response.end(projectScripts(ts)); + } + // This server only reads the editor's preview files; editor mutations stay disabled. + if (request.method !== 'GET' || pathname === '/update-db') { response.statusCode = 405; return response.end(); } + const upstream = await fetch('http://127.0.0.1:' + editorPort + request.url); + let body = Buffer.from(await upstream.arrayBuffer()); + response.statusCode = upstream.status; + response.setHeader('Content-Type', upstream.headers.get('content-type') || 'application/octet-stream'); + if (pathname === '/settings.js') body = Buffer.from(body.toString().replace(/jsList:\s*\[[\s\S]*?\]/, 'jsList: []')); + if (pathname === '/') body = Buffer.from(body.toString() + .replace('window.__socket_io__ = window.io;', 'window.__socket_io__ = function(){return {on:function(){}}};') + .replace('', '')); + response.end(body); + } catch (error) { + response.statusCode = 500; + response.end('Test preview failed. Keep this project open in Cocos Creator and check the editor port.\n' + error.message); + } + }).listen(port, '127.0.0.1', () => console.log('TestLayout preview: http://127.0.0.1:' + port)); +} + +module.exports = { loadTypeScript }; +if (require.main === module) start(); diff --git a/assets/test/tests/preview.cjs.meta b/assets/test/tests/preview.cjs.meta new file mode 100644 index 0000000..0678eab --- /dev/null +++ b/assets/test/tests/preview.cjs.meta @@ -0,0 +1,6 @@ +{ + "ver": "1.0.3", + "uuid": "d4af8d70-a4e6-4e47-a4e5-27602d995f1b", + "importer": "asset", + "subMetas": {} +} \ No newline at end of file diff --git a/build-templates/wechatgame/project.config.json b/build-templates/wechatgame/project.config.json index 1a2bdec..52ec70c 100644 --- a/build-templates/wechatgame/project.config.json +++ b/build-templates/wechatgame/project.config.json @@ -34,7 +34,12 @@ }, "isGameTourist": false, "packOptions": { - "ignore": [], + "ignore": [ + { + "type": "folder", + "value": "cocos" + } + ], "include": [] }, "editorSetting": { @@ -42,4 +47,4 @@ "tabSize": 2 }, "simulatorPluginLibVersion": {} -} \ No newline at end of file +} diff --git a/settings/project.json b/settings/project.json index 1778c9f..cb08a2a 100644 --- a/settings/project.json +++ b/settings/project.json @@ -1,5 +1,5 @@ { - "last-module-event-record-time": 1788866731401, + "last-module-event-record-time": 1789116749030, "group-list": [ "default", "Map" @@ -53,7 +53,7 @@ "enable": false } }, - "start-scene": "ea96c58a-b731-4349-bcfc-c446571823ad", + "start-scene": "8e21ec7e-3913-44ec-8f99-53ff52fe81ed", "migrate-history": [ "cloud-function" ], -- 2.43.0 From 194323ff2c0878b5cc34d93c152258bcd5b3e31e Mon Sep 17 00:00:00 2001 From: keke Date: Wed, 16 Sep 2026 12:00:42 +0800 Subject: [PATCH 2/2] chore: remove gameplay preview tools and restore production start scene --- assets/GameplayIntro/README.md | 141 +------ assets/GameplayIntro/script/GameplayIntro.ts | 15 - assets/test.meta | 13 - assets/test/README.md | 48 --- assets/test/README.md.meta | 6 - assets/test/scene.meta | 13 - assets/test/scene/TestLayout.fire | 258 ------------ assets/test/scene/TestLayout.fire.meta | 8 - assets/test/script.meta | 13 - assets/test/script/TestLayoutPanel.ts | 219 ---------- assets/test/script/TestLayoutPanel.ts.meta | 10 - assets/test/tests.meta | 13 - assets/test/tests/guide-carousel.cjs | 407 ------------------- assets/test/tests/guide-carousel.cjs.meta | 6 - assets/test/tests/home-prop-panel.cjs | 32 -- assets/test/tests/home-prop-panel.cjs.meta | 6 - assets/test/tests/preview.cjs | 94 ----- assets/test/tests/preview.cjs.meta | 6 - settings/project.json | 2 +- 19 files changed, 2 insertions(+), 1308 deletions(-) delete mode 100644 assets/test.meta delete mode 100644 assets/test/README.md delete mode 100644 assets/test/README.md.meta delete mode 100644 assets/test/scene.meta delete mode 100644 assets/test/scene/TestLayout.fire delete mode 100644 assets/test/scene/TestLayout.fire.meta delete mode 100644 assets/test/script.meta delete mode 100644 assets/test/script/TestLayoutPanel.ts delete mode 100644 assets/test/script/TestLayoutPanel.ts.meta delete mode 100644 assets/test/tests.meta delete mode 100644 assets/test/tests/guide-carousel.cjs delete mode 100644 assets/test/tests/guide-carousel.cjs.meta delete mode 100644 assets/test/tests/home-prop-panel.cjs delete mode 100644 assets/test/tests/home-prop-panel.cjs.meta delete mode 100644 assets/test/tests/preview.cjs delete mode 100644 assets/test/tests/preview.cjs.meta diff --git a/assets/GameplayIntro/README.md b/assets/GameplayIntro/README.md index 07b9856..47d56d8 100644 --- a/assets/GameplayIntro/README.md +++ b/assets/GameplayIntro/README.md @@ -17,146 +17,7 @@ Asset Bundle 名称:`gameplay_intro`。 预制体直接引用背景、标题、关闭按钮和两种分页圆点;34 种公共玩法图片由脚本按当前页及相邻页动态加载,不能把所有玩法图片拖到预制体中,以免一次加载全部图片。 -测试工具位于 `assets/test`。当前 `settings/project.json` 的启动场景是 `TestLayout`;不需要测试时,先在 Creator 中切回正式启动场景并保存设置,再删除 `assets/test` 和同级 `assets/test.meta`。当前 HomeScene 没有 HomePropTestPanel 节点。 - - - -## 与已拉取 main 的改动对比 - -核对日期:2026-09-15。最新 `origin/main`(`e992a8f`)已快进合入当前 `layout-optimization` 分支。以下以 `e992a8f` 为基准,包含当前工作区的暂存、未暂存和未跟踪文件;本地 `main`(`122b504`)未移动,不作为本清单的对比基准。此清单是本次核对快照,后续代码变更需同步更新。 - -新增业务代码并非全部放在新包:面板与滑动脚本位于 `gameplay_intro`,测试代码位于 `test`,`LevelGameplay.ts` 位于原公共脚本目录。 - -``` -GameplayIntro/ -├─ assets/ -│ ├─ GameplayIntro/ 【新增资源包:gameplay_intro】 -│ │ ├─ script/ -│ │ │ ├─ GameplayIntro.ts 面板展示、图片加载与释放 -│ │ │ └─ GuideCarousel.ts 滑动翻页、分页指示 -│ │ ├─ prefab/ -│ │ │ └─ GameplayIntro.prefab 玩法介绍面板 -│ │ ├─ Texture/ 玩法介绍美术资源 -│ │ └─ README.md -│ │ -│ ├─ test/ 【新增资源包:test】 -│ │ ├─ script/ -│ │ │ └─ TestLayoutPanel.ts 独立测试面板 -│ │ ├─ scene/ -│ │ │ └─ TestLayout.fire 测试场景 -│ │ ├─ tests/ -│ │ │ ├─ guide-carousel.cjs -│ │ │ ├─ home-prop-panel.cjs 残留测试,依赖的脚本已不存在 -│ │ │ └─ preview.cjs -│ │ └─ README.md -│ │ -│ ├─ Script/ 【原有脚本目录】 -│ │ ├─ module/Tool/ -│ │ │ └─ LevelGameplay.ts 【新增】当前关卡玩法筛选 -│ │ ├─ SceneManager.ts 【修改】入口、加载、暂停与恢复 -│ │ └─ ItemGuide.ts 无实际内容差异 -│ │ -│ ├─ NewMode/script/ -│ │ └─ NewMode.ts 无实际内容差异 -│ │ -│ ├─ Scene/ -│ │ ├─ GameScene.fire 【修改】问号按钮及面板接线 -│ │ └─ HomeScene.fire 无实际内容差异 -│ │ -│ ├─ gacha_bundle/img/cat.meta 【删除】 -│ ├─ libs/dn-sdk-minigame.meta 【删除】 -│ ├─ passCheck/script.meta 【删除】 -│ └─ pause/texture.meta 【删除】 -│ -├─ build-templates/wechatgame/ -│ └─ project.config.json 【修改】上传时忽略 cocos 目录 -│ -└─ settings/ - └─ project.json 【修改】启动场景、记录时间 -``` - -新增资源对应的 `.meta` 文件未逐一展开。**原有脚本实际只改了 `SceneManager.ts`;新增的 `LevelGameplay.ts` 位于公共脚本目录。两个新资源包尚未配置为微信独立分包。** - -`assets/custom/Json/level1961.json.meta` 至 `level1980.json.meta` 共 20 个关卡元数据文件现已包含在 main 基准中,合并时采用 main 的 UUID;未发现资源对旧本地 UUID 的引用。`.codex_tmp/prop-compare.jpg` 和 `.codex_tmp/texture-compare.jpg` 是本地对比图片,不是业务代码。`project.json`、`settings/services.json`、`settings/wechatgame.json` 相对上述基准没有实际文本内容差异。 - -本次合入 main 的 3 个提交包含:GameScene 点击拦截组件、门生方块介绍关卡改为 1943、AB 测试 B 方案第 15 关不返回首页及版本配置更新、战令图片更新。场景合并保留了 main 的拦截组件及本地问号按钮、坐标和面板接线;这些 main 自带改动不计入本地新增功能差异。 - -### 新增正式脚本及方法职责 - -整体调用关系:`SceneManager` 控制入口和暂停恢复 → `LevelGameplay` 筛选本关玩法 → `GameplayIntro` 管理内容与图片 → `GuideCarousel` 处理滑动分页。 - -#### LevelGameplay.ts(公共目录) - -| 方法 | 作用 | -| --- | --- | -| `getLevelGameplay(config, data)` | 输入全部介绍配置和当前关卡数据,扫描方块、门、特殊地块及生成队列,返回本关介绍列表;按名称去重,保留配置顺序 | -| 内部 `add(id)` | 将编号转换为 `daojuN` 存入 Set,重复玩法只记录一次 | - -该工具只读取数据。方块列表使用浅复制后再补充待生成方块,不向原数组追加内容。根据方块类型及附加属性区分普通/定次、木板/快递盒/彩色箱子等变体;花盆接收块借用的不可见盖板不额外计入箱子介绍。它不使用玩家进度或配置中的 `level` 筛选,也不负责显示、加载图片或暂停游戏;最终只返回在介绍配置中存在的条目。 - -#### GameplayIntro.ts(gameplay_intro 包) - -挂在正式预制体根节点,负责内容、开关动画和图片资源。 - -| 方法 | 作用 | -| --- | --- | -| `open(entries)` | 复制列表并按 `level` 倒序排列,绑定固定节点、初始化分页,播放打开动画;空列表直接关闭 | -| `syncPages()` | 只保留当前页和前后相邻页的图片,优先加载当前页 | -| `picture(...)` | 排队加载公共标题、道具或说明图;标题和道具图等比适配,说明图保持预制体设置;丢弃失效页面的加载结果 | -| `nextImage()` | 依次启动图片加载任务,避免集中加载 | -| `releasePage(index)` / `releasePages()` | 清空图片显示引用、归还资源引用、隐藏页面;批量清理时同时清空待加载队列,固定节点保留复用 | -| `close()` / `finishClose()` | 播放关闭动画后释放图片、隐藏面板并发出 `intro-closed`,由场景判断是否恢复游戏 | -| `stopPanelAnimation()` | 停止开关动画并重置状态 | -| `onDisable()` / `onDestroy()` | 隐藏或销毁时清理图片和动画 | -| `previewNavigate(action)` / `getPreviewState()` | 提供测试跳页入口,以及当前页、总页数和当前配置 | -| `showMessage(value)` | 使用预制体的 Status 节点显示提示 | - -#### GuideCarousel.ts(gameplay_intro 包) - -挂在 `IntroViewport`,只负责手势、页面位置和圆点动画。逻辑页循环使用预制体中的三个固定页面节点。 - -| 方法 | 作用 | -| --- | --- | -| `initialize(...)` / `clear()` | 初始化页数、默认页、圆点及输入监听;重复打开前清理旧状态 | -| `goTo(index, animated)` | 限制目标页范围,更新分页高亮,发出 `page-changed` 通知内容加载 | -| `begin(...)` / `move(...)` / `end(...)` | 开始拖动、跟手移动、根据距离和速度决定翻页或回弹;首尾越界有阻力 | -| `x(event)` | 将屏幕坐标转为视口局部坐标,适配面板缩放 | -| `touchStart/Move/End/Cancel` | 将带手指 ID 的触摸事件转给拖动逻辑 | -| `mouseDown/Move/Up/Leave` | 处理鼠标左键拖动与离开视口结束拖动 | -| `createDot()` / `styleDot()` | 创建圆点并设置选中样式 | -| `animateDots()` / `resetDotAnimation()` | 推进或重置圆点替换动画 | -| `render()` / `update(dt)` | 应用页面位移、缩放和透明度,每帧推进吸附、回弹及圆点动画 | -| `onDisable()` | 结束手势和动画,避免重开后残留中间状态 | - -### 原有 SceneManager.ts 的具体改动 - -| 改动位置 | 改动及作用 | -| --- | --- | -| 文件顶部 | 引入 `getLevelGameplay`;以 `import type` 引用 GameplayIntro,不产生启动时的运行时导入 | -| 新增字段 | `tipPanel` 绑定场景容器;`gameplayIntro` 缓存面板控制器;`levelGameplay` 保存本关介绍列表 | -| `isPauseOpen()` | 将介绍面板加载中、显示中计入暂停判断,防止重复打开及原有流程提前恢复游戏 | -| `onLoad()` | 增加 `refreshGameplayInfo()` 调用,进场景时初始化列表和问号显隐 | -| 新增 `refreshGameplayInfo()` | 使用已有 NEW_LEVEL 配置,未就绪则异步加载;按实际关卡筛选并缓存列表,没有介绍时隐藏问号 | -| 新增 `openGameplayIntro()` | 检查场景、暂停、结束、商城和容器状态;暂停游戏并停止炸弹计时;通过原有动态弹窗加载器加载或复用正式面板,再传入列表 | -| 内部 `resume()` 回调 | 关闭或加载失败时,仅在无冰冻、无其他暂停、无商城暂停且游戏未结束时恢复游戏;游戏已开始才恢复炸弹计时 | -| 加载完成回调 | 结束加载标记;失败时尝试恢复;成功时按组件名取得面板,同一实例只绑定一次关闭回调 | -| 原空 `start()` | 删除无内容的方法,没有移除实际业务逻辑 | - -`loadDynamicPopup()` 等通用加载方法沿用原有实现。`MapConroler`、`ItemGuide.ts`、`NewMode.ts` 等其他原有业务脚本没有内容修改。 - -配套非脚本改动:`GameScene.fire` 新增问号按钮、tipPanel 容器及点击/属性绑定;微信构建模板增加上传忽略 `cocos` 目录;`settings/project.json` 将启动场景改为 `TestLayout`(UUID `8e21ec7e-3913-44ec-8f99-53ff52fe81ed`)并更新记录时间。 - -### 新增测试脚本 - -| 脚本 | 作用 | -| --- | --- | -| `assets/test/script/TestLayoutPanel.ts` | 独立测试界面,支持指定本地关卡、全部玩法、重新打开和跳页;直接复用正式面板,不写玩家进度 | -| `assets/test/tests/guide-carousel.cjs` | 使用模拟 Cocos 环境检查筛选、分页、资源回收及场景入口的暂停恢复逻辑 | -| `assets/test/tests/preview.cjs` | 本地预览服务,使用 Creator 编译器及运行中的编辑器引擎/资源展示测试场景 | -| `assets/test/tests/home-prop-panel.cjs` | 残留道具测试;依赖的 HomePropTestPanel.ts 和 HomeScene 节点已不存在,当前不可正常运行 | - -运行方式与移除步骤见 `assets/test/README.md`。本次合并后执行 `node assets/test/tests/guide-carousel.cjs`,全部回归项通过;同时验证了场景引用索引和双方场景改动的保留情况。未进行 Creator 真机视觉及性能验证。 - +测试包及预览专用接口已移除,启动场景已恢复为主线正式场景。 ## 可编辑的固定玩法页结构 diff --git a/assets/GameplayIntro/script/GameplayIntro.ts b/assets/GameplayIntro/script/GameplayIntro.ts index 9277ad1..01c042d 100644 --- a/assets/GameplayIntro/script/GameplayIntro.ts +++ b/assets/GameplayIntro/script/GameplayIntro.ts @@ -59,7 +59,6 @@ export default class GameplayIntro extends cc.Component { viewport.on("page-changed", () => { // GuideCarousel 通知目标页已变化,此时及时加载新邻页、释放远处页。 this.syncPages(); - this.node.emit("guide-page-changed", this.getPreviewState()); }, this); } // 重置分页状态和滑动监听,保留预制体中的三个固定页面。 @@ -215,20 +214,6 @@ export default class GameplayIntro extends cc.Component { // 每次打开都会重新设置起始缩放。 } - /** 给测试面板使用的跳页入口,复用正式滑动组件的翻页逻辑。 */ - previewNavigate(action: string) { - if (!this.carousel || !this.carousel.pages.length) return; - const next = action === "first" ? 0 : action === "last" ? this.introductions.length - 1 : - this.carousel.index + (action === "previous" ? -1 : 1); - this.carousel.goTo(next); - } - - /** 返回当前索引、总页数和玩法配置,供测试面板显示状态。 */ - getPreviewState() { - const index = this.carousel && this.carousel.pages.length ? this.carousel.index : 0; - return { index, total: this.introductions.length, entry: this.introductions[index] || null }; - } - // 提示也复用主预制体的 Status 节点,避免向固定玩法页追加节点。 private showMessage(value: string) { this.status.string = value; diff --git a/assets/test.meta b/assets/test.meta deleted file mode 100644 index d28cb2c..0000000 --- a/assets/test.meta +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ver": "1.1.3", - "uuid": "4d8515b3-3552-48d4-8fdd-3032e2852e88", - "importer": "folder", - "isBundle": true, - "bundleName": "test", - "priority": 1, - "compressionType": {}, - "optimizeHotUpdate": {}, - "inlineSpriteFrames": {}, - "isRemoteBundle": {}, - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/README.md b/assets/test/README.md deleted file mode 100644 index 2da4b4a..0000000 --- a/assets/test/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# 本关玩法面板测试包(可整体移除) - -本目录只包含测试场景、测试面板、预览服务、回归脚本及对应 .meta,不存放正式美术资源。 - -核对日期:2026-09-15。测试面板直接加载正式 GameplayIntro 预制体。正式组件和素材位于 `assets/GameplayIntro`,资源包名 `gameplay_intro`;玩法筛选工具位于公共目录 `assets/Script/module/Tool/LevelGameplay.ts`。当前 HomeScene 没有 HomePropTestPanel 节点,测试目录也没有 HomePropTestPanel.ts。 - -当前 `settings/project.json` 的启动场景是 TestLayout(UUID `8e21ec7e-3913-44ec-8f99-53ff52fe81ed`),相对已合入当前分支的 `origin/main`(`e992a8f`)已发生变更。不需要测试时,先在 Creator 中切回正式启动场景并保存设置,再删除 `assets/test` 和同级 `assets/test.meta`。 - -正式脚本的方法职责、原有 SceneManager.ts 的逐项改动及完整 main 对比清单见 `assets/GameplayIntro/README.md`。 - -## 使用 - -保持当前项目在 Cocos Creator 2.4.15 中打开,在项目根目录执行: - -```powershell -node assets/test/tests/preview.cjs -``` - -打开 `http://127.0.0.1:7460/`。默认读取本项目 settings/project.json 中的编辑器预览端口;7460 是独立预览入口。也可以传入两个参数指定编辑器端口和测试端口。Node.js 需要 18 或以上。Ctrl+C 停止服务。 - -也可以在 Creator 中打开 `assets/test/scene/TestLayout.fire`,选择预览当前场景。 - -测试面板读取本地 `custom/Json/levelN`,支持右上角问号打开、点击面板外空白关闭、关卡输入、前后翻页、边界回弹、白点替换动画。预设第 1/7/44/511 关分别覆盖无道具、1/2/5 页;没有特殊道具时隐藏问号。只读本地关卡,不修改游戏进度,线上关卡更新后可能与本地样本不同。 - -## 回归测试 - -```powershell -node assets/test/tests/guide-carousel.cjs -``` - -覆盖本关类型筛选、变体匹配、生成队列、去重、零/单/双/多页、分页高亮、滑动回弹、相邻三页内存控制、过期加载回收、关闭重开及素材映射;同时用正式场景节点验证问号显隐、实际关卡输入、加载暂停和关闭恢复。 - -编译器默认从 `C:/ProgramData/cocos/editors/Creator/2.4.15` 读取;其他目录可通过 `COCOS_EDITOR_HOME` 指定。 - -## 残留的 HomeScene 道具测试 - -`tests/home-prop-panel.cjs` 仍保留,但它读取的 `script/HomePropTestPanel.ts` 已不存在,HomeScene 中对应节点也已不存在。因此当前没有该测试入口,这个脚本不能作为可运行的测试命令使用。本次仅更新文档,未删除残留测试文件。 - -## 测试脚本职责 - -| 脚本 | 作用 | -| --- | --- | -| `script/TestLayoutPanel.ts` | 创建测试控件,读取指定本地关卡并筛选介绍;支持全部玩法、问号重开和首尾/前后跳页,不修改玩家进度 | -| `tests/guide-carousel.cjs` | 在模拟 Cocos 环境中验证正式逻辑;不等同于真机视觉及性能验证 | -| `tests/preview.cjs` | 加载 Creator TypeScript 编译器,编译测试所需脚本,并通过本地服务读取编辑器预览引擎和资源 | -| `tests/home-prop-panel.cjs` | 已失去依赖的残留测试,当前不可正常运行 | - -2026-09-15 合入 main(`e992a8f`)后,已执行 `node assets/test/tests/guide-carousel.cjs`,全部回归项通过。本次未启动预览服务或进行真机验证。 diff --git a/assets/test/README.md.meta b/assets/test/README.md.meta deleted file mode 100644 index 3950d41..0000000 --- a/assets/test/README.md.meta +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ver": "2.0.2", - "uuid": "7ac21c73-64e5-4490-8e88-b7a391bc0158", - "importer": "markdown", - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/scene.meta b/assets/test/scene.meta deleted file mode 100644 index 6d5e7b0..0000000 --- a/assets/test/scene.meta +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ver": "1.1.3", - "uuid": "dd583467-1f59-4677-b71d-dc0d97d24a55", - "importer": "folder", - "isBundle": false, - "bundleName": "", - "priority": 1, - "compressionType": {}, - "optimizeHotUpdate": {}, - "inlineSpriteFrames": {}, - "isRemoteBundle": {}, - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/scene/TestLayout.fire b/assets/test/scene/TestLayout.fire deleted file mode 100644 index 36a7d3e..0000000 --- a/assets/test/scene/TestLayout.fire +++ /dev/null @@ -1,258 +0,0 @@ -[ - { - "__type__": "cc.SceneAsset", - "_name": "TestLayout", - "_objFlags": 0, - "_native": "", - "scene": { - "__id__": 1 - } - }, - { - "__type__": "cc.Scene", - "_objFlags": 0, - "_parent": null, - "_children": [ - { - "__id__": 2 - } - ], - "_active": false, - "_components": [], - "_prefab": null, - "_opacity": 255, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 0, - "height": 0 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_trs": { - "__type__": "TypedArray", - "ctor": "Float64Array", - "array": [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1 - ] - }, - "_is3DNode": true, - "_groupIndex": 0, - "groupIndex": 0, - "autoReleaseAssets": true, - "_id": "8e21ec7e-3913-44ec-8f99-53ff52fe81ed", - "_name": "TestLayout" - }, - { - "__type__": "cc.Node", - "_name": "Canvas", - "_objFlags": 0, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 3 - } - ], - "_active": true, - "_components": [ - { - "__id__": 5 - }, - { - "__id__": 6 - } - ], - "_prefab": null, - "_opacity": 255, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1080, - "height": 1920 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_trs": { - "__type__": "TypedArray", - "ctor": "Float64Array", - "array": [ - 540, - 960, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1 - ] - }, - "_eulerAngles": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_skewX": 0, - "_skewY": 0, - "_is3DNode": false, - "_groupIndex": 0, - "groupIndex": 0, - "_id": "a5esZu+45LA5mBpvttspPD" - }, - { - "__type__": "cc.Node", - "_name": "Main Camera", - "_objFlags": 0, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 4 - } - ], - "_prefab": null, - "_opacity": 255, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1080, - "height": 1920 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_trs": { - "__type__": "TypedArray", - "ctor": "Float64Array", - "array": [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1 - ] - }, - "_eulerAngles": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_skewX": 0, - "_skewY": 0, - "_is3DNode": false, - "_groupIndex": 0, - "groupIndex": 0, - "_id": "e1WoFrQ79G7r4ZuQE3HlNb" - }, - { - "__type__": "cc.Camera", - "_name": "", - "_objFlags": 0, - "node": { - "__id__": 3 - }, - "_enabled": true, - "_cullingMask": 4294967295, - "_clearFlags": 7, - "_backgroundColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_depth": -1, - "_zoomRatio": 1, - "_targetTexture": null, - "_fov": 60, - "_orthoSize": 10, - "_nearClip": 1, - "_farClip": 4096, - "_ortho": true, - "_rect": { - "__type__": "cc.Rect", - "x": 0, - "y": 0, - "width": 1, - "height": 1 - }, - "_renderStages": 1, - "_alignWithScreen": true, - "_id": "81GN3uXINKVLeW4+iKSlim" - }, - { - "__type__": "cc.Canvas", - "_name": "", - "_objFlags": 0, - "node": { - "__id__": 2 - }, - "_enabled": true, - "_designResolution": { - "__type__": "cc.Size", - "width": 1080, - "height": 1920 - }, - "_fitWidth": true, - "_fitHeight": true, - "_id": "59Cd0ovbdF4byw5sbjJDx7" - }, - { - "__type__": "47a56/pSx1KrYO40VL/jOFP", - "_name": "", - "_objFlags": 0, - "node": { - "__id__": 2 - }, - "_enabled": true, - "_id": "" - } -] diff --git a/assets/test/scene/TestLayout.fire.meta b/assets/test/scene/TestLayout.fire.meta deleted file mode 100644 index bbff55f..0000000 --- a/assets/test/scene/TestLayout.fire.meta +++ /dev/null @@ -1,8 +0,0 @@ -{ - "ver": "1.3.2", - "uuid": "8e21ec7e-3913-44ec-8f99-53ff52fe81ed", - "importer": "scene", - "asyncLoadAssets": false, - "autoReleaseAssets": true, - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/script.meta b/assets/test/script.meta deleted file mode 100644 index 55b4a66..0000000 --- a/assets/test/script.meta +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ver": "1.1.3", - "uuid": "426e063c-84c4-48c4-b73a-d999e2643c13", - "importer": "folder", - "isBundle": false, - "bundleName": "", - "priority": 1, - "compressionType": {}, - "optimizeHotUpdate": {}, - "inlineSpriteFrames": {}, - "isRemoteBundle": {}, - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/script/TestLayoutPanel.ts b/assets/test/script/TestLayoutPanel.ts deleted file mode 100644 index 8d157a2..0000000 --- a/assets/test/script/TestLayoutPanel.ts +++ /dev/null @@ -1,219 +0,0 @@ -import type GameplayIntro from "../../GameplayIntro/script/GameplayIntro"; -import { getLevelGameplay } from "../../Script/module/Tool/LevelGameplay"; -const { ccclass } = cc._decorator; - -/** 独立测试面板:直接加载正式 GameplayIntro 预制体,不运行登录/对局,也不写入存档。 */ -@ccclass -export default class TestLayoutPanel extends cc.Component { - private config: any[] = []; - // 配置准备好才能打开;loading 防止资源未返回时重复创建多个面板。 - private ready = false; - private loading = false; - private popup: cc.Node = null; - private controller: any = null; - private stage: cc.Node = null; - private status: cc.Label = null; - private levelInput: cc.EditBox = null; - private level = 511; - private allGameplay = false; - private entries: any[] = []; - private levelRequest = 0; - private infoButton: cc.Node = null; - - /** 创建测试按钮、关卡输入框和预览区域;这些辅助控件都属于可删除的 test 包。 */ - onLoad() { - this.node.color = cc.Color.WHITE; - this.box(this.node, "Background", 0, 0, 1080, 2400, new cc.Color(27, 32, 43)); - this.label(this.node, "本关玩法 · 问号按钮测试", 0, 867, 42); - this.label(this.node, "按关卡检查,或查看全部玩法 / 标题、道具、说明、分页", 0, 811, 25); - this.stage = new cc.Node("PreviewStage"); - this.stage.parent = this.node; - this.stage.setPosition(0, 25); - this.stage.setContentSize(1080, 1600); - this.stage.scale = 0.78; - // 测试控件显示在遮罩上方,方便面板打开时继续切换测试关卡。 - this.infoButton = this.createInfoButton(this.node, 440, 840, () => this.reopen()); - this.infoButton.active = false; - const controls = new cc.Node("Controls"); - controls.parent = this.node; - controls.zIndex = 10; - // 读取真实关卡,分别验证零项、单项、双项和多项。 - [1, 7, 44, 511].forEach((level, index) => { - this.button(controls, level === 1 ? "无道具 · 1关" : level === 7 ? "1页 · 7关" : level === 44 ? "2页 · 44关" : "5页 · 511关", - -375 + index * 250, 735, 235, () => this.open(level)); - }); - const input = this.box(controls, "LevelInput", -375, 660, 205, 58, new cc.Color(65, 75, 92)); - input.active = false; - this.levelInput = input.addComponent(cc.EditBox); - this.levelInput.textLabel = this.label(input, "511", 0, 0, 28); - this.levelInput.placeholderLabel = this.label(input, "输入关卡", 0, 0, 26); - [this.levelInput.textLabel, this.levelInput.placeholderLabel].forEach(label => { - label.node.setContentSize(185, 58); - label.node.setPosition(-92.5, 29); - }); - this.levelInput.fontSize = 28; - this.levelInput.fontColor = cc.Color.WHITE; - this.levelInput.inputMode = cc.EditBox.InputMode.NUMERIC; - this.levelInput.string = "511"; - input.active = true; - this.button(controls, "按关卡打开", -115, 660, 230, () => { - const level = Number(this.levelInput.string); - if (!Number.isInteger(level) || level < 1) { this.status.string = "请输入大于或等于 1 的整数关卡"; return; } - this.open(level); - }); - this.button(controls, "重新打开", 140, 660, 230, () => this.reopen()); - this.button(controls, "全部玩法", 395, 660, 230, () => this.openAllGameplay()); - this.status = this.label(controls, "正在加载正式弹窗和介绍资源…", 0, -635, 28); - this.status.node.width = 1020; - this.status.overflow = cc.Label.Overflow.RESIZE_HEIGHT; - ["第一页", "上一页", "下一页", "最后页"].forEach((title, index) => { - const actions = ["first", "previous", "next", "last"]; - this.button(controls, title, -375 + index * 250, -730, 230, () => { - if (this.popup && this.popup.active && this.controller) this.controller.previewNavigate(actions[index]); - }); - }); - this.label(controls, "无道具隐藏问号,单页不可滑动,打开先看最左端最新玩法", 0, -812, 25); - this.label(controls, "点击面板外空白处关闭;读取本地关卡,不修改游戏进度", 0, -856, 25); - // 只读取正式解锁配置,输入的测试关卡保存在本组件,不修改玩家实际进度。 - cc.resources.load("Json/NEW_LEVEL", cc.JsonAsset, (error, asset: cc.JsonAsset) => { - if (!cc.isValid(this, true)) return; - if (error) { this.status.string = "解锁配置加载失败:" + error.message; return; } - this.config = asset.json.NEW_LEVEL; - this.ready = true; - this.open(this.level); - - }); - } - - /** 用指定测试关卡打开正式预制体;已有面板直接复用,否则异步加载并实例化。 */ - private open(level: number) { - if (!this.ready) return; - this.allGameplay = false; - this.level = level; - this.levelInput.string = String(level); - this.infoButton.active = false; - if (this.popup && this.popup.active) this.controller.close(); - this.entries = []; - this.status.string = "正在读取第 " + level + " 关…"; - const token = ++this.levelRequest; - cc.assetManager.loadBundle("custom", (error, bundle) => { - if (!cc.isValid(this, true) || token !== this.levelRequest) return; - if (error) { this.status.string = "关卡包加载失败:" + error.message; return; } - bundle.load("Json/level" + level, cc.JsonAsset, (loadError, asset: cc.JsonAsset) => { - if (!cc.isValid(this, true) || token !== this.levelRequest) return; - if (loadError) { this.status.string = "本地关卡加载失败:" + loadError.message; return; } - this.entries = getLevelGameplay(this.config, asset.json); - this.infoButton.active = this.entries.length > 0; - if (!this.entries.length) { this.status.string = "本关没有特殊道具,问号已隐藏"; return; } - this.showEntries(); - }); - }); - } - - /** 策划验图入口:不按关卡过滤,仍使用正式面板按相邻三页加载资源。 */ - private openAllGameplay() { - if (!this.ready) return; - ++this.levelRequest; // 丢弃切换前尚未返回的关卡请求。 - this.allGameplay = true; - const seen = new Set(); - this.entries = this.config.filter(entry => { - if (seen.has(entry.name)) return false; - seen.add(entry.name); - return true; - }); - this.infoButton.active = this.entries.length > 0; - this.showEntries(); - } - - private reopen() { - if (this.allGameplay) this.openAllGameplay(); - else this.open(this.level); - } - - private showEntries() { - if (this.popup) { - this.controller.open(this.entries); - return; - } - if (this.loading) return; - this.loading = true; - cc.assetManager.loadBundle("gameplay_intro", (error, bundle) => { - if (!cc.isValid(this, true)) return; - if (error) { this.loading = false; this.status.string = "功能包加载失败:" + error.message; return; } - bundle.load("prefab/GameplayIntro", cc.Prefab, (prefabError, prefab: cc.Prefab) => { - if (!cc.isValid(this, true)) return; - this.loading = false; - if (prefabError) { this.status.string = "面板预制体加载失败:" + prefabError.message; return; } - // 测试挂在 stage 下;正式游戏由 SceneManager 挂在场景 tipPanel 下。 - this.popup = cc.instantiate(prefab); - this.popup.parent = this.stage; - this.popup.active = false; - this.controller = this.popup.getComponent("GameplayIntro") as GameplayIntro; - this.popup.on("guide-page-changed", state => { - this.status.string = (this.allGameplay ? "全部玩法" : "测试关卡 " + this.level) + " | " + (state.index + 1) + " / " + state.total + " 页\n" + - (state.entry ? state.entry.name + " · " + state.entry.tips : "暂无玩法"); - }); - this.popup.on("intro-closed", () => this.status.string = this.entries.length ? "面板已关闭,点击右上角问号重新查看" : "本关没有特殊道具,问号已隐藏"); - if (this.entries.length) this.controller.open(this.entries); - }); - }); - } - - // 仅测试场景用代码创建问号;正式游戏入口应由 GameScene 中的 Button 节点提供。 - private createInfoButton(parent: cc.Node, x: number, y: number, open: () => void): cc.Node { - const node = new cc.Node("GameplayInfoButton"); - node.parent = parent; - node.setPosition(x, y); - node.setContentSize(88, 88); - const sprite = node.addComponent(cc.Sprite); - sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; - cc.assetManager.loadBundle("gameplay_intro", (error, bundle) => { - if (!cc.isValid(node, true)) return; - if (error) { cc.error("History button bundle", error); return; } - bundle.load("Texture/wenhao", cc.SpriteFrame, (imageError, frame: cc.SpriteFrame) => { - if (!cc.isValid(node, true)) return; - if (imageError) { cc.error("History button image", imageError); return; } - sprite.spriteFrame = frame; - const size = frame.getOriginalSize(); - const scale = Math.min(88 / size.width, 88 / size.height); - node.setContentSize(size.width * scale, size.height * scale); - }); - }); - node.addComponent(cc.Button); - node.on("click", open); - return node; - } - - - // 以下三个工具方法用于绘制测试控件的底色、文字和按钮。 - private box(parent: cc.Node, name: string, x: number, y: number, width: number, height: number, color: cc.Color) { - const node = new cc.Node(name); - node.parent = parent; - node.setPosition(x, y); - node.setContentSize(width, height); - const graphics = node.addComponent(cc.Graphics); - graphics.fillColor = color; - graphics.roundRect(-width / 2, -height / 2, width, height, 12); - graphics.fill(); - return node; - } - - private label(parent: cc.Node, text: string, x: number, y: number, size: number) { - const node = new cc.Node("Label"); - node.parent = parent; - node.setPosition(x, y); - const label = node.addComponent(cc.Label); - label.string = text; - label.fontSize = size; - label.lineHeight = size + 10; - label.horizontalAlign = cc.Label.HorizontalAlign.CENTER; - return label; - } - - private button(parent: cc.Node, text: string, x: number, y: number, width: number, action: () => void) { - const node = this.box(parent, text, x, y, width, 58, new cc.Color(55, 91, 143)); - this.label(node, text, 0, 0, 26); - node.addComponent(cc.Button); - node.on("click", action, this); - } -} diff --git a/assets/test/script/TestLayoutPanel.ts.meta b/assets/test/script/TestLayoutPanel.ts.meta deleted file mode 100644 index bf29f41..0000000 --- a/assets/test/script/TestLayoutPanel.ts.meta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ver": "1.1.0", - "uuid": "47a56fe9-4b1d-4aad-83b8-d152ff8ce14f", - "importer": "typescript", - "isPlugin": false, - "loadPluginInWeb": true, - "loadPluginInNative": true, - "loadPluginInEditor": false, - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/tests.meta b/assets/test/tests.meta deleted file mode 100644 index aba7049..0000000 --- a/assets/test/tests.meta +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ver": "1.1.3", - "uuid": "e3931be8-ad66-48ed-8ba8-66ae216a6bf6", - "importer": "folder", - "isBundle": false, - "bundleName": "", - "priority": 1, - "compressionType": {}, - "optimizeHotUpdate": {}, - "inlineSpriteFrames": {}, - "isRemoteBundle": {}, - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/tests/guide-carousel.cjs b/assets/test/tests/guide-carousel.cjs deleted file mode 100644 index 1acdde8..0000000 --- a/assets/test/tests/guide-carousel.cjs +++ /dev/null @@ -1,407 +0,0 @@ -// Usage (project root): node assets/test/tests/guide-carousel.cjs -const fs = require('fs'); -const vm = require('vm'); -const path = require('path'); -const assert = require('assert'); -const ts = process.argv[2] ? require(path.resolve(process.argv[2])) : require("./preview.cjs").loadTypeScript(); -class Vec { constructor(x = 0, y = 0) { this.x = x; this.y = y; } clone() { return new Vec(this.x, this.y); } } -class Node { - constructor(name) { this.name = name; this.children = []; this.position = new Vec(); this.scale = 1; this.active = true; this.components = new Map(); this.events = {}; } - set parent(node) { this._parent = node; if (node) node.children.push(this); } - get parent() { return this._parent; } - set x(x) { this.position.x = x; } get x() { return this.position.x; } - set y(y) { this.position.y = y; } get y() { return this.position.y; } - setPosition(x, y) { this.position = new Vec(x, y); } - setContentSize(w, h) { this.width = w; this.height = h; } - addComponent(Type) { const component = new Type(); component.node = this; this.components.set(Type, component); return component; } - getComponent(Type) { return typeof Type === "string" ? Array.from(this.components.values()).find(c => c.constructor.name === Type) : this.components.get(Type); } - getChildByName(name) { return this.children.find(node => node.name === name); } - on(event, handler, target) { this.events[event] = handler.bind(target); this.events[event].target = target; } - emit(event, arg) { if (this.events[event]) this.events[event](arg); } - removeAllChildren() { this.children = []; } - removeFromParent() { if (this.parent) this.parent.children = this.parent.children.filter(n => n !== this); this._parent = null; } - targetOff(target) { Object.keys(this.events).forEach(e => { if (this.events[e].target === target) delete this.events[e]; }); } - destroy() {} - convertToNodeSpaceAR(pos) { return pos.clone(); } -} -Node.EventType = Object.fromEntries(['TOUCH_START','TOUCH_MOVE','TOUCH_END','TOUCH_CANCEL','MOUSE_DOWN','MOUSE_MOVE','MOUSE_UP','MOUSE_LEAVE'].map(x => [x, x])); -class Mask {} Mask.Type = { RECT: 0 }; -class Graphics { rect() {} circle() {} fill() {} stroke() {} roundRect() {} } -class Sprite {} Sprite.Type = { SLICED: 1 }; Sprite.SizeMode = { CUSTOM: 0 }; class Label {} Label.HorizontalAlign = { CENTER: 0 }; class Color {} Color.WHITE = {}; -const config = JSON.parse(fs.readFileSync('assets/resources/Json/NEW_LEVEL.json', 'utf8')).NEW_LEVEL; -let now = 1000; -let deferImages = false; -const pendingImages = []; -const imageLoads = []; -const frames = []; -const cc = { Node, Mask, Graphics, Sprite, Label, Color, Button: class {}, BlockInputEvents: class {}, SpriteFrame: class {}, Component: class {}, - _decorator: { ccclass: Type => Type, property: (...args) => args.length > 1 ? undefined : () => {} }, - fx: { GameConfig: { NEW_LEVEL: config, GM_INFO: { level: 510 } } }, isValid: () => true, - assetManager: { loadBundle(_name, cb) { cb(null, { load(_path, _type, callback) { const frame = { refs: 0, texture: {}, getTexture() { return this.texture; }, addRef() { this.refs++; return this; }, decRef() { this.refs--; return this; }, getOriginalSize: () => ({width:400,height:200}) }; - frames.push(frame); imageLoads.push(_path); - if (deferImages && !_path.endsWith('/mianba')) pendingImages.push(() => callback(null, frame)); else callback(null, frame); } }); } }, - instantiate(asset) { return instantiatePrefab(asset); } -}; -cc.assetManager.loadBundle("resources", (_error, bundle) => cc.resources = bundle); -let deferTweens = false; -const tweens = []; -cc.tween = target => { - const tween = { stopped: false, steps: [], - to(duration, props, options) { this.steps.push(() => Object.assign(target, props)); return this; }, - call(callback) { this.steps.push(callback); return this; }, - stop() { this.stopped = true; }, - finish() { for (const step of this.steps) { if (this.stopped) break; step(); } }, - start() { tweens.push(this); if (!deferTweens) this.finish(); return this; } - }; - return tween; -}; -const modules = {}; -for (const name of ['LevelGameplay', 'GuideCarousel', 'GameplayIntro']) { - const source = fs.readFileSync(name === 'LevelGameplay' ? 'assets/Script/module/Tool/LevelGameplay.ts' : 'assets/GameplayIntro/script/' + name + '.ts', 'utf8'); - const result = ts.transpileModule(source, { compilerOptions: { experimentalDecorators: true, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 } }); - const exports = {}; - vm.runInNewContext(result.outputText, { cc, exports, require: request => modules[request.slice(2)], Date: { now: () => now } }); - modules[name] = exports; -} -function entriesForLevel(level) { - const data = JSON.parse(fs.readFileSync('assets/custom/Json/level' + level + '.json', 'utf8')); - return modules.LevelGameplay.getLevelGameplay(config, data); -} -function instantiatePrefab(data) { - const objects = data.map(o => o.__type__ === 'cc.Node' ? new Node(o._name) : null); - data.forEach((o, i) => { - const node = objects[i]; - if (!node) return; - node.setContentSize(o._contentSize.width, o._contentSize.height); - node.setPosition(o._trs.array[0], o._trs.array[1]); - node.scale = o._trs.array[7]; - if (o._parent) node.parent = objects[o._parent.__id__]; - o._components.forEach(ref => { - const c = data[ref.__id__]; - const Type = c.__type__.startsWith('cc.') ? cc[c.__type__.slice(3)] : - c.__type__.startsWith('b6043') ? modules.GameplayIntro.default : modules.GuideCarousel.default; - const instance = node.addComponent(Type); - if (c.dotFrame) instance.dotFrame = c.dotFrame.__uuid__; - if (c.selectedDotFrame) instance.selectedDotFrame = c.selectedDotFrame.__uuid__; - }); - }); - return objects[data[0].data.__id__]; -} -function setup(entries) { - const popup = instantiatePrefab(JSON.parse(fs.readFileSync('assets/GameplayIntro/prefab/GameplayIntro.prefab', 'utf8'))); - const component = popup.getComponent(modules.GameplayIntro.default); - component.open(typeof entries === 'number' ? entriesForLevel(entries) : entries); - return { component, carousel: component.carousel, popup }; -} -function event(x, id = 1, button = 0) { return { getID: () => id, getButton: () => button, getLocation: () => new Vec(x), stopPropagation() {} }; } -function drag(carousel, delta, touch = false) { - carousel[touch ? 'touchStart' : 'mouseDown'](event(0)); now += 250; - carousel[touch ? 'touchMove' : 'mouseMove'](event(delta)); now += 150; - carousel[touch ? 'touchEnd' : 'mouseUp'](event(delta)); carousel.update(0.4); -} -const names = entries => Array.from(entries, entry => entry.name); -assert.deepEqual(names(entriesForLevel(1)), []); -assert.deepEqual(names(entriesForLevel(7)), ['daoju0']); -assert.deepEqual(names(entriesForLevel(15)), ['daoju1']); -assert.deepEqual(names(entriesForLevel(44)), ['daoju3', 'daoju4']); -assert.deepEqual(names(entriesForLevel(511)), ['daoju0', 'daoju2', 'daoju5', 'daoju11', 'daoju13']); -// These two introduction assets describe different mechanics despite their unlock-table order. -assert(names(entriesForLevel(1354)).includes('daoju27')); // extending rod -assert(!names(entriesForLevel(1354)).includes('daoju26')); -assert(names(entriesForLevel(1398)).includes('daoju26')); // moving colored gate -assert(!names(entriesForLevel(1398)).includes('daoju27')); -assert(names(entriesForLevel(1881)).includes('daoju32')); -assert(!names(entriesForLevel(1923)).includes('daoju33')); // this local fixture has no spawn gates -const filter = modules.LevelGameplay.getLevelGameplay; -const data = (blocks = [], walls = [], level = {}) => ({ BLOCK_INFO: [blocks], WALL_INFO: [walls], LEVEL_INFO: [level] }); -const block = fields => ({ block: 0, color: 1, type: 0, ...fields }); -// Distinct variants must not add an unrelated basic explanation for the same object. -const cases = [ - [block({ type: 7 }), 'daoju0'], [block({ type: 1, stacking: 2 }), 'daoju1'], - [block({ type: 4 }), 'daoju2'], [block({ type: 5 }), 'daoju3'], - [block({ type: 2 }), 'daoju4'], [block({ type: 9 }), 'daoju5'], - [block({ type: 6 }), 'daoju7'], [block({ color: 11 }), 'daoju11'], - [block({ floor: 1, floorTime: 3 }), 'daoju12'], [block({ type: 14 }), 'daoju14'], - [block({ type: 16 }), 'daoju16'], [block({ type: 9, lockTime: 3 }), 'daoju17'], - [block({ type: 17 }), 'daoju18'], [block({ lock: false }), 'daoju19'], - [block({ floor: 1, floorTime: 3, floorMove: true }), 'daoju20'], - [block({ type: 1, colorChange: true }), 'daoju21'], [block({ type: 21 }), 'daoju22'], - [block({ flowerReceive: 33, floor: 300, floorTime: 3000, floorMove: true }), 'daoju24'], - [block({ colorArray: '12' }), 'daoju25'], [block({ type: 22 }), 'daoju28'], - [block({ floor: 1, floorTime: 3, floorMove: false, floorColor: 2 }), 'daoju29'], - [block({ type: 8, lockTime: 3 }), 'daoju31'] -]; -for (const [definition, expected] of cases) { - assert.deepEqual(names(filter(config, data([definition]))), [expected], JSON.stringify(definition)); -} -for (const [definition, expected] of [ - [{ special: 2 }, 'daoju6'], [{ special: 3 }, 'daoju9'], [{ special: 4 }, 'daoju10'], - [{ colorArray: '12' }, 'daoju13'], [{ special: 6 }, 'daoju14'], [{ longAndShort: 12 }, 'daoju15'], - [{ special: 7 }, 'daoju23'], [{ special: 10 }, 'daoju26'], [{ special: 9 }, 'daoju27'], [{ toggle: 2 }, 'daoju30'] -]) assert.deepEqual(names(filter(config, data([], [definition]))), [expected]); -assert.deepEqual(names(filter(config, data([], [], { risefall: [{ color: 2 }] }))), ['daoju8']); -assert.deepEqual(names(filter(config, data([], [], { risefall: [{ unLockFall: true }] }))), ['daoju30']); -assert.deepEqual(names(filter(config, data([], [], { risefall: [{ vine: 0 }] }))), []); -assert.deepEqual(names(filter(config, data([], [], { risefall: [{ vine: 1 }] }))), ['daoju32']); -const queued = data([block({ type: 4 })], [], { spawnGates: [{ spawnQueue: [{ members: [block({ type: 4 }), block({ type: 21 })] }] }] }); -const before = JSON.stringify(queued); -assert.deepEqual(names(filter(config, queued)), ['daoju2', 'daoju22', 'daoju33']); -assert.equal(JSON.stringify(queued), before); -assert.deepEqual(names(filter(config.concat(config), queued)), ['daoju2', 'daoju22', 'daoju33']); -console.log('PASS: actual level filtering, variants, walls/tiles, generated blocks, deduplication and unmodified input'); - -let { component, carousel } = setup(511); -assert.equal(component.getPreviewState().total, 5); -assert.equal(component.getPreviewState().index, 0); -assert.equal(component.getPreviewState().entry.name, 'daoju13'); -assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); -assert.equal(carousel.offset, 0); -assert.deepEqual(names(component.introductions), ['daoju13', 'daoju11', 'daoju5', 'daoju2', 'daoju0']); -drag(carousel, -300); assert.equal(carousel.index, 1); assert.equal(component.getPreviewState().entry.name, 'daoju11'); -assert.equal(carousel.dots[1].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); -drag(carousel, 300, true); assert.equal(carousel.index, 0); -carousel.goTo(4); carousel.update(0.4); -carousel.mouseDown(event(0)); now += 100; carousel.mouseMove(event(-500)); -assert(carousel.offset < -4 * 850 && carousel.offset > -4 * 850 - 120); -carousel.mouseUp(event(-500)); carousel.update(0.1); -const caught = carousel.offset; -carousel.mouseDown(event(0)); carousel.mouseMove(event(0)); assert(Math.abs(carousel.offset - caught) < 0.001); -carousel.mouseLeave(); carousel.update(0.4); assert.equal(carousel.offset, -4 * 850); -component.previewNavigate('first'); carousel.update(0.4); assert.equal(carousel.index, 0); -carousel.touchStart(event(0)); now += 100; carousel.touchMove(event(500)); assert(carousel.offset > 0 && carousel.offset < 120); -const offset = carousel.offset; carousel.touchMove(event(900, 2)); assert.equal(carousel.offset, offset); -carousel.touchCancel(event(500)); carousel.update(0.4); assert.equal(carousel.offset, 0); -({ component, carousel } = setup(7)); -assert.equal(carousel.pages.length, 1); assert.equal(carousel.dots.length, 0); -assert.equal(component.board.getChildByName('Pagination').active, false); -assert.equal(Object.keys(carousel.node.events).filter(e => e !== 'page-changed').length, 0); -carousel.mouseDown(event(0)); carousel.mouseMove(event(400)); assert.equal(carousel.offset, 0); -carousel.touchStart(event(0)); carousel.touchMove(event(-400)); assert.equal(carousel.offset, 0); -({ component, carousel } = setup(44)); -assert.equal(carousel.pages.length, 2); assert.equal(carousel.dots.length, 2); -assert.equal(carousel.incomingDot, null); assert.equal(carousel.index, 0); -assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); -assert.deepEqual(Array.from(carousel.dots, dot => dot.x), [-20, 20]); -drag(carousel, -400); assert.equal(carousel.index, 1); assert.equal(carousel.dots[1].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); -drag(carousel, -400); assert.equal(carousel.index, 1); -drag(carousel, 400); assert.equal(carousel.index, 0); assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); -component.close(); component.open(entriesForLevel(511)); -assert.equal(component.getPreviewState().index, 0); -assert.equal(component.carousel.dots[0].getComponent(Sprite).spriteFrame, component.carousel.selectedDotFrame); -assert.equal(component.board.getChildByName('Pagination').active, true); -assert.equal(component.carousel.node.children.length, 1); -component.open(entriesForLevel(7)); assert.equal(component.carousel.dots.length, 0); -assert.equal(component.board.getChildByName('Pagination').children.length, 0); -let closed = false; component.node.on('intro-closed', () => closed = true); -const emptyLoadStart = imageLoads.length; -component.open([]); assert(closed); assert.equal(component.node.active, false); -assert.equal(component.getPreviewState().total, 0); assert.equal(imageLoads.length, emptyLoadStart); -component.previewNavigate('next'); -({ component, carousel } = setup(1)); assert.equal(component.node.active, false); -const source = fs.readFileSync('assets/GameplayIntro/script/GameplayIntro.ts', 'utf8'); -assert(!source.includes('"Texture/"')); -for (let id = 0; id < 34; id++) { - const before = imageLoads.length; - const { component: panel } = setup([{name: 'daoju' + id, level: id}]); - const artId = id === 26 ? 27 : id === 27 ? 26 : id; - assert.deepEqual(imageLoads.slice(before), ['title', 'icon', 'Introduction'].map(prefix => 'Window_Prop/' + prefix + artId)); - for (const resource of imageLoads.slice(before)) assert(fs.existsSync('assets/resources/' + resource + '.png')); - panel.board.getChildByName('CloseButton').emit('click'); - assert.equal(panel.node.active, false); -} -// Image assignment must not override editor-authored dimensions or scale. -for (const id of [0, 21]) { - const { component: panel } = setup([{ name: 'daoju' + id, level: id }]); - const description = panel.loadedPages[0].node.getChildByName('DescriptionImage'); - const authored = JSON.parse(fs.readFileSync('assets/GameplayIntro/prefab/GameplayIntro.prefab', 'utf8')) - .find(n => n._name === 'DescriptionImage'); - assert.equal(description.y, authored._trs.array[1]); - assert.equal(description.scale, authored._trs.array[7]); - assert.equal(description.width, authored._contentSize.width); - assert.equal(panel.loadedPages[0].node.children.length, 3); - panel.close(); -} -const layoutPanel = setup([{ name: 'daoju0', level: 1 }]).component; -layoutPanel.close(); -const descriptionData = layoutPanel.carousel.pages[0].getChildByName('DescriptionImage'); -descriptionData.setContentSize(500, 100); -descriptionData.scale = 0.8; -descriptionData.y = -280; -layoutPanel.open([{ name: 'daoju0', level: 1 }]); -assert.equal(layoutPanel.loadedPages[0].node.getChildByName('DescriptionImage').y, -280); -assert.equal(layoutPanel.loadedPages[0].node.getChildByName('DescriptionImage').scale, 0.8); -assert.equal(descriptionData.width, 500); -assert.equal(descriptionData.height, 100); -layoutPanel.close(); -console.log('PASS: serialized page layout, editor size/position changes and no per-image correction'); - -// The viewport hierarchy is authored once and survives every page switch and reopen. -const fixedPanel = setup(config).component; -const viewport = fixedPanel.board.getChildByName('IntroViewport'); -const collectNodes = node => [node, ...node.children.flatMap(collectNodes)]; -const fixedNodes = collectNodes(viewport); -const track = viewport.getChildByName('Pages'); -assert.equal(track.children.length, 3); -for (let i = 0; i < 34; i++) { - fixedPanel.carousel.goTo(i, false); - assert.deepEqual(collectNodes(viewport), fixedNodes); - assert(Object.keys(fixedPanel.loadedPages).length <= 3); - assert.equal(fixedPanel.carousel.pages[i].active, true); -} -fixedPanel.close(); -assert.deepEqual(collectNodes(viewport), fixedNodes); -fixedPanel.open(entriesForLevel(511)); -assert.deepEqual(collectNodes(viewport), fixedNodes); -fixedPanel.close(); -console.log('PASS: fixed viewport hierarchy across all 34 pages, close and reopen'); - -// Sorting uses unlock levels, not the input order or the numerical artwork IDs. -const shuffled = [config.find(e => e.name === 'daoju21'), config.find(e => e.name === 'daoju22')]; -({ component, carousel } = setup(shuffled)); -assert.equal(component.getPreviewState().entry.name, 'daoju21'); -assert.equal(carousel.index, 0); -assert.deepEqual(names(shuffled), ['daoju21', 'daoju22']); -component.close(); -console.log('PASS: latest unlocked page at left edge, zero/single/two/many pages, reopen/reset, boundaries and 34 asset mappings'); - -// Window residency and actual texture references shrink on navigation/close. -const loadStart = imageLoads.length; -({ component, carousel } = setup(config)); // Stress the renderer with every existing artwork. -assert.equal(Object.keys(component.loadedPages).length, 2); -assert.equal(imageLoads.length - loadStart, 6); -for (let i = 0; i < 12; i++) { - drag(carousel, -300); - assert(Object.keys(component.loadedPages).length <= 3); - assert(Object.keys(component.loadedPages).every(key => Math.abs(Number(key) - carousel.index) <= 1)); -} -const owned = Object.values(component.loadedPages).flatMap(page => page.frames); -assert.equal(owned.length, 9); -component.node.getChildByName('Backdrop').emit('click'); -assert.equal(component.node.active, false); assert(owned.every(frame => frame.refs === 0)); -deferImages = true; -({ component, carousel } = setup(config)); assert.equal(pendingImages.length, 1); -component.previewNavigate('last'); component.close(); -const loadsAtClose = imageLoads.length; -pendingImages.shift()(); -assert.equal(imageLoads.length, loadsAtClose); assert.equal(pendingImages.length, 0); -assert.equal(Object.keys(component.loadedPages).length, 0); assert.equal(frames[frames.length - 1].refs, 0); -deferImages = false; -console.log('PASS: outside close, <=3 resident pages, sequential loads, released references and stale in-flight completion'); - -({ component, carousel } = setup(511)); -carousel.goTo(0, false); -carousel.goTo(1); carousel.update(0.4); carousel.goTo(2); carousel.update(0.4); carousel.goTo(3); -assert.equal(carousel.activeDot, 2); assert.equal(carousel.incomingDot.active, true); -carousel.update(0.1); assert(carousel.dots[2].x < 40); assert(carousel.incomingDot.opacity > 0); -carousel.update(0.3); assert.equal(carousel.incomingDot.active, false); -carousel.goTo(2); carousel.update(0.4); carousel.goTo(1); carousel.update(0.4); carousel.goTo(0); -assert.equal(carousel.dotDirection, 1); -carousel.onDisable(); assert.equal(carousel.incomingDot.active, false); assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); -carousel.goTo(-1); assert.equal(carousel.incomingDot.active, false); -component.close(); -console.log('PASS: edge dot replacement, symmetric motion, disable cleanup and boundary snap'); - -// Closing retains content and pause until the exit tween completes; reopening cancels it. -deferTweens = true; -({ component } = setup(511)); -assert.equal(component.board.scale, 0.3); -tweens[tweens.length - 1].finish(); -assert.equal(component.board.scale, 1); -let closeEvents = 0; -component.node.on('intro-closed', () => closeEvents++); -component.close(); -const exitTween = tweens[tweens.length - 1]; -component.close(); assert.equal(tweens[tweens.length - 1], exitTween); -assert.equal(closeEvents, 0); assert(component.node.active); -assert(Object.keys(component.loadedPages).length > 0); -component.open(entriesForLevel(511)); exitTween.finish(); -assert(component.node.active); assert.equal(closeEvents, 0); -component.close(); tweens[tweens.length - 1].finish(); -assert.equal(component.node.active, false); assert.equal(closeEvents, 1); -assert.equal(Object.keys(component.loadedPages).length, 0); -deferTweens = false; -console.log('PASS: animated close timing, duplicate close guard, reopen cancellation and resource cleanup'); - -// Scene teardown can destroy child nodes before the panel controller. -({ component } = setup(511)); -component.close(); -Object.defineProperty(component.board, 'scale', { set() { throw new Error('scale accessed after child destruction'); } }); -assert.doesNotThrow(() => component.onDestroy()); -({ component } = setup(511)); -const invalid = new Set(); -for (const page of Object.values(component.loadedPages)) { - invalid.add(page.node); - Object.defineProperty(page.node, 'active', { set() { throw new Error('destroyed page accessed'); } }); - for (const sprite of page.sprites) { - invalid.add(sprite); - Object.defineProperty(sprite, 'spriteFrame', { set() { throw new Error('destroyed sprite accessed'); } }); - } -} -const previousIsValid = cc.isValid; -cc.isValid = object => !invalid.has(object); -const teardownFrames = Object.values(component.loadedPages).flatMap(page => page.frames); -assert.doesNotThrow(() => component.onDestroy()); -assert(teardownFrames.every(frame => frame.refs === 0)); -assert.equal(Object.keys(component.loadedPages).length, 0); -cc.isValid = previousIsValid; -console.log('PASS: teardown after child destruction without node writes or leaked image references'); - -// Exercise the real question-entry controller against the serialized game scene. -const sceneData = JSON.parse(fs.readFileSync('assets/Scene/GameScene.fire', 'utf8')); -const sceneNodes = sceneData.map(o => o.__type__ === 'cc.Node' ? new Node(o._name) : null); -sceneData.forEach((o, i) => { if (sceneNodes[i] && o._parent && sceneNodes[o._parent.__id__]) sceneNodes[i].parent = sceneNodes[o._parent.__id__]; }); -const canvas = sceneNodes.find(n => n && n.name === 'Canvas'); -const infoButton = sceneNodes.find(n => n && n.name === 'GameplayInfoButton'); -cc.find = (relative, root) => relative.split('/').reduce((node, part) => node && node.getChildByName(part), root); -cc.macro = { MAX_ZINDEX: 9999 }; -cc.error = () => {}; -let stops = 0, starts = 0, frozen = false; -const map = { gameOver: false, shopPause: false, gameStart: true, pause: false, - stopBoom() { stops++; }, startBoom() { starts++; }, iceTrue: () => frozen }; -const managerExports = {}; -const managerCode = ts.transpileModule(fs.readFileSync('assets/Script/SceneManager.ts', 'utf8'), { - compilerOptions: { experimentalDecorators: true, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 } -}).outputText; -assert(!managerCode.includes("../GameplayIntro/script/GameplayIntro"), "Startup must not require the unloaded introduction bundle"); -vm.runInNewContext(managerCode, { cc, exports: managerExports, require: request => - request === './Map' ? { default: { _instance: map } } : modules[request.split('/').pop()] || {} }); -const manager = new managerExports.default(); -manager.node = canvas; -manager.tipPanel = new Node('tipPanel'); -manager.isPopupRuntimeAlive = () => true; -manager.getDynamicPopup = () => null; -manager.dynamicPopupLoads = {}; -let popupLoads = 0, completeLoad; -manager.loadDynamicPopup = (key, bundle, file, parent, callback) => { - popupLoads++; manager.dynamicPopupLoads[key] = true; completeLoad = callback; -}; -manager.finishDynamicPopupLoad = key => delete manager.dynamicPopupLoads[key]; -function setLevel(level) { Object.assign(cc.fx.GameConfig, JSON.parse(fs.readFileSync('assets/custom/Json/level' + level + '.json', 'utf8'))); manager.refreshGameplayInfo(); } -setLevel(1); assert.equal(infoButton.active, false); -manager.openGameplayIntro(); assert.equal(popupLoads, 0); assert.equal(stops, 0); assert.equal(map.pause, false); -setLevel(511); assert.equal(infoButton.active, true); -cc.fx.GameConfig.GM_INFO.level = 0; // Display follows the loaded map, not player progress. -manager.openGameplayIntro(); assert.equal(popupLoads, 1); assert.equal(map.pause, true); assert.equal(stops, 1); -assert.equal(manager.isPauseOpen(), true); -manager.openGameplayIntro(); assert.equal(popupLoads, 1); -({ component } = setup(511)); component.close(); -completeLoad(null, component.node); -assert.equal(component.getPreviewState().total, 5); assert.equal(component.getPreviewState().index, 0); -assert.equal(map.pause, true); -component.close(); assert.equal(map.pause, false); assert.equal(starts, 1); -manager.openGameplayIntro(); completeLoad(null, component.node); component.close(); assert.equal(starts, 2); -manager.openGameplayIntro(); completeLoad(new Error('load failure')); assert.equal(map.pause, false); -frozen = true; -manager.openGameplayIntro(); completeLoad(null, component.node); component.close(); assert.equal(map.pause, true); -frozen = false; -console.log('PASS: real scene question visibility, empty-entry no-op, actual-map input, loading pause, close/reopen resume and frozen pause'); -// Introduction metadata may finish loading after the game scene enters. -let finishConfig; -cc.resources = { load(_path, _type, callback) { finishConfig = callback; } }; -cc.fx.GameConfig.NEW_LEVEL = null; -manager.refreshGameplayInfo(); assert.equal(infoButton.active, false); -finishConfig(null, { json: { NEW_LEVEL: config } }); assert.equal(infoButton.active, true); -assert.equal(manager.levelGameplay.length, 5); -manager.refreshGameplayInfo(); manager.isPopupRuntimeAlive = () => false; -finishConfig(null, { json: { NEW_LEVEL: config } }); assert.equal(infoButton.active, false); -console.log('PASS: delayed introduction metadata and discarded callback after scene exit'); diff --git a/assets/test/tests/guide-carousel.cjs.meta b/assets/test/tests/guide-carousel.cjs.meta deleted file mode 100644 index 5e3cbd2..0000000 --- a/assets/test/tests/guide-carousel.cjs.meta +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ver": "1.0.3", - "uuid": "f9ccaa27-0fd1-41ca-9625-537a4e381fac", - "importer": "asset", - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/tests/home-prop-panel.cjs b/assets/test/tests/home-prop-panel.cjs deleted file mode 100644 index 643a7eb..0000000 --- a/assets/test/tests/home-prop-panel.cjs +++ /dev/null @@ -1,32 +0,0 @@ -// 在项目根目录运行:node assets/test/tests/home-prop-panel.cjs -const fs = require('fs'), vm = require('vm'), assert = require('assert'); -const ts = require('./preview.cjs').loadTypeScript(); -class Node { - constructor(name) { this.name=name;this.children=[];this.components=[];this.events={};this.active=true; } - set parent(n) { this._parent=n;n.children.push(this); } - get parent() { return this._parent; } - setPosition() {} setContentSize() {} - addComponent(Type) { const c=new Type();c.node=this;this.components.push(c);return c; } - on(name,callback,target) { this.events[name]=callback.bind(target); } -} -class Graphics { roundRect() {} fill() {} } -class Label {} Label.HorizontalAlign={CENTER:1}; -let saved={freezeAmount:90,hammerAmount:90,magicAmount:90,other:'preserved'}, writes=0, fail=false; -const info={freezeAmount:2,hammerAmount:3,magicAmount:4}; -const cc={Node,Graphics,Label,Color:class {},Widget:class {},Button:class {},BlockInputEvents:class {},Component:class {}, - _decorator:{ccclass:C=>C},macro:{MAX_ZINDEX:32767},error(){},fx:{GameConfig:{GM_INFO:info},StorageMessage:{ - getStorage(key){assert.equal(key,'prop');return saved;}, - setStorage(key,value){assert.equal(key,'prop');if(fail)throw Error('full');saved=value;writes++;} - }}}; -const code=ts.transpileModule(fs.readFileSync('assets/test/script/HomePropTestPanel.ts','utf8'),{compilerOptions:{experimentalDecorators:true,module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017}}).outputText; -function setup(debug){const exports={};vm.runInNewContext(code,{cc,exports,CC_DEBUG:debug});const c=new exports.default();c.node=new Node('HomePropTestPanel');c.node.parent=new Node('Canvas');c.onLoad();return c;} -const panel=setup(true);assert.equal(writes,0);assert.equal(panel.panel.active,false);panel.open(); -panel.grant(['freezeAmount'],1);assert.equal(info.freezeAmount,3);assert.equal(info.hammerAmount,3);assert.equal(saved.freezeAmount,3);assert.equal(saved.other,'preserved'); -panel.grant(['freezeAmount'],10);assert.equal(info.freezeAmount,13); -panel.grant(['freezeAmount','hammerAmount','magicAmount'],10);assert.equal(info.freezeAmount,23);assert.equal(info.hammerAmount,13);assert.equal(info.magicAmount,14);assert(saved.timestamp>0); -panel.close();panel.open();assert.equal(writes,3);assert(panel.counts[0].string.includes('23')); -fail=true;panel.grant(['freezeAmount'],10);assert.equal(info.freezeAmount,23);assert.equal(writes,3);fail=false; -cc.fx.GameConfig.GM_INFO=null;panel.grant(['freezeAmount'],1);assert.equal(writes,3);assert(panel.message.string.includes('未准备好'));cc.fx.GameConfig.GM_INFO=info; -assert.equal(setup(false).node.children.length,0); -const scene=JSON.parse(fs.readFileSync('assets/Scene/HomeScene.fire','utf8'));assert.equal(scene.filter(n=>n.__type__==='cc.Node'&&n._name==='HomePropTestPanel').length,1); -console.log('PASS: no automatic grant, single/all/repeated grants, local persistence, preserved fields, reopen, storage failure, unready data, release-build exclusion and scene mount.'); diff --git a/assets/test/tests/home-prop-panel.cjs.meta b/assets/test/tests/home-prop-panel.cjs.meta deleted file mode 100644 index 51f2278..0000000 --- a/assets/test/tests/home-prop-panel.cjs.meta +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ver": "1.0.3", - "uuid": "a0a6b889-301e-46fa-bdee-0cd0374358ae", - "importer": "asset", - "subMetas": {} -} \ No newline at end of file diff --git a/assets/test/tests/preview.cjs b/assets/test/tests/preview.cjs deleted file mode 100644 index da82e4f..0000000 --- a/assets/test/tests/preview.cjs +++ /dev/null @@ -1,94 +0,0 @@ -// Run from the project root: node assets/test/tests/preview.cjs [editorPort=7457] [port=7460] -// Uses the installed Cocos 2.4.15 compiler and the running editor's engine/assets. -const fs = require('fs'); -const http = require('http'); -const path = require('path'); -const Module = require('module'); -const root = path.resolve(__dirname, '../../..'); - -function loadTypeScript() { - const editor = process.env.COCOS_EDITOR_HOME || path.join(process.env.ProgramData || 'C:/ProgramData', 'cocos/editors/Creator/2.4.15'); - const archive = path.join(editor, 'resources/app.asar'); - const fd = fs.openSync(archive, 'r'); - try { - const header = Buffer.alloc(16); - fs.readSync(fd, header, 0, 16, 0); - const json = Buffer.alloc(header.readUInt32LE(12)); - fs.readSync(fd, json, 0, json.length, 16); - const files = JSON.parse(json).files.node_modules.files.typescript.files.lib.files; - const entry = files['typescript.js']; - const code = Buffer.alloc(entry.size); - fs.readSync(fd, code, 0, code.length, 8 + header.readUInt32LE(4) + Number(entry.offset)); - const compiler = new Module(path.join(__dirname, 'cocos-typescript.js'), module); - compiler.filename = path.join(__dirname, 'cocos-typescript.js'); - compiler.paths = module.paths; - compiler._compile(code.toString(), compiler.filename); - return compiler.exports; - } finally { fs.closeSync(fd); } -} - -function compress(uuid) { - if (uuid.length !== 36) return uuid; - const hex = uuid.replace(/-/g, ''); - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - let result = hex.slice(0, 5); - for (let i = 5; i < 32; i += 3) { - const value = parseInt(hex.slice(i, i + 3), 16); - result += chars[value >> 6] + chars[value & 63]; - } - return result; -} - -function projectScripts(ts) { - let result = 'window.__quick_compile_project__={load:function(done){const modules={};\n'; - for (const relative of ['assets/Script/module/Tool/LevelGameplay.ts', 'assets/GameplayIntro/script/GuideCarousel.ts', 'assets/GameplayIntro/script/GameplayIntro.ts', 'assets/test/script/TestLayoutPanel.ts']) { - const file = path.join(root, relative); - const name = path.basename(file, '.ts'); - const uuid = compress(JSON.parse(fs.readFileSync(file + '.meta')).uuid); - const code = ts.transpileModule(fs.readFileSync(file, 'utf8'), { - compilerOptions: { experimentalDecorators: true, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 } - }).outputText; - result += `(function(){const module={exports:{}};const exports=module.exports;const require=p=>modules[p.split('/').pop()];cc._RF.push(module,${JSON.stringify(uuid)},${JSON.stringify(name)});\n${code}\ncc._RF.pop();modules[${JSON.stringify(name)}]=module.exports;})();\n`; - } - return result + 'done();}};'; -} - -function start() { - const ts = loadTypeScript(); - const projectSettings = JSON.parse(fs.readFileSync(path.join(root, 'settings/project.json'), 'utf8')); - const editorPort = Number(process.argv[2] || projectSettings['preview-port'] || 7456); - const port = Number(process.argv[3] || 7460); - http.createServer(async (request, response) => { - try { - const pathname = request.url.split('?')[0]; - response.setHeader('Cache-Control', 'no-store'); - if (pathname === '/preview-scene.json') { - const scene = JSON.parse(fs.readFileSync(path.join(root, 'assets/test/scene/TestLayout.fire'))); - scene[6].__type__ = compress(scene[6].__type__); - response.setHeader('Content-Type', 'application/json'); - return response.end(JSON.stringify(scene)); - } - if (pathname === '/preview-scripts/__quick_compile__.js') { - response.setHeader('Content-Type', 'application/javascript'); - return response.end(projectScripts(ts)); - } - // This server only reads the editor's preview files; editor mutations stay disabled. - if (request.method !== 'GET' || pathname === '/update-db') { response.statusCode = 405; return response.end(); } - const upstream = await fetch('http://127.0.0.1:' + editorPort + request.url); - let body = Buffer.from(await upstream.arrayBuffer()); - response.statusCode = upstream.status; - response.setHeader('Content-Type', upstream.headers.get('content-type') || 'application/octet-stream'); - if (pathname === '/settings.js') body = Buffer.from(body.toString().replace(/jsList:\s*\[[\s\S]*?\]/, 'jsList: []')); - if (pathname === '/') body = Buffer.from(body.toString() - .replace('window.__socket_io__ = window.io;', 'window.__socket_io__ = function(){return {on:function(){}}};') - .replace('', '')); - response.end(body); - } catch (error) { - response.statusCode = 500; - response.end('Test preview failed. Keep this project open in Cocos Creator and check the editor port.\n' + error.message); - } - }).listen(port, '127.0.0.1', () => console.log('TestLayout preview: http://127.0.0.1:' + port)); -} - -module.exports = { loadTypeScript }; -if (require.main === module) start(); diff --git a/assets/test/tests/preview.cjs.meta b/assets/test/tests/preview.cjs.meta deleted file mode 100644 index 0678eab..0000000 --- a/assets/test/tests/preview.cjs.meta +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ver": "1.0.3", - "uuid": "d4af8d70-a4e6-4e47-a4e5-27602d995f1b", - "importer": "asset", - "subMetas": {} -} \ No newline at end of file diff --git a/settings/project.json b/settings/project.json index cb08a2a..3c518e7 100644 --- a/settings/project.json +++ b/settings/project.json @@ -53,7 +53,7 @@ "enable": false } }, - "start-scene": "8e21ec7e-3913-44ec-8f99-53ff52fe81ed", + "start-scene": "ea96c58a-b731-4349-bcfc-c446571823ad", "migrate-history": [ "cloud-function" ], -- 2.43.0