feat: add hex trail puzzle, level editor, and visual design guide

This commit is contained in:
cabinw
2026-09-08 21:45:44 +08:00
commit ef2724bb2e
63 changed files with 15036 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
# Cocos Creator 3.8 接入
## 可复用部分
`core.js`、`path-math.js`、`geometry.js` 不依赖 DOM、Canvas 或浏览器物理引擎。转盘和连续碰撞都是普通数学运算,Cocos 与 HTML 直接复用同一套规则。场景绘制只要求多边形、线段、圆和图集 Sprite。
[Cocos 官方模块说明](https://docs.cocos.com/creator/3.8/manual/en/scripting/modules/index.html)与 [Graphics 说明](https://docs.cocos.com/creator/3.8/manual/en/ui-system/components/editor/graphics.html)对应本项目的 ESM/绘制适配方式。
## 接入步骤
复制这些目录到 Creator 的 `assets/hex-trails/`,保留相对路径:
```text
hex-trails/
cocos/HexTrailsBoard.ts
cocos/CandyUiSkin.ts
src/core.js
src/path-math.js
src/geometry.js
src/renderer.js
src/collision-animation.js
src/tile-shape.js
src/sprites.js
src/levels.js
src/design-tokens.js
src/ui-skin.js
design/tokens.json
assets/tiles-matte-clean-v8.png
assets/ui-surfaces.png
assets/ui-icons-generated.png
```
1. 在 Canvas 下建立 `Board` 节点,UITransform 设为 390×390,挂载 `HexTrailsBoard`,使用正常 UI 正交相机。
2. 将 `tiles-matte-clean-v8.png` 的 Texture2D 资源赋给 `tileAtlas`。组件根据 `TILE_FRAMES` 建立四个 SpriteFrame,复用原图,不需要重新生成贴图。未指定时使用基础几何绘制。
3. 棋盘 Graphics 负责轨道;Sprite 池与圆角六边形 Mask 负责哑光棋子;前景 Graphics 负责箭头。源矩形及顶面锚点在 `src/sprites.js` 中,箭头方向与贴图朝向分离。
4. `levelIndex` 选择内置关卡,或把单关 JSON 赋给 `levelAsset`。
5. 按钮绑定 `rotateLeft()`、`rotateRight()`、`undo()`、`restart()`、`hint()`。
6. 监听 `trail-state`,更新步数、剩余棋子、朝向、胜负和忙碌状态。监听 `trail-hint-rotation` 高亮对应旋转按钮;`trail-blocked` 可接声音/震动。
7. 按 `THEME.background` 设置纯色背景;当前主界面只保留居中的 Level、其下方的剩余步数,以及左侧选关、右侧设置。按钮与面板使用下述图片皮肤,动态文字与布局使用 Label、Button、Layout、Widget、EditBox、ScrollView 等节点搭建。原生端不需要 WebView。
## 坐标与动画
逻辑坐标从左上角出发、y 向下,Cocos 默认居中、y 向上。适配器已处理绘制和触摸两侧的转换,并使用相同的正六边形命中计算。
`update(dt)` 驱动转盘和路径动画。转盘动画只转轨道底板,棋子保持不动。失败动画沿计算得到的接触路径前进再回退,使用与浏览器相同的 `drawScene()` 和扫掠碰撞结果。
## 需要替换的宿主接口
| HTML | 原生接口 |
| --- | --- |
| Canvas 图集采样 | SpriteFrame.rect + Sprite,已提供 |
| requestAnimationFrame | Component.update(dt),已提供 |
| pointerup | TOUCH_END,已提供 |
| HTML 按钮、CSS 布局 | Button、Label、Widget、Layout |
| 浏览器 localStorage | 存储适配,可使用 sys.localStorage |
| 文件上传/下载 | Creator 资源系统或目标平台文件接口 |
| CSS 纯色页面背景 | 使用共享 background token 的 Graphics 或纯色 Sprite |
| CSS 面板与图标 | CandyUiSkin 的纯色 PNG、圆角 Mask 与图标 Sprite |
Cocos 示例提供按钮旋转;若需要同样的边缘拖动手势,可复用 `app.js` 中的角度差判定,接到 TOUCH_START/TOUCH_END。文件接口不属于游戏规则,不应直接复制浏览器 `app.js` 到原生环境。
## 验证限制
已核对本机 Creator 3.8.8 的相关接口,适配脚本通过 TypeScript 语法检查。当前交付不是完整 Creator 工程,尚未运行 Creator 场景或完成 Android/iOS 构建。接入后需验证相机缩放、SpriteFrame 裁切、触摸、Mask、目标平台存储与性能。静态轨道可单独缓存,降低动画时的绘制开销。
## 图片 UI 移植
配色与组件规范以 [Design.md](../Design.md) 为准。`src/design-tokens.js` 是颜色数值的唯一来源;改色后在网页工程运行 `npm run design`,再同步生成的 `design/tokens.json` 与 `assets/ui-surfaces.png`。`src/ui-skin.js` 依赖该 token 模块,并提供网页和 Cocos 共用的图集路径、尺寸和裁切矩形。
| 导入资源 | 规格与用途 |
| --- | --- |
| `assets/ui-surfaces.png` | 416×32,单行 13 个 32×32 纯色色块;颜色由 token 烘入 PNG |
| `assets/ui-icons-generated.png` | 1254×1254,用户选定的生成白色图标原图;按 `ICONS` 的正方形矩形采样 |
| `src/ui-skin.js` | `PANELS`、`ICONS` 与 `UI_REFERENCE` 的共用定义;不可手抄旧版裁切坐标 |
`CandyUiSkin` 挂在 UITransform 节点上,将两张图集的 Texture2D 分别赋给 `panels` 和 `icons`。`panel` 默认是 `surface`,`corner` 默认 14;弹窗设为 24,小角标设为 8。面板使用 `Sprite.Type.SIMPLE` 拉伸纯色色块,通过 `Mask.Type.GRAPHICS_STENCIL` 和 `Graphics.roundRect` 保持圆角;`PANELS` 的 `cap` 为 0,不采用九宫格。Sprite 保持原色,不再加 Color tint 或亮度滤镜。
面板角色包括 `surface`、`primary`、`secondary`、`utility`、`warning`、`reward`、`input`、`selection`、`disabled` 和四个 `tileBlue/tilePink/tileYellow/tilePurple` 色块。常规面板用 `surface`,下一关和当前关卡用 `primary`,次级动作与普通关卡用 `secondary`,选关、设置和道具图标底板统一用 `utility`。Label 颜色读取 token 中的配对角色,例如 `primary/onPrimary`、`surface/textPrimary`。
图标由 `icon` 名称选择,组件按原裁切比例适配节点并居中,不能拉伸为长方形。需要不同面板和图标尺寸时,分别使用背景节点与居中的图标子节点;图标子节点将 `panel` 留空。左右转只放大生成箭头并在下方放置 Label,同样将 `panel` 留空,不创建按钮底板。原图细节和颜色保持不变;轮廓阴影属于额外原生渲染效果,当前组件未实现,需单独对齐网页并验收。
尺寸及锚点变化会自动更新裁切形状与图标位置;运行时修改 `panel`、`icon`、`corner` 或纹理后调用 `refresh()`。组件复用至多两个 SpriteFrame,并在销毁时释放,不要在每次缩放时另外创建图集帧。按钮交互由宿主实现:禁用需同时关闭 Button 交互,若对齐网页透明度则用 UIOpacity 140(约 55%);按压在 150ms 内缩放至 94%,松开恢复。弹窗遮罩、禁止后方交互、入场动画和音效也由原生宿主实现,皮肤组件不负责这些行为。
Canvas 使用参考设计分辨率 390×844、适配宽度。`docs/ui-layout.json` 是旧版界面的历史坐标快照,其中包含已经移除的控件和旧面板名称,不能直接作为当前场景模板。若从当前网页重新量取左上角矩形,转换到 Cocos 居中锚点:x = rect.x + rect.width/2 - 195;y = 422 - rect.y - rect.height/2。所有动态文案仍用 Label;统一字体资源后再检查字宽。轨道与路径为运行时玩法几何,继续使用共享 renderer。
旧的 `ui-panels.png`、`ui-icons.png`、`ui-icons-flat.png`、`board-tray.png`、背景图和 Logo 均不属于当前 UI 导入清单。生成图标只做登记矩形采样,未对原 PNG 承诺像素级去噪,需检查实机透明边缘。当前已验证网页,Cocos 原生场景尚未完成视觉验收。
## 哑光版本与碰撞反馈
背景使用 `THEME.background`(当前 #A8BECF),移除旧背景图片节点。棋子采用 tiles-matte-clean-v8.png,共享 src/sprites.js 裁切。src/collision-animation.js 是 renderer.js 的依赖,也必须复制进 Cocos。碰撞总长 1.25 秒,前 42% 接近,42%–64% 接触停顿及双棋子反馈,最后 36% 原路退回。不会修改阻挡棋子的模型坐标;视觉接触位置依据 Sprite 外形提前截断,避免相互穿插。网页减少动态效果模式下取消阻挡棋子震动。
## 干净边缘与背景
背景采用冷蓝灰 `background`,棋盘采用暖白 `board`(当前 #F2EEE5),轨道与分格也读取同一 token 模块。棋子 PNG 只取每个色块中央 20% 的干净区域,运行时通过 `src/tile-shape.js` 的圆角正六边形遮罩显示;网页 Canvas.clip 和 Cocos Mask.GRAPHICS_STENCIL 共用轮廓。必须复制 tile-shape.js。原图边缘仍有生成杂点,不要直接显示整张原图或旧裁切矩形。
+72
View File
@@ -0,0 +1,72 @@
# 关卡、旋转、碰撞与难度
## 数据格式
平顶六边形使用轴向 `(q,r)`,逻辑 y 轴向下。中心为 `x=1.5qR`、`y=√3(r+q/2)R`。
| 方向 | 朝向 | 邻格变化 |
| --- | --- | --- |
| 0 | ↑ | (0,-1) |
| 1 | ↗ | (1,-1) |
| 2 | ↘ | (1,0) |
| 3 | ↓ | (0,1) |
| 4 | ↙ | (-1,1) |
| 5 | ↖ | (-1,0) |
```json
{
"version": 1,
"id": "my-level",
"name": "转个方向",
"rotationEnabled": true,
"moveLimit": 3,
"cells": [{ "q": 0, "r": 0, "tracks": [3, 4, 5, 0, 1, 2] }],
"tiles": [{ "id": "t0", "q": 0, "r": 0, "dir": 1, "color": 0 }]
}
```
`tracks[i]` 为进入 i 端口后使用的出口,必须满足 `tracks[tracks[i]]===i` 且不能与自己相连。六端口共有 15 种完整配对。`color` 为 0–3。`rotationEnabled` 可选,默认开启;旧版 JSON 仍可导入,但会按新的碰撞规则重新验证。
导入限制:1–61 格、坐标半径不超过 4、步数 1–999。编辑器提供半径 2/3 的常用板型。底板不具备六重旋转对称性时,禁用整体旋转,避免棋子旋转后悬空。
## 旋转是真实的规则变化
顺时针一步将所有底板格子变为 `(q,r) → (-r,q+r)`,同时将轨道端口和对应出口都加 1(模 6)。上层棋子的 `(q,r,dir)` 均不改变。六次旋转恢复原状。
每次旋转扣 1 步。撤销保存并恢复底板、棋子、步数、朝向和旋转计数。重开读取原始关卡,不能从已旋转的中间状态重开。
## 连续碰撞
`path-math.js` 同时服务渲染与碰撞:每条三次贝塞尔轨道采样为 20 段,碰撞针对这些实际绘制的折线进行连续扫掠,而不是仅检查当前动画帧的位置。
每个棋子的顶面碰撞体为半径 `0.88R` 的平顶六边形,阴影和视觉侧壁不计入碰撞。两个同向六边形的 Minkowski 和是半径 `1.76R` 的六边形。将移动中心线的每段与此凸六边形做半平面裁剪,即可得到最早接触时刻。长段移动也不会穿透。
例如:移动棋子在 `(1,1)` 向上走,另一棋子在 `(0,0)`。路线经过的格子不含 `(0,0)`,但经过相邻两格之间时,移动棋子的侧面可能擦到它,因此仍被阻挡。这比仅检查路线格子占用更严格。
判定发生在移动前,动画只展示结果:成功沿线离场;失败走到接触位置再退回。不允许多个棋子同时运动,避免时序歧义。生成器、求解器、提示与工坊都调用同一个 `trace()`。
## 旋转求解与生成
移走可行棋子只会减少障碍,不改变轨道,因此在任意朝向下先移除所有安全棋子不会损害后续可解性。求解器对“朝向+剩余棋子集合”做 BFS,每次转盘代价为 1,到达一个朝向后先消除所有可离场棋子。
最短步数 = 初始棋子数 + 最少旋转数。BFS 返回混合 `tap` / `rotate` 动作序列。最大扩展状态数为 20,000;若触及上限,则返回 `exhausted` 表示尚未确定,不能宣称无解。
关卡生成先在碰撞规则下逆向放入可离场棋子,再旋转初始底盘,筛选必须旋转才能完成的布局,最后重新求解验证。主线按评分非递减排列,共 24 关,必要旋转 1–3 次。
## 数字化难度(0–100)
设 N=棋子数、T=忽略占用的平均转弯数、W=解答中的消除层数、F=初始无碰撞可离场数量、R=最少旋转数、K=N+R=最短步数、M=上限:
```text
规模 = min(1, N/37) × 15
弯道追踪 = min(1, T/5) × 20
解锁层数 = min(1, max(0,W-1)/9) × 20
路线阻塞 = (1-F/N) × 15
必要旋转 = min(1, R/4) × 20
步数压力 = clamp(1-(M-K)/max(1,0.4N), 0, 1) × 10
总分 = 四舍五入各项未取整值之和
```
空关卡为 0 分且不能判为有效可解关卡。不可解、搜索未确定和步数不足分别展示。分项单独取整,显示值之和可能与总分略有差异。
0–29 轻松、30–49 进阶、50–69 烧脑、70–100 挑战。目前 24 关范围为 25–75。它是用于关卡排序的结构估计,没有经过玩家通关率标定;后续可用首次成功率、碰撞率、提示率、旋转次数、撤销率与耗时校准。
+21
View File
@@ -0,0 +1,21 @@
{
"designResolution": {"width":390,"height":844},
"origin":"top-left",
"note":"Browser reference at 390x844. Native Labels require the same bundled font for matching metrics.",
"nodes": {
"map-button":{"x":21,"y":19,"width":43,"height":43,"panel":"white","icon":"grid","corner":10},
"help-button":{"x":326,"y":19,"width":43,"height":43,"panel":"white","icon":"gear","corner":10},
"difficulty-button":{"x":299.3,"y":86.5,"width":73.7,"height":24.5,"panel":"white","corner":6},
"moves":{"x":181.2,"y":171.75,"width":19.6,"height":33,"type":"Label"},
"remaining":{"x":70,"y":176.2,"width":24.45,"height":21,"type":"Label"},
"orientation":{"x":305,"y":176.2,"width":14.66,"height":21,"type":"Label"},
"board":{"x":22.5,"y":220.5,"width":345,"height":345},
"rotate-left":{"x":28,"y":597.5,"width":128,"height":54,"panel":"purple","icon":"left","corner":15},
"rotate-right":{"x":234,"y":597.5,"width":128,"height":54,"panel":"purple","icon":"right","corner":15},
"undo-button":{"x":64.5,"y":674.5,"width":61,"height":75,"icon":"undo","panelChild":{"width":57,"height":52,"panel":"blue"}},
"reset-button":{"x":164.5,"y":674.5,"width":61,"height":75,"icon":"restart","panelChild":{"width":57,"height":52,"panel":"pink"}},
"hint-button":{"x":264.5,"y":674.5,"width":61,"height":75,"icon":"bulb","panelChild":{"width":57,"height":52,"panel":"gold"}},
"play-tab":{"x":35,"y":775.5,"width":58,"height":55.5,"panel":"white","icon":"gem","corner":12},
"edit-tab":{"x":291,"y":775.5,"width":64,"height":55.5,"panel":"white","icon":"edit","corner":12}
}
}
+11
View File
@@ -0,0 +1,11 @@
# 参考玩法与实现规则
参考:`/Users/wukaibing/Grok/dailyrun/x-videos-index/videos/53-x282665-tiles.mp4`,约 15.06 秒,720×900,60 fps。通过每 0.5 秒抽帧及清晰帧观察箭头沿弯轨离场、途中自动转向、离场后露出轨道以及顶部步数变化。
根据用户补充,玩法还包含旋转棋盘改变路线,以及运动途中不能碰撞其他棋子。当前实现将整块轨道底盘每次旋转 60°,上层棋子位置和箭头不变;底板格子与轨道一起旋转,形成不同路线。可使用左右按钮或沿棋盘边缘拖动。
棋子的整个顶面参与沿途碰撞检测;即使经过的格子为空,途中擦碰邻近棋子也不能通过。碰撞时显示沿线接触和回退动画,判定由共享数学模型完成。
片段没有完整展示所有底板、胜负规则、扣步规则和完整旋转过程。四种颜色只负责区分,轨道交叉处不换轨;每次点击或旋转扣 1 步,失败点击也扣步,撤销恢复状态。这些细节是明确的实现约定。内置关卡为重新设计的可解关卡,不声称复原了被棋子遮住的全部原始轨道。
美术以生成式 PNG 图集、背景和立体标题实现。棋子使用厚侧壁、倒角和高光,箭头单独绘制,移动转向不改变棋子材质的光照方向。UI 使用游戏 HUD、转盘按钮、道具栏和关卡弹窗。没有将原视频截图作为游戏贴图。