Initial Cocos Creator port of main-site Unity WebGL game.

Includes core gameplay, 600 exported levels, visual assets, web bridge, and bootstrap scene.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-22 14:57:46 +08:00
commit cba5105908
88 changed files with 13798 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
#///////////////////////////
# Cocos Creator 3D Project
#///////////////////////////
library/
temp/
local/
build/
profiles/
native
#//////////////////////////
# NPM
#//////////////////////////
node_modules/
#//////////////////////////
# VSCode
#//////////////////////////
.vscode/
#//////////////////////////
# WebStorm
#//////////////////////////
.idea/

18
AGENTS.md Normal file
View File

@@ -0,0 +1,18 @@
# Cocos 主站 tfrh001 — Agent 指南
- 引擎:**Cocos Creator 3.8.8**
- 源 Unity 项目:`/Users/liuyufei/tfrh/主站文件/主站`
- 设计对齐 Unity `Platformer.*` 命名与 JS API
## 开发约束
1. 关卡只改 `assets/scripts/level/LevelRegistry.ts` 或新增 JSON勿硬编码在 GameManager。
2. 保持 `SendMessage` 对象名GameController、Player、Vehicle、UIMain。
3. 移动规则只改 `assets/scripts/core/Define.ts`
4. 对外回调用 `JsBridge.call(name, jsonString)`
## 关键文件
- `AppBootstrap.ts` — 场景自动搭建
- `GameController.ts` — JS SendMessage 入口
- `gameplay/Movement.ts` — 移动队列与规则

75
README.md Normal file
View File

@@ -0,0 +1,75 @@
# tfrh001 — 主站网格编程游戏Cocos Creator 版)
Unity「主站」移植工程**Cocos Creator 3.8.8** + **Web**API 与 `unityInstance.SendMessage` 兼容。
## 当前进度
| 项 | 状态 |
|----|------|
| 核心玩法 / JS 桥 | ✅ |
| Unity Levels600600 关) | ✅ 已导出 |
| 丝路主题 Sprite | ✅ player/ship/coin/tile |
| 主站 Web 对接模板 | ✅ `web-template/` |
## 快速开始
1. Cocos Creator 3.8.8 打开本目录
2. 新建场景 → 空节点挂 **AppBootstrap** → 存 `assets/scenes/main.scene` → 设为启动场景
3. 播放预览
## 关卡
- **600 关**`assets/scripts/level/levels-600.generated.ts`(由 `tools/export_unity_levels.py` 生成)
- 注册入口:`assets/scripts/level/LevelRegistry.ts`
- 重新导出:
```bash
python3 tools/export_unity_levels.py \
--input "/path/to/Unity/Assets/Scripts/Core/Levels600.cs" \
--output assets/scripts/level/levels-600.generated.ts
# 或
bash tools/export_all.sh "/path/to/Unity项目"
```
## 贴图资源
已复制至 `assets/resources/textures/`(需在编辑器中刷新资源):
- `silu/player_F.png`, `player_B.png`, `ship_F.png`, `ship_B.png`, `Baseblock.png`
- `ui/coin.png`, `ui/bg.png`
加载逻辑:`assets/scripts/visual/VisualAssets.ts`
## 主站 Web 联调
1. **构建发布** → Web Desktop → 得到 `build/web-desktop/`
2. 阅读 [`web-template/主站对接说明.md`](web-template/主站对接说明.md)
3. 使用 [`web-template/main-site.html`](web-template/main-site.html) + [`cocos-bridge.js`](web-template/cocos-bridge.js) 联调
```javascript
// 与 Unity 完全相同
unityInstance.SendMessage("GameController", "SwitchLevel", 21);
unityInstance.SendMessage("Player", "CallMove", 2);
unityInstance.SendMessage("Player", "CallPlayerInfo");
```
## 目录
```
assets/scripts/ 游戏逻辑
assets/resources/ 贴图Sprite
assets/scripts/level/levels-600.generated.ts
web-template/ 主站对接
tools/ 关卡导出脚本
```
## 源项目
`/Users/liuyufei/tfrh/主站文件/主站`
## 后续
- 运行 `tools/export_all.sh` 导出 Levels1000、Levels10000 等
- 将 Unity 动画改为 Cocos Animation / Spine
- 用 Tiled 导出真实地块填充 `LevelConfig.ground`

14
assets/resources.meta Normal file
View File

@@ -0,0 +1,14 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e4dc033d-0f14-4dfc-8475-f3f23d26d6dd",
"files": [],
"subMetas": {},
"userData": {
"isBundle": true,
"bundleConfigID": "default",
"bundleName": "resources",
"priority": 8
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "d81d86b4-23de-4bc2-a3a5-82add6f6e534",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "cd9ff0f5-a276-4990-9d03-407d44cf21e9",
"files": [],
"subMetas": {},
"userData": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1,42 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "5625da25-9915-416f-be60-c6decb355672",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "5625da25-9915-416f-be60-c6decb355672@6c48a",
"displayName": "Baseblock",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "repeat",
"wrapModeT": "repeat",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true,
"imageUuidOrDatabaseUri": "5625da25-9915-416f-be60-c6decb355672",
"visible": false
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "texture",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "5625da25-9915-416f-be60-c6decb355672@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,42 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "fa305708-508c-4b5d-8ddc-f834daf89cfd",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "fa305708-508c-4b5d-8ddc-f834daf89cfd@6c48a",
"displayName": "player_B",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "repeat",
"wrapModeT": "repeat",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true,
"imageUuidOrDatabaseUri": "fa305708-508c-4b5d-8ddc-f834daf89cfd",
"visible": false
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "texture",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "fa305708-508c-4b5d-8ddc-f834daf89cfd@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,42 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "369d2601-41a6-41a4-8804-c59fb1ab1eef",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "369d2601-41a6-41a4-8804-c59fb1ab1eef@6c48a",
"displayName": "player_F",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "repeat",
"wrapModeT": "repeat",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true,
"imageUuidOrDatabaseUri": "369d2601-41a6-41a4-8804-c59fb1ab1eef",
"visible": false
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "texture",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "369d2601-41a6-41a4-8804-c59fb1ab1eef@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,42 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "6d61e1e0-435a-490e-af33-addbe85ee32d",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "6d61e1e0-435a-490e-af33-addbe85ee32d@6c48a",
"displayName": "ship_B",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "repeat",
"wrapModeT": "repeat",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true,
"imageUuidOrDatabaseUri": "6d61e1e0-435a-490e-af33-addbe85ee32d",
"visible": false
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "texture",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "6d61e1e0-435a-490e-af33-addbe85ee32d@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,42 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "6d5f9230-ee4e-46c2-aaef-12077b6936fc",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "6d5f9230-ee4e-46c2-aaef-12077b6936fc@6c48a",
"displayName": "ship_F",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "repeat",
"wrapModeT": "repeat",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true,
"imageUuidOrDatabaseUri": "6d5f9230-ee4e-46c2-aaef-12077b6936fc",
"visible": false
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "texture",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "6d5f9230-ee4e-46c2-aaef-12077b6936fc@6c48a"
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "9310a763-9c47-4200-8140-97ff97d09e11",
"files": [],
"subMetas": {},
"userData": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@@ -0,0 +1,42 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "2b1adaab-2489-4ff0-8125-665717c6275a",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "2b1adaab-2489-4ff0-8125-665717c6275a@6c48a",
"displayName": "bg",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "repeat",
"wrapModeT": "repeat",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true,
"imageUuidOrDatabaseUri": "2b1adaab-2489-4ff0-8125-665717c6275a",
"visible": false
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "texture",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "2b1adaab-2489-4ff0-8125-665717c6275a@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,42 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "f2192a39-42df-419b-9ef0-06b4a35fb223",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "f2192a39-42df-419b-9ef0-06b4a35fb223@6c48a",
"displayName": "coin",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "repeat",
"wrapModeT": "repeat",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true,
"imageUuidOrDatabaseUri": "f2192a39-42df-419b-9ef0-06b4a35fb223",
"visible": false
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "texture",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "f2192a39-42df-419b-9ef0-06b4a35fb223@6c48a"
}
}

9
assets/scenes.meta Normal file
View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e990322a-3266-4055-842e-e674f66f6fa3",
"files": [],
"subMetas": {},
"userData": {}
}

494
assets/scenes/main.scene Normal file
View File

@@ -0,0 +1,494 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "main",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"scene": {
"__id__": 1
}
},
{
"__type__": "cc.Scene",
"_name": "main",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 5
},
{
"__id__": 7
}
],
"_active": true,
"_components": [],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 9
},
"_id": "d071e7d3-2dc4-4815-8cb8-c258c4b7c515"
},
{
"__type__": "cc.Node",
"_name": "Main Light",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": -0.06397656665577071,
"y": -0.44608233363525845,
"z": -0.8239028751062036,
"w": -0.3436591377065261
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": -117.894,
"y": -194.909,
"z": 38.562
},
"_id": "d381mi5H1Nyq4+wgo0zLCX"
},
{
"__type__": "cc.DirectionalLight",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 250,
"b": 240,
"a": 255
},
"_useColorTemperature": false,
"_colorTemperature": 6550,
"_staticSettings": {
"__id__": 4
},
"_visibility": -325058561,
"_illuminanceHDR": 65000,
"_illuminance": 65000,
"_illuminanceLDR": 1.6927083333333335,
"_shadowEnabled": false,
"_shadowPcf": 0,
"_shadowBias": 0.00001,
"_shadowNormalBias": 0,
"_shadowSaturation": 1,
"_shadowDistance": 50,
"_shadowInvisibleOcclusionRange": 200,
"_csmLevel": 4,
"_csmLayerLambda": 0.75,
"_csmOptimizationMode": 2,
"_csmAdvancedOptions": false,
"_csmLayersTransition": false,
"_csmTransitionRange": 0.05,
"_shadowFixedArea": false,
"_shadowNear": 0.1,
"_shadowFar": 10,
"_shadowOrthoSize": 5,
"_id": "b4ITpe9cBLQ7QkVb+TkXWc"
},
{
"__type__": "cc.StaticLightSettings",
"_baked": false,
"_editorOnly": false,
"_castShadow": false
},
{
"__type__": "cc.Node",
"_name": "Main Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 6
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": -10,
"y": 10,
"z": 10
},
"_lrot": {
"__type__": "cc.Quat",
"x": -0.27781593346944056,
"y": -0.36497167621709875,
"z": -0.11507512748638377,
"w": 0.8811195706053617
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": -35,
"y": -45,
"z": 0
},
"_id": "45wksGP0VK44RZQ6l+OECN"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 5
},
"_enabled": true,
"__prefab": null,
"_projection": 1,
"_priority": 0,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 10,
"_near": 1,
"_far": 1000,
"_color": {
"__type__": "cc.Color",
"r": 51,
"g": 51,
"b": 51,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 14,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 1822425087,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "80VFXpZJ9E8qFokxruq7p4"
},
{
"__type__": "cc.Node",
"_name": "AppRoot",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 8
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "4fu9hupTlJ84UrhzTAt969"
},
{
"__type__": "c0468XFPX1InLN14gtZ5PLf",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 7
},
"_enabled": true,
"__prefab": null,
"_id": "eeKeOJWZBOzI20RcHfalSe"
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 10
},
"shadows": {
"__id__": 11
},
"_skybox": {
"__id__": 12
},
"fog": {
"__id__": 13
},
"octree": {
"__id__": 14
},
"skin": {
"__id__": 15
},
"lightProbeInfo": {
"__id__": 16
},
"postSettings": {
"__id__": 17
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
{
"__type__": "cc.AmbientInfo",
"_skyColorHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyColor": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyIllumHDR": 20000,
"_skyIllum": 20000,
"_groundAlbedoHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_groundAlbedo": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_skyColorLDR": {
"__type__": "cc.Vec4",
"x": 0.452588,
"y": 0.607642,
"z": 0.755699,
"w": 0
},
"_skyIllumLDR": 0.8,
"_groundAlbedoLDR": {
"__type__": "cc.Vec4",
"x": 0.618555,
"y": 0.577848,
"z": 0.544564,
"w": 0
}
},
{
"__type__": "cc.ShadowsInfo",
"_enabled": false,
"_type": 0,
"_normal": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1,
"z": 0
},
"_distance": 0,
"_planeBias": 1,
"_shadowColor": {
"__type__": "cc.Color",
"r": 76,
"g": 76,
"b": 76,
"a": 255
},
"_maxReceived": 4,
"_size": {
"__type__": "cc.Vec2",
"x": 1024,
"y": 1024
}
},
{
"__type__": "cc.SkyboxInfo",
"_envLightingType": 0,
"_envmapHDR": {
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_envmap": {
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_envmapLDR": {
"__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_diffuseMapHDR": null,
"_diffuseMapLDR": null,
"_enabled": true,
"_useHDR": true,
"_editableMaterial": null,
"_reflectionHDR": null,
"_reflectionLDR": null,
"_rotationAngle": 0
},
{
"__type__": "cc.FogInfo",
"_type": 0,
"_fogColor": {
"__type__": "cc.Color",
"r": 200,
"g": 200,
"b": 200,
"a": 255
},
"_enabled": false,
"_fogDensity": 0.3,
"_fogStart": 0.5,
"_fogEnd": 300,
"_fogAtten": 5,
"_fogTop": 1.5,
"_fogRange": 1.2,
"_accurate": false
},
{
"__type__": "cc.OctreeInfo",
"_enabled": false,
"_minPos": {
"__type__": "cc.Vec3",
"x": -1024,
"y": -1024,
"z": -1024
},
"_maxPos": {
"__type__": "cc.Vec3",
"x": 1024,
"y": 1024,
"z": 1024
},
"_depth": 8
},
{
"__type__": "cc.SkinInfo",
"_enabled": true,
"_blurRadius": 0.01,
"_sssIntensity": 3
},
{
"__type__": "cc.LightProbeInfo",
"_giScale": 1,
"_giSamples": 1024,
"_bounces": 2,
"_reduceRinging": 0,
"_showProbe": true,
"_showWireframe": true,
"_showConvex": false,
"_data": null,
"_lightProbeSphereVolume": 1
},
{
"__type__": "cc.PostSettingsInfo",
"_toneMappingType": 0
}
]

View File

@@ -0,0 +1,11 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "d071e7d3-2dc4-4815-8cb8-c258c4b7c515",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,8 @@
在 Cocos Creator 3.8.8 中:
1. 新建场景 → 保存为 main.scene与本目录同级
2. 创建空节点 AppRoot添加组件 AppBootstrap脚本路径 assets/scripts/AppBootstrap.ts
3. 项目设置中将 main 设为启动场景
4. 点击播放即可
无需手动创建 GameController / PlayerAppBootstrap 会自动搭建。

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "2e31b18a-7901-449d-b1c1-d2a5c09dd773",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

9
assets/scripts.meta Normal file
View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "8254b8f8-57eb-4bc8-ab6d-062a44e75e22",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,117 @@
import {
_decorator, Component, Node, Canvas, Camera, UITransform, view, Color,
director, Label, find,
} from 'cc';
import { GameController } from './GameController';
import { GameManager } from './manager/GameManager';
import { VisualAssets } from './visual/VisualAssets';
const { ccclass, executionOrder } = _decorator;
/** 每格像素尺寸UI 坐标) */
export const CELL_PIXEL = 56;
@ccclass('AppBootstrap')
@executionOrder(-100)
export class AppBootstrap extends Component {
async onLoad() {
try {
await this.bootstrap();
} catch (e) {
console.error('[AppBootstrap] 初始化失败', e);
}
}
private async bootstrap() {
console.log('[AppBootstrap] 开始初始化…');
await VisualAssets.preload();
const scene = director.getScene()!;
let mainCam = find('Main Camera', scene)?.getComponent(Camera) ?? null;
if (!mainCam) {
const camNode = new Node('Main Camera');
camNode.parent = scene;
mainCam = camNode.addComponent(Camera);
}
this.setupCamera(mainCam);
const light = find('Main Light', scene);
if (light) light.active = false;
let canvasNode = find('Canvas', scene);
if (!canvasNode) {
canvasNode = new Node('Canvas');
canvasNode.parent = scene;
canvasNode.addComponent(Canvas);
}
const canvas = canvasNode.getComponent(Canvas)!;
canvas.cameraComponent = mainCam;
let canvasUi = canvasNode.getComponent(UITransform);
if (!canvasUi) canvasUi = canvasNode.addComponent(UITransform);
const size = view.getVisibleSize();
canvasUi.setContentSize(size.width, size.height);
let gameRoot = canvasNode.getChildByName('GameRoot');
if (!gameRoot) {
gameRoot = new Node('GameRoot');
gameRoot.parent = canvasNode;
const grUi = gameRoot.addComponent(UITransform);
grUi.setContentSize(size.width, size.height);
}
let gcNode = scene.getChildByName('GameController');
if (!gcNode) {
gcNode = new Node('GameController');
gcNode.parent = scene;
gcNode.addComponent(GameManager);
gcNode.addComponent(GameController);
}
const gctl = gcNode.getComponent(GameController);
const gm = gcNode.getComponent(GameManager)!;
if (gctl) {
if (gctl.mainLevelEntrance) gm.mainLevelEntrance = gctl.mainLevelEntrance;
gm.initialLevelID = gctl.initialLevelID;
gm.playerSkin = gctl.playerSkin;
}
let entrance = gameRoot.getChildByName('MainLevelEntrance');
if (!entrance) {
entrance = new Node('MainLevelEntrance');
entrance.parent = gameRoot;
const eUi = entrance.addComponent(UITransform);
eUi.setContentSize(size.width, size.height);
}
gm.mainLevelEntrance = entrance;
gm.initialLevelID = 1;
this.ensureHint(canvasNode);
await gm.createNewLevel(gm.initialLevelID);
console.log('[AppBootstrap] 关卡已加载');
}
private setupCamera(cam: Camera) {
const camNode = cam.node;
camNode.setPosition(0, 0, 1000);
camNode.setRotationFromEuler(0, 0, 0);
cam.projection = Camera.ProjectionType.ORTHO;
cam.orthoHeight = 360;
cam.near = 1;
cam.far = 2000;
cam.clearFlags = Camera.ClearFlag.SOLID_COLOR;
cam.clearColor = new Color(30, 40, 60, 255);
}
private ensureHint(canvasNode: Node) {
if (canvasNode.getChildByName('Hint')) return;
const hint = new Node('Hint');
hint.parent = canvasNode;
const ui = hint.addComponent(UITransform);
ui.setContentSize(400, 40);
hint.setPosition(0, 280, 0);
const label = hint.addComponent(Label);
label.string = '主站 Cocos · 关卡运行中';
label.fontSize = 22;
label.color = new Color(200, 220, 255);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c04685c5-3d7d-489c-b375-e20b59e4f2df",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,275 @@
import {
_decorator, Component, Node, Enum, CCString, CCInteger, CCBoolean, game,
} from 'cc';
import { GameManager } from './manager/GameManager';
import { Skin } from './core/Define';
import { VisualAssets } from './visual/VisualAssets';
import { getLevelCount } from './level/LevelRegistry';
const { ccclass, property, executionOrder } = _decorator;
/** UI 主题(与 Unity ChangeUIStyle 参数一致) */
export enum UIStyleType {
default = 0,
chinese = 1,
redArmy = 2,
numMan = 3,
snow = 4,
sanxing = 5,
}
const UIStyleNames = ['default', 'chinese', 'redArmy', 'numMan', 'snow', 'sanxing'];
/** 多人角色 */
export enum MultRole {
PlayerA1 = 0,
PlayerA2 = 1,
PlayerA3 = 2,
PlayerB1 = 3,
PlayerB2 = 4,
PlayerB3 = 5,
}
const MultRoleNames = ['PlayerA1', 'PlayerA2', 'PlayerA3', 'PlayerB1', 'PlayerB2', 'PlayerB3'];
/**
* 对应 Unity 场景 GameController + Inspector 调试面板TestGame2
* 挂在 GameController 节点,与 GameManager 同级或同节点
*/
@ccclass('GameController')
@executionOrder(-50)
export class GameController extends Component {
@property({ group: { name: '场景', id: '1' }, type: Node, displayName: 'Main Level Entrance' })
mainLevelEntrance: Node | null = null;
@property({ group: { name: '场景', id: '1' }, type: Node, displayName: 'Cur Level', readonly: true })
curLevelNode: Node | null = null;
@property({ group: { name: '关卡', id: '2' }, type: CCInteger, displayName: 'Initial Level ID' })
initialLevelID = 1;
@property({ group: { name: '关卡', id: '2' }, type: CCInteger, displayName: 'Cur Level ID', readonly: true })
curLevelID = 1;
@property({ group: { name: '关卡', id: '2' }, displayName: '已注册关卡数', readonly: true })
registeredLevelCount = 0;
@property({ group: { name: '角色', id: '3' }, type: Enum(Skin), displayName: 'Player Skin' })
playerSkin: Skin = Skin.Silu;
@property({ group: { name: '多人', id: '4' }, displayName: 'Mult Mode' })
multMode = false;
@property({ group: { name: '多人', id: '4' }, type: Enum(MultRole), displayName: 'Mult Player Role' })
multPlayerRoleEnum: MultRole = MultRole.PlayerA1;
@property({ group: { name: '主题', id: '5' }, type: Enum(UIStyleType), displayName: 'UI Style' })
uiStyleEnum: UIStyleType = UIStyleType.default;
// --- 与 Unity Inspector 输入框一致 ---
@property({ group: { name: '调试输入', id: '6' }, displayName: 'inputLevel' })
inputLevel = '1';
@property({ group: { name: '调试输入', id: '6' }, displayName: 'inputStyle' })
inputStyle = 'default';
@property({ group: { name: '调试输入', id: '6' }, multiline: true, displayName: 'coinStr' })
coinStr = '["-2,0","-2,2","-1,2"]';
// --- 播放时勾选即执行(等同 Unity 按钮) ---
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ SwitchLevel' })
execSwitchLevel = false;
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ EndInput' })
execEndInput = false;
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ ChangeStyle' })
execChangeStyle = false;
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ StartMultPlay' })
execStartMultPlay = false;
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ Mute' })
execMute = false;
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ Unmute' })
execUnmute = false;
private gm!: GameManager;
onLoad() {
this.gm = this.getComponent(GameManager) || this.node.getComponentInChildren(GameManager)!;
if (!this.gm) {
this.gm = this.node.addComponent(GameManager);
}
this.syncToManager();
this.registerWebApi();
this.registeredLevelCount = getLevelCount();
}
start() {
this.refreshReadonly();
}
update() {
if (!game.isRunning()) return;
this.refreshReadonly();
this.pollInspectorActions();
}
private refreshReadonly() {
this.curLevelID = this.gm?.curLevelID ?? this.curLevelID;
if (this.gm?.mainLevelEntrance) {
this.mainLevelEntrance = this.gm.mainLevelEntrance;
const lv = this.gm.mainLevelEntrance.children.find((c) => c.name.startsWith('Level_'));
this.curLevelNode = lv ?? null;
}
this.multMode = this.gm?.multMode ?? false;
}
private syncToManager() {
if (!this.gm) return;
if (this.mainLevelEntrance) this.gm.mainLevelEntrance = this.mainLevelEntrance;
this.gm.initialLevelID = this.initialLevelID;
this.gm.playerSkin = this.playerSkin;
this.gm.multMode = this.multMode;
this.gm.multPlayerRole = MultRoleNames[this.multPlayerRoleEnum] ?? 'PlayerA1';
this.gm.uiStyle = UIStyleNames[this.uiStyleEnum] ?? 'default';
this.gm.curLevelID = this.initialLevelID;
}
private pollInspectorActions() {
if (this.execSwitchLevel) {
this.execSwitchLevel = false;
this.onInspectorSwitchLevel();
}
if (this.execEndInput) {
this.execEndInput = false;
this.callSetIsInputEnd(1);
}
if (this.execChangeStyle) {
this.execChangeStyle = false;
this.onInspectorChangeStyle();
}
if (this.execStartMultPlay) {
this.execStartMultPlay = false;
this.onInspectorStartMultPlay();
}
if (this.execMute) {
this.execMute = false;
this.callMute();
}
if (this.execUnmute) {
this.execUnmute = false;
this.callUnmute();
}
}
// --- Inspector / SendMessage 共用 ---
onInspectorSwitchLevel() {
const id = parseInt(this.inputLevel, 10);
if (Number.isNaN(id)) {
console.warn('[GameController] inputLevel 无效:', this.inputLevel);
return;
}
this.syncToManager();
this.gm.switchLevel(id);
this.curLevelID = id;
console.log('[GameController] SwitchLevel', id);
}
onInspectorChangeStyle() {
const style = this.inputStyle.trim() || UIStyleNames[this.uiStyleEnum];
this.gm.changeUIStyle(style);
this.uiStyleEnum = UIStyleNames.indexOf(style) as UIStyleType;
if (this.uiStyleEnum < 0) this.uiStyleEnum = UIStyleType.default;
console.log('[GameController] ChangeUIStyle', style);
}
onInspectorStartMultPlay() {
const role = MultRoleNames[this.multPlayerRoleEnum] ?? 'PlayerA1';
this.gm.startMultPlay(role, this.coinStr);
this.multMode = true;
console.log('[GameController] StartMultPlay', role, this.coinStr);
}
applyPlayerSkinFromInspector() {
const player = this.gm.findNodeByName('Player')
?? this.gm.findNodeByName(this.gm.multPlayerRole);
const pc = player?.getComponent('PlayerController') as { callChangeSkin?: (n: number) => void };
pc?.callChangeSkin?.(this.playerSkin);
this.gm.playerSkin = this.playerSkin;
}
// --- JS Bridge (SendMessage) ---
private registerWebApi() {
const api = {
SendMessage: (objectName: string, methodName: string, param?: string | number) => {
this.sendMessage(objectName, methodName, param);
},
};
if (typeof window !== 'undefined') {
(window as unknown as { cocosIns?: typeof api }).cocosIns = api;
(window as unknown as { unityInstance?: typeof api }).unityInstance = api;
}
}
sendMessage(objectName: string, methodName: string, param?: string | number) {
if (objectName === 'GameController') {
this.invoke(this, methodName, param);
return;
}
if (objectName === 'UIMain') {
const n = this.gm.findNodeByName('UIMain');
const c = n?.getComponent('UIMain');
if (c) this.invoke(c, methodName, param);
return;
}
const node = this.gm.findNodeByName(objectName);
if (!node) {
console.warn(`SendMessage: 未找到 ${objectName}`);
return;
}
for (const comp of node.getComponents(Component)) {
if (typeof (comp as Record<string, unknown>)[methodName] === 'function') {
this.invoke(comp, methodName, param);
return;
}
}
console.warn(`SendMessage: ${objectName}.${methodName} 无此方法`);
}
private invoke(target: object, methodName: string, param?: string | number) {
const fn = (target as Record<string, unknown>)[methodName];
if (typeof fn !== 'function') return;
if (param === undefined) (fn as () => void).call(target);
else (fn as (p: string | number) => void).call(target, param);
}
switchLevel(levelID: number) {
this.inputLevel = String(levelID);
this.onInspectorSwitchLevel();
}
callSetIsInputEnd(v: number) {
this.gm.callSetIsInputEnd(v);
}
changeUIStyle(style: string) {
this.inputStyle = style;
this.onInspectorChangeStyle();
}
startMultPlay(role: string, coins: string) {
this.coinStr = coins;
const idx = MultRoleNames.indexOf(role);
if (idx >= 0) this.multPlayerRoleEnum = idx as MultRole;
this.onInspectorStartMultPlay();
}
callMute() {
this.gm.callMute();
}
callUnmute() {
this.gm.callUnmute();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8940a3d2-2898-4628-a201-13fa8a9c59bb",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "ba5d5aa4-760d-4aae-aef0-83f80c1f2cba",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,28 @@
/**
* 对应 Unity Application.ExternalCall
* Web 环境下调用 window 上的全局回调
*/
declare global {
interface Window {
processData?: (json: string) => void;
processVehicleData?: (json: string) => void;
externalResult?: (json: string) => void;
externalLevelInfo?: (json: string) => void;
coinsData?: (json: string) => void;
[key: string]: unknown;
}
}
export class JsBridge {
static call(callbackName: string, jsonData: string) {
if (typeof window !== 'undefined') {
const fn = window[callbackName];
if (typeof fn === 'function') {
(fn as (json: string) => void)(jsonData);
return;
}
}
console.log(`[JsBridge] ${callbackName}`, jsonData);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d34fa121-5277-49b2-9938-ed0886a31fc4",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6fcf8603-44fa-444f-900d-118529aca8dc",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,213 @@
import { _decorator, Vec3 } from 'cc';
import { Direction, GameState, GridType, Skin } from '../core/Define';
import { EventManager, EventType } from '../core/EventManager';
import { JsBridge } from '../bridge/JsBridge';
import { GameManager } from '../manager/GameManager';
import { Movement } from '../gameplay/Movement';
import { VehicleController } from './VehicleController';
import { VisualAssets } from '../visual/VisualAssets';
const { ccclass } = _decorator;
export interface ExternalData {
position: { x: number; y: number; z: number };
gridType: number;
direction: string;
}
export interface ExternalDataList {
direction: number;
externalDatas: ExternalData[];
}
export interface ExternalResult {
isWin: boolean;
stepNum: number;
direction: number;
isInputEnd: boolean;
}
@ccclass('PlayerController')
export class PlayerController extends Movement {
coins = 0;
private vehicle: VehicleController | null = null;
private sendFinally = false;
onLoad() {
this.moverRole = 'player';
EventManager.register(EventType.LevelInit, this.onLevelInit);
EventManager.register(EventType.InputEnd, this.onInputEnd);
this.coins = 0;
}
onDestroy() {
EventManager.remove(EventType.LevelInit, this.onLevelInit);
EventManager.remove(EventType.InputEnd, this.onInputEnd);
}
start() {
super.start();
VisualAssets.applyPlayerSprite(this.node, this.direction);
if (this.node.name === 'Player' && GameManager.instance) {
this.callChangeSkin(GameManager.instance.playerSkin);
}
}
override setDirection(dir: Direction) {
super.setDirection(dir);
VisualAssets.applyPlayerSprite(this.node, dir);
}
private onLevelInit = () => {
this.checkRide();
};
private onInputEnd = () => {
this.externalCallResult(false);
};
protected onMoveNextSet(isJump: boolean) {
if (isJump && this.targetGridType === GridType.Jump) {
const p = this.node.worldPosition.clone();
p.y += 0.15;
this.targetPosition.set(p);
}
}
onMoving() {
if (this.targetGridType === GridType.None && this.vehicle && !Movement.callEach) {
this.vehicle.setPosition(this.node.worldPosition);
}
}
protected onMoveFail(isJump: boolean) {
this.externalCallResult(false);
const gm = GameManager.instance!;
if (gm.multMode) {
const other = gm.findNodeByName(this.node.name === 'Player' ? 'Enemy' : 'Player');
other?.getComponent(PlayerController)?.externalCallResult(true);
}
console.log(`${this.node.name} 无法移动`, isJump);
}
protected onMoveToTarget() {
if (this.curGrid === GridType.Ride) {
const obj = GameManager.instance!.getGameObject(this.node.worldPosition);
if (obj) {
this.vehicle = obj.getComponent(VehicleController);
this.vehicle?.setPlayer(this);
if (this.vehicle) this.vehicle.setDirection(this.direction);
}
} else if (this.curGrid !== GridType.None && this.vehicle) {
this.vehicle.setPlayer(null);
this.vehicle = null;
}
if (this.vehicle) {
this.vehicle.setPosition(this.node.worldPosition);
GameManager.instance!.removeObj(this.lastPosition);
GameManager.instance!.addObj(this.node.worldPosition, GridType.Ride, this.vehicle.node);
}
this.externalCall();
}
callChangeSkin(n: number) {
if (n < 0 || n > Skin.sanxing) return;
if (GameManager.instance) GameManager.instance.playerSkin = n as Skin;
}
callPlayerInfo() {
this.externalCall();
}
setName(name: string) {
this.vehicle?.setName(name);
}
getCoinsNum() {
JsBridge.call('coinsData', JSON.stringify({ coinsNum: this.coins, gameObjectName: this.node.name }));
}
addCoins() {
this.coins++;
}
setPosition(pos: Vec3) {
this.node.setPosition(pos);
}
/** 供 VehicleController 同步 */
syncFromVehicle(targetGridType: GridType, isJump: boolean) {
this.targetGridType = targetGridType;
this.onMoveNextSet(isJump);
}
syncMoveToTargetFromVehicle() {
this.onMoveToTarget();
}
externalCall() {
const gm = GameManager.instance!;
const list: ExternalDataList = { direction: this.direction, externalDatas: [] };
const self = gm.worldToCell(this.node.worldPosition);
list.externalDatas.push({
position: { x: self.x, y: self.y, z: 0 },
gridType: this.curGrid,
direction: 'self',
});
for (let d = Direction.North; d <= Direction.West; d++) {
const wp = gm.nextGridPosition(this.node.worldPosition, d);
const cell = gm.worldToCell(wp);
list.externalDatas.push({
position: { x: cell.x, y: cell.y, z: 0 },
gridType: gm.calculateNextGridType(this.node.worldPosition, d),
direction: gm.getRelativePosition(this.direction, d),
});
}
const json = JSON.stringify(list);
if (gm.multMode) JsBridge.call(`process${this.node.name}`, json);
else JsBridge.call('processData', json);
}
externalCallResult(isWin: boolean) {
const gm = GameManager.instance!;
if (isWin && (this.node.name === 'Player' || this.node.name === gm.multPlayerRole)) {
/* success audio */
} else if (!isWin && (this.node.name === 'Player' || this.node.name === gm.multPlayerRole)) {
/* fail audio */
}
let myStep = gm.stepNum;
if (gm.multMode) {
switch (gm.multPlayerRole) {
case 'PlayerA1': myStep = gm.stepA1Num; break;
case 'PlayerA2': myStep = gm.stepA2Num; break;
case 'PlayerA3': myStep = gm.stepA3Num; break;
case 'PlayerB1': myStep = gm.stepB1Num; break;
case 'PlayerB2': myStep = gm.stepB2Num; break;
case 'PlayerB3': myStep = gm.stepB3Num; break;
}
}
if ((this.node.name === 'Player' || this.node.name === gm.multPlayerRole) && !this.sendFinally) {
const payload: ExternalResult = {
isWin,
stepNum: myStep,
direction: this.direction,
isInputEnd: gm.isInputEnd,
};
JsBridge.call('externalResult', JSON.stringify(payload));
this.sendFinally = true;
}
if (gm.gameState !== GameState.Run) return;
gm.setGameState(isWin ? GameState.ResultWin : GameState.ResultFail);
}
private checkRide() {
if (this.curGrid === GridType.Ride) {
const obj = GameManager.instance!.getGameObject(this.node.worldPosition);
if (obj) {
this.vehicle = obj.getComponent(VehicleController);
this.vehicle?.setDirection(this.direction);
this.vehicle?.setPlayer(this);
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "93a110e7-a99e-4719-b6c2-d2c2d8469d7f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,59 @@
import { _decorator, Component, Vec3 } from 'cc';
import { GameManager } from '../manager/GameManager';
import { PlayerController } from './PlayerController';
const { ccclass } = _decorator;
@ccclass('PropController')
export class PropController extends Component {
private collected = false;
update() {
if (this.collected || !GameManager.instance) return;
const gm = GameManager.instance;
const players = gm.curLevel?.children.filter((c) => c.name.includes('Player')) ?? [];
const propCell = gm.worldToCell(this.node.worldPosition);
for (const p of players) {
const pc = p.getComponent(PlayerController);
if (!pc) continue;
const pcCell = gm.worldToCell(p.worldPosition);
if (pcCell.x !== propCell.x || pcCell.y !== propCell.y) continue;
this.onCollected(pc);
break;
}
}
private onCollected(player: PlayerController) {
if (this.collected) return;
this.collected = true;
const gm = GameManager.instance!;
player.addCoins();
gm.removeProp(this.node.worldPosition);
const remaining = (gm.curLevel?.children.filter((c) => c.name.includes('Prop') && c !== this.node).length ?? 0);
this.node.destroy();
if (remaining === 0) {
if (gm.multMode) this.resolveMultWin(gm);
else player.externalCallResult(true);
}
}
private resolveMultWin(gm: GameManager) {
const sum = (names: string[]) =>
names.reduce((t, n) => t + (gm.findNodeByName(n)?.getComponent(PlayerController)?.coins ?? 0), 0);
const totalA = sum(['PlayerA1', 'PlayerA2', 'PlayerA3']);
const totalB = sum(['PlayerB1', 'PlayerB2', 'PlayerB3']);
const winA = totalA > totalB || (totalA === totalB && gm.stepA1Num + gm.stepA2Num + gm.stepA3Num <
gm.stepB1Num + gm.stepB2Num + gm.stepB3Num);
const set = (names: string[], win: boolean) => {
for (const n of names) gm.findNodeByName(n)?.getComponent(PlayerController)?.externalCallResult(win);
};
if (winA) {
set(['PlayerA1', 'PlayerA2', 'PlayerA3'], true);
set(['PlayerB1', 'PlayerB2', 'PlayerB3'], false);
} else {
set(['PlayerA1', 'PlayerA2', 'PlayerA3'], false);
set(['PlayerB1', 'PlayerB2', 'PlayerB3'], true);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7b8d3aef-659e-486a-8e93-a08689bed871",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,114 @@
import { _decorator, Vec3 } from 'cc';
import { Direction, GridType } from '../core/Define';
import { EventManager, EventType } from '../core/EventManager';
import { JsBridge } from '../bridge/JsBridge';
import { GameManager } from '../manager/GameManager';
import { Movement } from '../gameplay/Movement';
import { PlayerController, ExternalDataList } from './PlayerController';
import { VisualAssets } from '../visual/VisualAssets';
const { ccclass } = _decorator;
@ccclass('VehicleController')
export class VehicleController extends Movement {
private player: PlayerController | null = null;
onLoad() {
this.moverRole = 'vehicle';
this.moveState = 0;
}
start() {
super.start();
this.setIcon();
}
setPlayer(p: PlayerController | null) {
this.player = p;
}
setPosition(pos: Vec3) {
this.node.setPosition(pos);
}
setName(name: string) {
/* 可挂 Label 显示名称 */
}
override setDirection(dir: Direction) {
super.setDirection(dir);
this.setIcon();
if (Movement.callEach) return;
Movement.callEach = true;
this.player?.setDirection(dir);
Movement.callEach = false;
}
setIcon() {
const style = GameManager.instance?.uiStyle ?? 'default';
VisualAssets.applyVehicleSprite(this.node, this.direction, style);
}
protected onMoveNextSet(isJump: boolean) {
if (Movement.callEach) return;
Movement.callEach = true;
this.player?.syncFromVehicle(this.targetGridType, isJump);
Movement.callEach = false;
}
protected onMoving() {
if (Movement.callEach) return;
Movement.callEach = true;
if (this.player) {
this.player.onMoving();
this.player.setPosition(this.node.worldPosition);
}
Movement.callEach = false;
}
protected onMoveToTarget() {
GameManager.instance!.removeObj(this.lastPosition);
GameManager.instance!.addObj(this.node.worldPosition, GridType.Ride, this.node);
if (Movement.callEach) return;
Movement.callEach = true;
if (this.player) {
this.player.setPosition(this.node.worldPosition);
this.player.syncMoveToTargetFromVehicle();
}
Movement.callEach = false;
this.externalCall();
}
protected onMoveFail(isJump: boolean) {
if (!GameManager.instance!.multMode) {
EventManager.dispatch(EventType.InputEnd, GameManager.instance!.gameState);
}
}
callVehicleInfo() {
this.externalCall();
}
externalCall() {
const gm = GameManager.instance!;
const list: ExternalDataList = { direction: this.direction, externalDatas: [] };
const self = gm.worldToCell(this.node.worldPosition);
list.externalDatas.push({
position: { x: self.x, y: self.y, z: 0 },
gridType: this.curGrid,
direction: 'self',
});
for (let d = Direction.North; d <= Direction.West; d++) {
const wp = gm.nextGridPosition(this.node.worldPosition, d);
const cell = gm.worldToCell(wp);
list.externalDatas.push({
position: { x: cell.x, y: cell.y, z: 0 },
gridType: gm.calculateNextGridType(this.node.worldPosition, d),
direction: gm.getRelativePosition(this.direction, d),
});
}
const json = JSON.stringify(list);
if (gm.multMode) JsBridge.call(`process${this.node.name}`, json);
else JsBridge.call('processVehicleData', json);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "aa054a89-5920-470a-aac7-9af275a2d789",
"files": [],
"subMetas": {},
"userData": {}
}

9
assets/scripts/core.meta Normal file
View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "f398f642-6b7c-47cf-9e92-19d6cf61c05c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,152 @@
/** 与 Unity Platformer.Core.Define 对齐 */
export enum Direction {
North = 0,
East = 1,
South = 2,
West = 3,
}
export enum MoveState {
Idle = 0,
Moving = 1,
}
export enum GridType {
Across = 0,
Jump = 1,
Block = 2,
Ride = 3,
None = 4,
Boundary = 5,
}
export enum Skin {
Silu = 0,
Panda = 1,
RedArmy = 2,
numMan = 3,
snow = 4,
sanxing = 5,
}
export enum GameState {
Run = 0,
ResultWin = 1,
ResultFail = 2,
}
export type MoverRole = 'player' | 'vehicle';
export const CELL_SIZE = 1;
export const CommonDefine = {
TilemapGround: 'Ground',
TilemapBorder: 'Border',
Prop: 'Prop',
Vehicle: 'Vehicle',
BlockBase: 'Baseblock',
BlockJump: 'JumpBlock',
};
export function addDirection(dir: Direction, delta: number): Direction {
const n = 4;
return (((dir + delta) % n) + n) % n as Direction;
}
type MoveKey = string;
function mk(role: MoverRole, cur: GridType, next: GridType, jump: boolean): MoveKey {
return `${role}|${cur}|${next}|${jump ? 1 : 0}`;
}
function buildMoveTable(entries: [MoverRole, GridType, GridType, boolean, number][]): Map<MoveKey, number> {
const m = new Map<MoveKey, number>();
for (const [role, cur, next, jump, v] of entries) {
m.set(mk(role, cur, next, jump), v);
}
return m;
}
/** 单人 moveCondition */
export const moveCondition = buildMoveTable([
['player', GridType.Across, GridType.Across, false, 1],
['player', GridType.Jump, GridType.Across, false, 1],
['player', GridType.Jump, GridType.Across, true, 1],
['player', GridType.Ride, GridType.Across, false, 1],
['player', GridType.Ride, GridType.Across, true, 1],
['player', GridType.Across, GridType.Jump, true, 1],
['player', GridType.Across, GridType.Ride, false, 1],
['player', GridType.Across, GridType.Ride, true, 1],
['player', GridType.Jump, GridType.Ride, false, 1],
['player', GridType.Jump, GridType.Ride, true, 1],
['player', GridType.Jump, GridType.Jump, false, 1],
['player', GridType.Jump, GridType.Jump, true, 1],
['player', GridType.Ride, GridType.None, false, 1],
['player', GridType.Ride, GridType.None, true, 1],
['player', GridType.None, GridType.None, false, 1],
['player', GridType.None, GridType.None, true, 1],
['player', GridType.None, GridType.Across, false, 1],
['player', GridType.None, GridType.Across, true, 1],
['vehicle', GridType.None, GridType.None, false, 1],
['vehicle', GridType.None, GridType.None, true, 1],
['vehicle', GridType.Ride, GridType.None, false, 1],
]);
/** 多人 moveConditionMult与 Unity Define.moveConditionMult 一致) */
const multEntries: [MoverRole, GridType, GridType, boolean, number][] = [
['player', GridType.Across, GridType.Across, false, 1],
['player', GridType.Across, GridType.Across, true, 1],
['player', GridType.Jump, GridType.Across, false, 1],
['player', GridType.Jump, GridType.Across, true, 1],
['player', GridType.Ride, GridType.Across, false, 1],
['player', GridType.Ride, GridType.Across, true, 1],
['player', GridType.Across, GridType.Jump, true, 1],
['player', GridType.Across, GridType.Jump, false, 1],
['player', GridType.Across, GridType.Ride, false, 1],
['player', GridType.Across, GridType.Ride, true, 1],
['player', GridType.Jump, GridType.Ride, false, 1],
['player', GridType.Jump, GridType.Ride, true, 1],
['player', GridType.Jump, GridType.Jump, false, 1],
['player', GridType.Jump, GridType.Jump, true, 1],
['player', GridType.Ride, GridType.None, false, 1],
['player', GridType.Ride, GridType.None, true, 1],
['player', GridType.None, GridType.None, false, 1],
['player', GridType.None, GridType.None, true, 1],
['player', GridType.None, GridType.Across, false, 1],
['player', GridType.None, GridType.Across, true, 1],
['player', GridType.Across, GridType.Block, false, 0],
['player', GridType.Across, GridType.Block, true, 0],
['vehicle', GridType.Ride, GridType.Block, false, 0],
['vehicle', GridType.Ride, GridType.Block, true, 0],
['vehicle', GridType.Ride, GridType.Across, false, 0],
['vehicle', GridType.Ride, GridType.Across, true, 0],
['vehicle', GridType.None, GridType.None, false, 1],
['vehicle', GridType.None, GridType.None, true, 1],
['vehicle', GridType.Ride, GridType.None, false, 1],
['vehicle', GridType.Ride, GridType.None, true, 1],
];
export const moveConditionMultMap = buildMoveTable(multEntries);
export const skinPath: Record<Skin, { unlockLevel: number }> = {
[Skin.Silu]: { unlockLevel: 0 },
[Skin.Panda]: { unlockLevel: 400 },
[Skin.RedArmy]: { unlockLevel: 800 },
[Skin.numMan]: { unlockLevel: 1200 },
[Skin.snow]: { unlockLevel: 1600 },
[Skin.sanxing]: { unlockLevel: 1600 },
};
export function getMoveCondition(mult: boolean): Map<MoveKey, number> {
return mult ? moveConditionMultMap : moveCondition;
}
export function lookupMove(
table: Map<MoveKey, number>,
role: MoverRole,
cur: GridType,
next: GridType,
jump: boolean,
): number | undefined {
return table.get(mk(role, cur, next, jump));
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e5d9eb64-9856-42f8-b14c-ab18ad6ff50e",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,27 @@
export enum EventType {
LevelInit = 'LevelInit',
InputEnd = 'InputEnd',
}
type Handler = (...args: unknown[]) => void;
const listeners = new Map<EventType, Handler[]>();
export const EventManager = {
register(type: EventType, handler: Handler) {
if (!listeners.has(type)) listeners.set(type, []);
const list = listeners.get(type)!;
if (!list.includes(handler)) list.push(handler);
},
remove(type: EventType, handler: Handler) {
const list = listeners.get(type);
if (!list) return;
const i = list.indexOf(handler);
if (i >= 0) list.splice(i, 1);
},
dispatch(type: EventType, ...args: unknown[]) {
const list = listeners.get(type);
if (!list) return;
for (const h of [...list]) h(...args);
},
};

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d101e574-7e0a-4ccf-9ead-12d3872afe82",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "189e768a-9029-4554-9763-dd1099995a8d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,178 @@
import { _decorator, Component, Vec3 } from 'cc';
import {
Direction, GameState, GridType, MoveState, MoverRole,
addDirection, getMoveCondition, lookupMove,
} from '../core/Define';
import { GameManager } from '../manager/GameManager';
const { ccclass, property } = _decorator;
@ccclass('Movement')
export class Movement extends Component {
@property
moveSpeed = 4;
direction: Direction = Direction.North;
moveState: MoveState = MoveState.Idle;
moverRole: MoverRole = 'player';
protected targetPosition = new Vec3();
protected targetGridType: GridType = GridType.None;
protected lastPosition = new Vec3();
protected step = 0;
private moveWait = false;
private queue: Promise<void> = Promise.resolve();
static callEach = false;
get curGrid(): GridType {
return GameManager.instance!.calculateGridType(this.node.position);
}
get nextGrid(): GridType {
return GameManager.instance!.calculateNextGridType(this.node.position, this.direction);
}
get lastGrid(): GridType {
return GameManager.instance!.calculateLastGridType(this.node.position, this.direction);
}
get isFront(): boolean {
return this.direction === Direction.South || this.direction === Direction.East;
}
start() {
this.setDirection(this.direction);
}
update(dt: number) {
if (this.moveState !== MoveState.Moving) return;
const pos = this.node.position;
const next = new Vec3();
Vec3.moveTowards(next, pos, this.targetPosition, this.moveSpeed * dt);
this.node.setPosition(next);
this.onMoving();
if (Vec3.distance(next, this.targetPosition) < 0.01) {
this.node.setPosition(this.targetPosition);
this.moveState = MoveState.Idle;
this.moveWait = false;
this.onMoveToTarget();
}
}
setDirection(dir: Direction) {
this.direction = dir;
}
protected onMoving() {}
protected onMoveToTarget() {}
protected onMoveNextSet(_isJump: boolean) {}
protected onMoveFail(_isJump: boolean) {}
protected playMoveAnim() {}
private moveNextCheck(isJump: boolean, toFront: boolean): number {
const gm = GameManager.instance!;
const dir = toFront ? this.direction : addDirection(this.direction, 2);
const targetTmp = gm.nextGridPosition(this.node.position, dir);
const targetType = gm.calculateGridType(targetTmp);
const table = getMoveCondition(gm.multMode);
const nextG = toFront ? this.nextGrid : this.lastGrid;
const v = lookupMove(table, this.moverRole, this.curGrid, nextG, isJump);
if (v !== undefined) {
if (v === 1) {
if (gm.multMode) {
const nextCell = gm.worldToCell(targetTmp);
for (const n of ['PlayerA1', 'PlayerA2', 'PlayerA3', 'PlayerB1', 'PlayerB2', 'PlayerB3']) {
const p = gm.findNodeByName(n);
if (p) {
const c = gm.worldToCell(p.worldPosition);
if (c.x === nextCell.x && c.y === nextCell.y) return 0;
}
}
if (nextG === GridType.Ride) {
const ride = gm.getGameObject(targetTmp);
const expect = this.node.name.replace('Player', 'Vehicle');
if (ride && ride.name !== expect) return 0;
}
}
this.targetPosition.set(targetTmp);
this.targetGridType = targetType;
}
return v;
}
return gm.multMode ? 0 : -1;
}
private enqueue(fn: () => Promise<void>) {
this.queue = this.queue.then(fn).catch((e) => console.error(e));
}
private waitUntil(cond: () => boolean): Promise<void> {
return new Promise((resolve) => {
const tick = () => {
if (cond()) resolve();
else requestAnimationFrame(tick);
};
tick();
});
}
private async moveCoroutine(n: number, isJump: boolean) {
await this.waitUntil(() => !this.moveWait);
const toFront = n > 0;
this.step = Math.abs(n);
const gm = GameManager.instance!;
while (this.step > 0 && gm.gameState === GameState.Run) {
this.step--;
const r = this.moveNextCheck(isJump, toFront);
if (r === -1) {
this.onMoveFail(isJump);
return;
}
if (r === 1) {
this.moveWait = true;
this.lastPosition.set(this.node.position);
this.moveState = MoveState.Moving;
this.onMoveNextSet(isJump);
await this.waitUntil(() => !this.moveWait);
} else {
this.playMoveAnim();
this.moveState = MoveState.Moving;
}
}
}
private async rotateCoroutine(n: number) {
await this.waitUntil(() => !this.moveWait);
this.moveWait = true;
this.setDirection(addDirection(this.direction, n));
this.moveWait = false;
this.onMoveToTarget();
}
protected jsCallCheck(n: number): boolean {
const gm = GameManager.instance!;
const name = this.node.name;
if (name === 'Player' || name === 'Vehicle') return gm.jsCallCheck(n);
if (gm.multMode) return gm.jsCallCheckMultMode(n, name);
return false;
}
callMove(n: number) {
if (!this.jsCallCheck(n)) return;
this.enqueue(() => this.moveCoroutine(n, false));
}
callRotateLeft(n: number) {
if (!this.jsCallCheck(n)) return;
this.enqueue(() => this.rotateCoroutine(-n));
}
callRotateRight(n: number) {
if (!this.jsCallCheck(n)) return;
this.enqueue(() => this.rotateCoroutine(n));
}
callJump() {
if (this.moverRole !== 'player') return;
if (!this.jsCallCheck(1)) return;
this.enqueue(() => this.moveCoroutine(1, true));
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5fdb4109-5d6a-4002-90f9-a7568122ac93",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "95df233f-f174-4e12-a4f4-cc94d163a21d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,47 @@
import { Direction } from '../core/Define';
import { LevelConfig } from './LevelTypes';
import { LEVELS_600 } from './levels-600.generated';
/** 额外关卡(多人等) */
const EXTRA_LEVELS: Record<number, LevelConfig> = {
601: {
levelID: 601,
boundary: { x: 20, y: 20 },
spawns: [
{ x: 0, y: 0, kind: 'player', playerDirection: Direction.North },
{ x: 6, y: 6, kind: 'prop' },
{ x: -1, y: 2, kind: 'prop' },
],
},
999001: {
levelID: 999001,
boundary: { x: 999, y: 999 },
spawns: [
{ x: -9, y: -9, kind: 'player', playerDirection: Direction.South },
{ x: 9, y: 9, kind: 'player', playerDirection: Direction.North },
{ x: -9, y: -10, kind: 'vehicle', vehicleDirection: Direction.North },
{ x: 9, y: 10, kind: 'vehicle', vehicleDirection: Direction.South },
],
},
};
const allLevels: Record<number, LevelConfig> = {
...LEVELS_600,
...EXTRA_LEVELS,
};
export function getLevelConfig(levelID: number): LevelConfig | null {
return allLevels[levelID] ?? null;
}
export function hasLevel(levelID: number): boolean {
return levelID in allLevels;
}
export function registerLevel(config: LevelConfig) {
allLevels[config.levelID] = config;
}
export function getLevelCount(): number {
return Object.keys(allLevels).length;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "05e4b37c-f71a-42c0-b8f6-88fda70b655d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,22 @@
import { Direction } from '../core/Define';
export type SpawnKind = 'player' | 'vehicle' | 'prop' | 'prop_decor' | 'enemy';
export interface SpawnConfig {
x: number;
y: number;
kind: SpawnKind;
playerDirection?: Direction;
vehicleDirection?: Direction;
}
/** 稀疏地块key 为 "x,y" */
export interface LevelConfig {
levelID: number;
boundary: { x: number; y: number };
spawns: SpawnConfig[];
/** Ground 层 tile 名 */
ground?: Record<string, string>;
/** Border 阻挡格 */
border?: Record<string, boolean>;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9fd69354-fd48-4ecf-9bd3-2cb056258612",
"files": [],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"ver": "2.0.1",
"importer": "json",
"imported": true,
"uuid": "c5bd9bd8-738e-4bbc-8f04-8b1c77279929",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "507007a3-8bd2-41d5-b962-44c38a653bbb",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "f289d7c6-e9b4-45e2-9068-532f08608094",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,388 @@
import { _decorator, Component, Node, Vec3, Color, Graphics, UITransform, director } from 'cc';
import { CELL_PIXEL } from '../AppBootstrap';
import {
CELL_SIZE, CommonDefine, Direction, GameState, GridType, Skin, addDirection,
} from '../core/Define';
import { EventManager, EventType } from '../core/EventManager';
import { JsBridge } from '../bridge/JsBridge';
import { getLevelConfig, hasLevel, registerLevel } from '../level/LevelRegistry';
import { LevelConfig, SpawnConfig } from '../level/LevelTypes';
import { PlayerController } from '../controller/PlayerController';
import { VehicleController } from '../controller/VehicleController';
import { PropController } from '../controller/PropController';
import { VisualAssets } from '../visual/VisualAssets';
import { SpawnKind } from '../level/LevelTypes';
const { ccclass, property } = _decorator;
interface GridEntry {
type: GridType;
node: Node;
}
@ccclass('GameManager')
export class GameManager extends Component {
static instance: GameManager | null = null;
@property(Node)
mainLevelEntrance: Node | null = null;
@property
initialLevelID = 1;
playerSkin: Skin = Skin.Silu;
multMode = false;
multPlayerRole = '';
gameState: GameState = GameState.Run;
isInputEnd = false;
uiStyle = 'default';
curLevelID = 1;
stepNum = 0;
stepA1Num = 0;
stepA2Num = 0;
stepA3Num = 0;
stepB1Num = 0;
stepB2Num = 0;
stepB3Num = 0;
private creating = false;
private curLevel: Node | null = null;
private curConfig: LevelConfig | null = null;
private gridTypes = new Map<string, GridEntry>();
private gridTypesForProps = new Map<string, GridEntry>();
private groundCells = new Map<string, string>();
private borderCells = new Set<string>();
onLoad() {
if (GameManager.instance && GameManager.instance !== this) {
this.destroy();
return;
}
GameManager.instance = this;
}
start() {
/* 关卡由 AppBootstrap 在就绪后加载 */
}
onDestroy() {
if (GameManager.instance === this) GameManager.instance = null;
}
// --- 状态 ---
setGameState(s: GameState) { this.gameState = s; }
initData() {
this.setGameState(GameState.Run);
this.stepNum = 0;
this.stepA1Num = this.stepA2Num = this.stepA3Num = 0;
this.stepB1Num = this.stepB2Num = this.stepB3Num = 0;
this.callSetIsInputEnd(0);
}
jsCallCheck(n: number): boolean {
if (this.isInputEnd || this.gameState !== GameState.Run) return false;
this.stepNum += Math.abs(n);
return true;
}
jsCallCheckMultMode(n: number, name: string): boolean {
if (this.isInputEnd || this.gameState !== GameState.Run) return false;
const a = Math.abs(n);
switch (name) {
case 'PlayerA1':
case 'VehicleA1': this.stepA1Num += a; break;
case 'PlayerA2':
case 'VehicleA2': this.stepA2Num += a; break;
case 'PlayerA3':
case 'VehicleA3': this.stepA3Num += a; break;
case 'PlayerB1':
case 'VehicleB1': this.stepB1Num += a; break;
case 'PlayerB2':
case 'VehicleB2': this.stepB2Num += a; break;
case 'PlayerB3':
case 'VehicleB3': this.stepB3Num += a; break;
default: break;
}
return true;
}
callSetIsInputEnd(v: number) {
this.isInputEnd = v !== 0;
if (this.isInputEnd) EventManager.dispatch(EventType.InputEnd, this.gameState);
}
getRelativePosition(given: Direction, input: Direction): string {
const rel = (((input - given) % 4) + 4) % 4;
return ['front', 'right', 'back', 'left'][rel];
}
// --- 坐标 ---
cellKey(x: number, y: number) { return `${x},${y}`; }
cellToWorld(cell: Vec3): Vec3 {
return new Vec3(cell.x * CELL_PIXEL, cell.y * CELL_PIXEL, 0);
}
worldToCell(world: Vec3): Vec3 {
return new Vec3(Math.round(world.x / CELL_PIXEL), Math.round(world.y / CELL_PIXEL), 0);
}
nextGridPosition(pos: Vec3, dir: Direction): Vec3 {
const c = this.worldToCell(pos);
switch (dir) {
case Direction.North: c.x += 1; break;
case Direction.South: c.x -= 1; break;
case Direction.East: c.y -= 1; break;
case Direction.West: c.y += 1; break;
}
return this.cellToWorld(c);
}
calculateGridType(pos: Vec3): GridType {
const cell = this.worldToCell(pos);
const key = this.cellKey(cell.x, cell.y);
const dyn = this.gridTypes.get(key);
if (dyn) return dyn.type;
if (this.borderCells.has(key)) return GridType.Block;
const g = this.groundCells.get(key);
if (g === CommonDefine.BlockBase) return GridType.Across;
if (g === CommonDefine.BlockJump) return GridType.Jump;
const b = this.curConfig?.boundary;
if (b && (Math.abs(cell.x) >= b.x || Math.abs(cell.y) >= b.y)) return GridType.Boundary;
return GridType.None;
}
calculateNextGridType(pos: Vec3, dir: Direction): GridType {
return this.calculateGridType(this.nextGridPosition(pos, dir));
}
calculateLastGridType(pos: Vec3, dir: Direction): GridType {
return this.calculateGridType(this.nextGridPosition(pos, addDirection(dir, 2)));
}
getGameObject(pos: Vec3): Node | null {
const key = this.cellKey(this.worldToCell(pos).x, this.worldToCell(pos).y);
return this.gridTypes.get(key)?.node ?? null;
}
addObj(pos: Vec3, type: GridType, node: Node) {
const c = this.worldToCell(pos);
this.gridTypes.set(this.cellKey(c.x, c.y), { type, node });
}
removeObj(pos: Vec3) {
const c = this.worldToCell(pos);
this.gridTypes.delete(this.cellKey(c.x, c.y));
}
removeProp(pos: Vec3) {
const c = this.worldToCell(pos);
this.gridTypesForProps.delete(this.cellKey(c.x, c.y));
}
countProp(): number {
if (!this.curLevel) return 0;
return this.curLevel.children.filter((c) => c.isValid && c.getComponent(PropController)).length;
}
getCurLevel() { return this.curConfig; }
findNodeByName(name: string): Node | null {
const scene = director.getScene();
if (!scene) return null;
return this.findInTree(scene, name);
}
private findInTree(root: Node, name: string): Node | null {
if (root.name === name) return root;
for (const ch of root.children) {
const f = this.findInTree(ch, name);
if (f) return f;
}
return null;
}
// --- 关卡 ---
destroyCurLevel() {
if (this.curLevel?.isValid) this.curLevel.destroy();
this.curLevel = null;
this.gridTypes.clear();
this.gridTypesForProps.clear();
this.groundCells.clear();
this.borderCells.clear();
}
switchLevel(levelID: number) {
if (!hasLevel(levelID) || this.creating) return;
this.multMode = levelID >= 999000;
this.destroyCurLevel();
this.createNewLevel(levelID);
}
async createNewLevel(levelID: number) {
if (this.creating) return;
this.creating = true;
const config = getLevelConfig(levelID);
if (!config || !this.mainLevelEntrance) {
this.creating = false;
return;
}
this.curLevelID = levelID;
this.curConfig = config;
const levelRoot = new Node(`Level_${levelID}`);
levelRoot.parent = this.mainLevelEntrance;
this.curLevel = levelRoot;
if (config.ground) {
for (const [k, v] of Object.entries(config.ground)) this.groundCells.set(k, v);
}
if (config.border) {
for (const k of Object.keys(config.border)) this.borderCells.add(k);
}
this.drawGridDebug(levelRoot, config);
const spawned: Node[] = [];
for (const s of config.spawns) {
const node = this.spawnEntity(levelRoot, s);
if (node) spawned.push(node);
}
this.initGridTypes();
this.initData();
EventManager.dispatch(EventType.LevelInit);
this.externalCallLevelInfo(spawned);
this.creating = false;
}
resetLevel() {
this.destroyCurLevel();
this.createNewLevel(this.curLevelID);
}
startMultPlay(role: string, coinsJson: string) {
this.multMode = true;
this.multPlayerRole = role;
let coins: string[] = [];
try {
coins = JSON.parse(coinsJson) as string[];
} catch (e) {
console.error('StartMultPlay coins parse', e);
}
const base = getLevelConfig(999001);
if (!base) return;
const extra = coins.map((s) => {
const [xs, ys] = s.split(',');
return { x: parseInt(xs, 10), y: parseInt(ys, 10), kind: 'prop' as const };
});
registerLevel({ ...base, spawns: [...base.spawns, ...extra] });
this.switchLevel(999001);
}
changeUIStyle(style: string) {
this.uiStyle = style;
}
callMute() { /* 可由 UIMain 实现 */ }
callUnmute() { /* 可由 UIMain 实现 */ }
private initGridTypes() {
this.gridTypes.clear();
this.gridTypesForProps.clear();
if (!this.curLevel) return;
const vehicles = this.curLevel.children.filter((c) => c.name.includes('Vehicle'));
for (const v of vehicles) {
const c = this.worldToCell(v.worldPosition);
this.gridTypes.set(this.cellKey(c.x, c.y), { type: GridType.Ride, node: v });
}
const props = this.curLevel.children.filter((c) => c.getComponent(PropController));
for (const p of props) {
const c = this.worldToCell(p.worldPosition);
this.gridTypesForProps.set(this.cellKey(c.x, c.y), {
type: this.calculateGridType(p.worldPosition),
node: p,
});
}
}
private spawnEntity(parent: Node, s: SpawnConfig): Node | null {
const pos = this.cellToWorld(new Vec3(s.x, s.y, 0));
let node: Node;
if (s.kind === 'player') {
node = new Node(s.x === -9 ? 'PlayerA1' : s.x === 9 ? 'PlayerB1' : 'Player');
node.addComponent(PlayerController);
const pc = node.getComponent(PlayerController)!;
pc.direction = s.playerDirection ?? Direction.South;
} else if (s.kind === 'vehicle') {
node = new Node(s.x < 0 ? 'VehicleA1' : 'VehicleB1');
node.addComponent(VehicleController);
const vc = node.getComponent(VehicleController)!;
vc.direction = s.vehicleDirection ?? Direction.North;
} else if (s.kind === 'prop') {
node = new Node('Prop');
node.addComponent(PropController);
} else if (s.kind === 'prop_decor') {
node = new Node('PropDecor');
} else {
return null;
}
this.attachVisual(node, s.kind, s.playerDirection, s.vehicleDirection);
node.setPosition(pos);
const ui = node.getComponent(UITransform) || node.addComponent(UITransform);
ui.setContentSize(CELL_PIXEL * 0.9, CELL_PIXEL * 0.9);
node.parent = parent;
return node;
}
private attachVisual(
node: Node,
kind: SpawnKind,
playerDir?: Direction,
vehicleDir?: Direction,
) {
const dir = kind === 'player' ? playerDir : kind === 'vehicle' ? vehicleDir : undefined;
VisualAssets.setupEntityVisual(node, kind, dir);
}
private drawGridDebug(root: Node, config: LevelConfig) {
const tiles = new Node('Ground');
tiles.parent = root;
const bx = config.boundary.x;
const by = config.boundary.y;
const half = CELL_PIXEL * 0.45;
for (let x = -bx + 1; x < bx; x++) {
for (let y = -by + 1; y < by; y++) {
const key = this.cellKey(x, y);
if (this.borderCells.has(key)) continue;
const p = this.cellToWorld(new Vec3(x, y, 0));
const cell = new Node(`cell_${x}_${y}`);
cell.parent = tiles;
cell.setPosition(p);
const cui = cell.addComponent(UITransform);
cui.setContentSize(CELL_PIXEL * 0.9, CELL_PIXEL * 0.9);
VisualAssets.applySprite(cell, 'tile', false, 1, 50);
}
}
}
externalCallLevelInfo(objects: Node[]) {
const info: { LevelID: number; PlayerName: string; VehicleName: string } = {
LevelID: this.curLevelID,
PlayerName: '',
VehicleName: '',
};
for (const obj of objects) {
if (this.multMode) {
if (obj.name === this.multPlayerRole) info.PlayerName = obj.name;
else if (obj.name === this.multPlayerRole.replace('Player', 'Vehicle')) info.VehicleName = obj.name;
} else {
if (obj.name.includes('Player')) info.PlayerName = obj.name;
if (obj.name.includes('Vehicle')) info.VehicleName = obj.name;
}
}
JsBridge.call('externalLevelInfo', JSON.stringify(info));
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "bfc2e3c7-1217-4813-b019-2c0014cb1579",
"files": [],
"subMetas": {},
"userData": {}
}

9
assets/scripts/ui.meta Normal file
View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e5583f3f-b1d5-4acb-b0ff-33493e54c023",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,20 @@
import { _decorator, Component } from 'cc';
import { GameManager } from '../manager/GameManager';
const { ccclass } = _decorator;
/** 对应 Unity UIMainJS 可 SendMessage("UIMain", "SetText", ...) */
@ccclass('UIMain')
export class UIMain extends Component {
private textVisible = false;
private textContent = '';
setText(str: string) {
this.textContent = str;
console.log('[UIMain]', str);
}
setTextActive(active: string) {
this.textVisible = active === 'true';
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "0585296d-ea8c-427b-8cc0-8d89a56719bb",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "66712587-a8af-4a45-8c43-129ae3bc8ae7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,147 @@
import {
Node, Sprite, SpriteFrame, UITransform, resources, Color, Graphics,
ImageAsset, Texture2D,
} from 'cc';
import { Direction } from '../core/Define';
import { SpawnKind } from '../level/LevelTypes';
type SpriteKey = 'player_F' | 'player_B' | 'ship_F' | 'ship_B' | 'coin' | 'tile';
const PATHS: Record<SpriteKey, string> = {
player_F: 'textures/silu/player_F',
player_B: 'textures/silu/player_B',
ship_F: 'textures/silu/ship_F',
ship_B: 'textures/silu/ship_B',
coin: 'textures/ui/coin',
tile: 'textures/silu/Baseblock',
};
export class VisualAssets {
private static frames = new Map<SpriteKey, SpriteFrame>();
private static loading: Promise<void> | null = null;
static async preload(): Promise<void> {
if (this.loading) return this.loading;
this.loading = (async () => {
const keys = Object.keys(PATHS) as SpriteKey[];
const results = await Promise.all(keys.map((k) => this.loadOne(k)));
const ok = results.filter(Boolean).length;
console.log(`[VisualAssets] 贴图加载 ${ok}/${keys.length}`);
if (ok === 0) {
console.warn('[VisualAssets] 未加载到贴图,将使用色块。请确认 assets/resources/textures 已导入');
}
})().catch((e) => {
console.error('[VisualAssets] preload failed', e);
this.loading = null;
});
return this.loading;
}
private static loadOne(key: SpriteKey): Promise<boolean> {
if (this.frames.has(key)) return Promise.resolve(true);
const base = PATHS[key];
return new Promise((resolve) => {
resources.load(`${base}/spriteFrame`, SpriteFrame, (err, sf) => {
if (!err && sf) {
this.frames.set(key, sf);
resolve(true);
return;
}
resources.load(base, SpriteFrame, (err2, sf2) => {
if (!err2 && sf2) {
this.frames.set(key, sf2);
resolve(true);
return;
}
resources.load(base, ImageAsset, (err3, img) => {
if (!err3 && img) {
const tex = new Texture2D();
tex.image = img;
const frame = new SpriteFrame();
frame.texture = tex;
this.frames.set(key, frame);
resolve(true);
} else {
console.warn(`[VisualAssets] 加载失败: ${base}`, err3 || err2 || err);
resolve(false);
}
});
});
});
});
}
static getFrame(key: SpriteKey): SpriteFrame | null {
return this.frames.get(key) ?? null;
}
static applyPlayerSprite(node: Node, direction: Direction) {
const isFront = direction === Direction.South || direction === Direction.East;
const flipX = direction === Direction.West || direction === Direction.East;
const key: SpriteKey = isFront ? 'player_F' : 'player_B';
this.applySprite(node, key, flipX);
}
static applyVehicleSprite(node: Node, direction: Direction, uiStyle = 'default') {
void uiStyle;
const isFront = direction === Direction.South || direction === Direction.East;
const flipX = direction === Direction.West || direction === Direction.East;
const key: SpriteKey = isFront ? 'ship_F' : 'ship_B';
this.applySprite(node, key, flipX);
}
static setupEntityVisual(node: Node, kind: SpawnKind, direction?: Direction) {
if (kind === 'player') {
this.applyPlayerSprite(node, direction ?? Direction.South);
return;
}
if (kind === 'vehicle') {
this.applyVehicleSprite(node, direction ?? Direction.North);
return;
}
if (kind === 'prop') {
this.applySprite(node, 'coin', false, 0.85);
return;
}
if (kind === 'prop_decor') {
this.applySprite(node, 'coin', false, 0.6, 120);
}
}
/** Sprite 与 Graphics 不能共存,二选一 */
static applySprite(node: Node, key: SpriteKey, flipX: boolean, scale = 1, alpha = 255) {
let ui = node.getComponent(UITransform);
if (!ui) {
ui = node.addComponent(UITransform);
ui.setContentSize(48, 48);
}
const sf = this.getFrame(key);
const spr = node.getComponent(Sprite);
const g = node.getComponent(Graphics);
if (sf) {
if (g) g.destroy();
const sprite = spr || node.addComponent(Sprite);
sprite.spriteFrame = sf;
sprite.sizeMode = Sprite.SizeMode.CUSTOM;
const w = ui.contentSize.width * scale;
const h = ui.contentSize.height * scale;
ui.setContentSize(w, h);
sprite.color = new Color(255, 255, 255, alpha);
} else {
if (spr) spr.destroy();
const graphics = g || node.addComponent(Graphics);
graphics.fillColor = key === 'coin'
? new Color(255, 220, 0, alpha)
: new Color(80, 160, 255, alpha);
const w = ui.contentSize.width * 0.45 * scale;
graphics.clear();
graphics.rect(-w, -w, w * 2, w * 2);
graphics.fill();
}
const sx = flipX ? -Math.abs(scale) : Math.abs(scale);
node.setScale(sx, Math.abs(scale), 1);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c813fc15-e977-4bb5-9d75-abc460539690",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,16 @@
{
"package_version": 2,
"name": "game-controller-inspector",
"version": "1.0.0",
"description": "GameController Inspector 调试按钮(对齐 Unity TestGame2",
"main": "./dist/main.js",
"contributions": {
"inspector": {
"section": {
"node": {
"GameController": "./dist/inspector.js"
}
}
}
}
}

View File

@@ -0,0 +1,83 @@
/**
* GameController 自定义 Inspector需启用扩展 game-controller-inspector
* 构建: 在扩展目录执行 npm run build或在 Creator 扩展管理器中启用
*/
export const template = `
<div class="game-controller-inspector">
<ui-prop type="dump" prop="dump"></ui-prop>
<ui-section header="关卡调试">
<ui-input id="input-level" placeholder="inputLevel"></ui-input>
<ui-button id="btn-switch">SwitchLevel</ui-button>
<ui-button id="btn-end">EndInput</ui-button>
</ui-section>
<ui-section header="主题">
<ui-input id="input-style" placeholder="inputStyle"></ui-input>
<ui-button id="btn-style">ChangeStyle</ui-button>
</ui-section>
<ui-section header="多人">
<ui-input id="input-coins" placeholder='coinStr JSON'></ui-input>
<ui-button id="btn-mult">StartMultPlay</ui-button>
</ui-section>
<ui-section header="音频">
<ui-button id="btn-mute">Mute</ui-button>
<ui-button id="btn-unmute">Unmute</ui-button>
</ui-section>
</div>
`;
export const $ = {
inputLevel: '#input-level',
inputStyle: '#input-style',
inputCoins: '#input-coins',
btnSwitch: '#btn-switch',
btnEnd: '#btn-end',
btnStyle: '#btn-style',
btnMult: '#btn-mult',
btnMute: '#btn-mute',
btnUnmute: '#btn-unmute',
} as Record<string, string>;
type Dump = { value: Record<string, { value: unknown }> };
export function update(this: { dump: Dump; $: Record<string, HTMLElement> }, dump: Dump) {
this.dump = dump;
const v = dump.value;
if (this.$.inputLevel && v.inputLevel) {
(this.$.inputLevel as unknown as { value: string }).value = String(v.inputLevel.value ?? '1');
}
if (this.$.inputStyle && v.inputStyle) {
(this.$.inputStyle as unknown as { value: string }).value = String(v.inputStyle.value ?? 'default');
}
if (this.$.inputCoins && v.coinStr) {
(this.$.inputCoins as unknown as { value: string }).value = String(v.coinStr.value ?? '[]');
}
}
function callMethod(uuid: string, name: string, args: unknown[] = []) {
// @ts-expect-error Editor global
if (typeof Editor !== 'undefined' && Editor.Message) {
// @ts-expect-error Editor API
Editor.Message.request('scene', 'execute-component-method', { uuid, name, args });
}
}
export function ready(this: { dump: Dump; $: Record<string, HTMLElement> }) {
const uuid = () => this.dump.value.uuid?.value as string;
this.$.btnSwitch?.addEventListener('confirm', () => {
const id = parseInt((this.$.inputLevel as unknown as { value: string }).value || '1', 10);
callMethod(uuid(), 'switchLevel', [id]);
});
this.$.btnEnd?.addEventListener('confirm', () => callMethod(uuid(), 'callSetIsInputEnd', [1]));
this.$.btnStyle?.addEventListener('confirm', () => {
const s = (this.$.inputStyle as unknown as { value: string }).value || 'default';
callMethod(uuid(), 'changeUIStyle', [s]);
});
this.$.btnMult?.addEventListener('confirm', () => {
const coins = (this.$.inputCoins as unknown as { value: string }).value || '[]';
callMethod(uuid(), 'startMultPlay', ['PlayerA1', coins]);
});
this.$.btnMute?.addEventListener('confirm', () => callMethod(uuid(), 'callMute', []));
this.$.btnUnmute?.addEventListener('confirm', () => callMethod(uuid(), 'callUnmute', []));
}

7
package.json Executable file
View File

@@ -0,0 +1,7 @@
{
"name": "tfrh001",
"uuid": "4683ce98-5131-488a-9c82-647c32645c91",
"creator": {
"version": "3.8.8"
}
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.3.9"
}

View File

@@ -0,0 +1,23 @@
{
"__version__": "3.0.9",
"game": {
"name": "未知游戏",
"app_id": "UNKNOW",
"c_id": "0"
},
"appConfigMaps": [
{
"app_id": "UNKNOW",
"config_id": "38c154"
}
],
"configs": [
{
"app_id": "UNKNOW",
"config_id": "38c154",
"config_name": "Default",
"config_remarks": "",
"services": []
}
]
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.0.1"
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.0.12"
}

View File

@@ -0,0 +1,23 @@
{
"__version__": "1.0.1",
"information": {
"customSplash": {
"id": "customSplash",
"label": "customSplash",
"enable": true,
"customSplash": {
"complete": false,
"form": "https://creator-api.cocos.com/api/form/show?sid=84b648999874269884ca64920657fb28"
}
},
"removeSplash": {
"id": "removeSplash",
"label": "removeSplash",
"enable": true,
"removeSplash": {
"complete": false,
"form": "https://creator-api.cocos.com/api/form/show?sid=84b648999874269884ca64920657fb28"
}
}
}
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.0.4"
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.0.6"
}

View File

@@ -0,0 +1,4 @@
{
"__version__": "1.0.3",
"current-scene": "d071e7d3-2dc4-4815-8cb8-c258c4b7c515"
}

92
tools/deploy-to-main.sh Normal file
View File

@@ -0,0 +1,92 @@
#!/bin/bash
# 将 Cocos Web 构建产物 + cocos-bridge 合并到主站静态目录
#
# 用法:
# ./tools/deploy-to-main.sh \
# --build build/web-desktop \
# --target "/path/to/主站静态目录/cocos"
#
# 示例(部署到 Unity 项目 Template 旁,便于本地对比):
# ./tools/deploy-to-main.sh \
# --build build/web-desktop \
# --target "/Users/liuyufei/tfrh/主站文件/主站/Template/cocos"
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BUILD_DIR=""
TARGET_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--build) BUILD_DIR="$2"; shift 2 ;;
--target) TARGET_DIR="$2"; shift 2 ;;
-h|--help)
echo "Usage: $0 --build <build/web-desktop> --target <主站/cocos目录>"
exit 0
;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
if [[ -z "$BUILD_DIR" || -z "$TARGET_DIR" ]]; then
echo "Error: 需要 --build 和 --target"
echo "Example:"
echo " $0 --build build/web-desktop --target /path/to/main-site/cocos"
exit 1
fi
BUILD_DIR="$(cd "$BUILD_DIR" 2>/dev/null && pwd || true)"
if [[ -z "$BUILD_DIR" || ! -f "$BUILD_DIR/index.html" ]]; then
echo "Error: 构建目录无效或缺少 index.html: $BUILD_DIR"
echo "请先在 Cocos Creator 中执行: 项目 → 构建发布 → Web Desktop"
exit 1
fi
mkdir -p "$TARGET_DIR"
echo ">>> 复制构建产物: $BUILD_DIR -> $TARGET_DIR"
rsync -a --delete \
--exclude '.DS_Store' \
"$BUILD_DIR/" "$TARGET_DIR/"
echo ">>> 复制 cocos-bridge.js"
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$TARGET_DIR/../cocos-bridge.js" 2>/dev/null || \
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$TARGET_DIR/cocos-bridge.js"
# 若 target 是 .../cocosbridge 放在上一级(与 index 同级)
if [[ -f "$TARGET_DIR/cocos-bridge.js" ]]; then
BRIDGE_PATH="$TARGET_DIR/cocos-bridge.js"
else
BRIDGE_PATH="$(dirname "$TARGET_DIR")/cocos-bridge.js"
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$BRIDGE_PATH"
fi
echo ">>> 生成联调页 cocos-demo.html"
DEMO="$PROJECT_DIR/web-template/main-site.html"
if [[ -f "$DEMO" ]]; then
cp "$DEMO" "$(dirname "$TARGET_DIR")/cocos-demo.html" 2>/dev/null || \
cp "$DEMO" "$TARGET_DIR/cocos-demo.html"
fi
cat <<EOF
部署完成。
目录结构建议:
$(dirname "$TARGET_DIR")/
cocos-bridge.js ← JS 回调 + unityInstance 兼容
cocos/ ← 构建产物(本脚本输出)
index.html
src/ ...
cocos-demo.html ← 可选联调页
主站 index.html 需:
1. 在 <head> 或游戏脚本前引入: <script src="cocos-bridge.js"></script>
2. 用 cocos/index.html 中的 script 标签替换原 Unity Build/*.js 段
3. 保留 processData / externalResult 等全局函数
本地预览(需先 cd 到含 cocos 的目录):
npx --yes serve "$(dirname "$TARGET_DIR")" -p 8080
EOF

24
tools/export_all.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# 批量导出 Unity Levels*.cs → Cocos generated ts
set -e
UNITY_ROOT="${1:-/Users/liuyufei/tfrh/主站文件/主站}"
OUT_DIR="$(dirname "$0")/../assets/scripts/level"
PY="$(dirname "$0")/export_unity_levels.py"
mkdir -p "$OUT_DIR"
export_one() {
local file="$1"
local name="$2"
echo "Exporting $file -> levels-${name}.generated.ts"
python3 "$PY" \
--input "$UNITY_ROOT/Assets/Scripts/Core/${file}" \
--output "$OUT_DIR/levels-${name}.generated.ts"
}
export_one "Levels600.cs" "600"
# 可按需取消注释:
# export_one "Levels1000.cs" "1000"
# export_one "Levels10000.cs" "10000"
echo "Done. Update LevelRegistry.ts to import new LEVELS_* maps."

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
从 Unity Levels*.cs 导出 Cocos LevelRegistry 数据。
用法:
python3 tools/export_unity_levels.py \
--input "/path/to/Levels600.cs" \
--output assets/scripts/level/levels-600.generated.ts \
--border-cache assets/scripts/level/border-cache.json
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
DIR = {
"Direction.North": "Direction.North",
"Direction.East": "Direction.East",
"Direction.South": "Direction.South",
"Direction.West": "Direction.West",
}
LEVEL_RE = re.compile(
r"\{(\d+),new Level\(\)\{LevelID\s*=\s*\d+,spawns\s*=\s*new List<Spawn>\(\)\{",
re.MULTILINE,
)
SPAWN_RE = re.compile(
r"new Spawn\(\)\{([^}]+(?:\{[^}]*\})?)\}",
re.MULTILINE,
)
POS_RE = re.compile(r"position\s*=\s*new Vector3Int\((-?\d+),(-?\d+),0\)")
PATH_RE = re.compile(r'path\s*=\s*"?([^",\s]+)"?')
PDIR_RE = re.compile(r"playerDirection\s*=\s*(Direction\.\w+)")
VDIR_RE = re.compile(r"vehicleDirection\s*=\s*(Direction\.\w+)")
BOUND_RE = re.compile(r"boundary\s*=\s*new Vector3Int\((\d+),(\d+),0\)")
def kind_from_path(path: str) -> str:
p = path.lower()
if "player" in p and "multplay" not in p:
return "player"
if "vehicle" in p:
return "vehicle"
if "enemy" in p:
return "enemy"
if "nprop" in p:
return "prop_decor"
if "prop" in p:
return "prop"
return "prop_decor"
def parse_spawns(block: str) -> list[dict]:
spawns = []
for m in SPAWN_RE.finditer(block):
body = m.group(1)
pm = POS_RE.search(body)
if not pm:
continue
path_m = PATH_RE.search(body)
path = path_m.group(1) if path_m else ""
item: dict = {
"x": int(pm.group(1)),
"y": int(pm.group(2)),
"kind": kind_from_path(path),
}
pdir = PDIR_RE.search(body)
vdir = VDIR_RE.search(body)
if pdir:
item["playerDirection"] = pdir.group(1)
if vdir:
item["vehicleDirection"] = vdir.group(1)
spawns.append(item)
return spawns
def ring_border_key(bx: int, by: int) -> str:
return f"{bx},{by}"
def parse_file(text: str) -> dict[int, dict]:
levels: dict[int, dict] = {}
matches = list(LEVEL_RE.finditer(text))
for i, m in enumerate(matches):
lid = int(m.group(1))
start = m.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
chunk = text[start:end]
bound = BOUND_RE.search(chunk)
bx, by = (10, 10)
if bound:
bx, by = int(bound.group(1)), int(bound.group(2))
spawns = parse_spawns(chunk)
levels[lid] = {
"levelID": lid,
"boundary": {"x": bx, "y": by},
"borderKey": ring_border_key(bx, by),
"spawns": spawns,
}
return levels
def spawn_to_ts(s: dict, indent: str) -> str:
parts = [
f"x: {s['x']}",
f"y: {s['y']}",
f"kind: '{s['kind']}'",
]
if "playerDirection" in s:
parts.append(f"playerDirection: {s['playerDirection']}")
if "vehicleDirection" in s:
parts.append(f"vehicleDirection: {s['vehicleDirection']}")
return indent + "{ " + ", ".join(parts) + " }"
def emit_ts(levels: dict[int, dict], border_cache: dict[str, dict]) -> str:
lines = [
"/* AUTO-GENERATED by tools/export_unity_levels.py — DO NOT EDIT */",
"import { Direction } from '../core/Define';",
"import { LevelConfig, SpawnConfig } from './LevelTypes';",
"",
"const BORDER_CACHE: Record<string, Record<string, boolean>> = " + json.dumps(border_cache, separators=(',', ':')) + ";",
"",
"function withBorder(key: string, cfg: Omit<LevelConfig, 'border'>): LevelConfig {",
" return { ...cfg, border: BORDER_CACHE[key] };",
"}",
"",
"export const LEVELS_600: Record<number, LevelConfig> = {",
]
for lid in sorted(levels.keys()):
L = levels[lid]
lines.append(f" {lid}: withBorder('{L['borderKey']}', {{")
lines.append(f" levelID: {lid},")
lines.append(f" boundary: {{ x: {L['boundary']['x']}, y: {L['boundary']['y']} }},")
lines.append(" spawns: [")
for s in L["spawns"]:
lines.append(spawn_to_ts(s, " ") + ",")
lines.append(" ],")
lines.append(" }),")
lines.append("};")
lines.append("")
return "\n".join(lines)
def build_border_cache(levels: dict[int, dict]) -> dict[str, dict]:
cache: dict[str, dict] = {}
for L in levels.values():
key = L["borderKey"]
if key in cache:
continue
bx, by = L["boundary"]["x"], L["boundary"]["y"]
border: dict[str, bool] = {}
for x in range(-bx, bx + 1):
for y in range(-by, by + 1):
if abs(x) == bx or abs(y) == by:
border[f"{x},{y}"] = True
cache[key] = border
return cache
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--input", required=True, help="Unity Levels*.cs path")
ap.add_argument("--output", required=True, help="Output .ts path")
ap.add_argument("--border-cache", default="", help="Optional border cache json")
args = ap.parse_args()
text = Path(args.input).read_text(encoding="utf-8")
levels = parse_file(text)
if not levels:
print("No levels parsed", file=sys.stderr)
sys.exit(1)
border_cache = build_border_cache(levels)
out = Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(emit_ts(levels, border_cache), encoding="utf-8")
if args.border_cache:
Path(args.border_cache).write_text(json.dumps(border_cache, indent=2), encoding="utf-8")
print(f"Exported {len(levels)} levels -> {out}")
if __name__ == "__main__":
main()

55
tools/patch-main-index.js Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* 根据 Cocos 构建出的 index.html生成可嵌入主站的片段说明
*
* node tools/patch-main-index.js build/web-desktop
*/
const fs = require('fs');
const path = require('path');
const buildDir = process.argv[2];
if (!buildDir) {
console.error('Usage: node patch-main-index.js <build/web-desktop>');
process.exit(1);
}
const indexPath = path.join(buildDir, 'index.html');
if (!fs.existsSync(indexPath)) {
console.error('Not found:', indexPath);
process.exit(1);
}
const html = fs.readFileSync(indexPath, 'utf8');
// 提取所有 script 标签Cocos 启动链)
const scripts = [];
const re = /<script[^>]*src=["']([^"']+)["'][^>]*><\/script>/gi;
let m;
while ((m = re.exec(html)) !== null) {
scripts.push(m[1]);
}
// 提取内联 scriptSystem.import 等)
const inline = html.match(/<script>([\s\S]*?)<\/script>/gi) || [];
console.log('=== 复制到主站 index.html替换 Unity Build 段)===\n');
console.log('<!-- ① 主站公共:放在 head 或 body 开头 -->');
console.log('<script src="cocos-bridge.js"></script>\n');
console.log('<!-- ② Cocos 启动脚本(路径相对主站页面,若游戏在 /cocos/ 子目录则加前缀 cocos/ -->');
for (const src of scripts) {
const rel = src.startsWith('http') ? src : `cocos/${src.replace(/^\.\//, '')}`;
console.log(`<script src="${rel}" charset="utf-8"></script>`);
}
console.log('\n<!-- ③ 容器(若主站已有 #GameDiv 可保留原 id -->');
const canvasMatch = html.match(/<canvas[^>]*id=["']([^"']+)["'][^>]*>/i);
const divMatch = html.match(/id=["'](GameDiv|Cocos3dGameContainer)["']/i);
console.log('<!-- Cocos 构建页中的 canvas id:', canvasMatch ? canvasMatch[1] : 'GameCanvas', '-->');
if (inline.length) {
console.log('\n<!-- ④ 内联启动(从 build/index.html 复制以下 block -->');
inline.forEach((block) => console.log(block));
}
console.log('\n=== 删除原 Unity 段 ===');
console.log('- Build/build.loader.js');
console.log('- createUnityInstance(...)');

View File

@@ -0,0 +1,30 @@
# Unity 关卡迁移到 Cocos
Unity 原项目约 1.1 万个 `Level*.prefab`Cocos 版使用 **JSON/TS 配置** 代替 Tilemap Prefab。
## 配置格式
```typescript
registerLevel({
levelID: 601,
boundary: { x: 20, y: 20 },
border: { "10,0": true, ... }, // 可选
ground: { "0,0": "Baseblock", "1,0": "JumpBlock" }, // 可选
spawns: [
{ x: 0, y: 0, kind: 'player', playerDirection: 0 },
{ x: 3, y: 0, kind: 'prop' },
],
});
```
## 批量迁移步骤
1.`Assets/Scripts/Core/Levels*.cs` 提取 `LevelID``spawns``boundary`
2. 若有 Tiled 导出或 Tilemap 脚本,生成 `ground` / `border` 稀疏表
3. 写入 `assets/scripts/level/LevelRegistry.ts` 或独立 `assets/resources/levels/{id}.json`
4.`LevelRegistry.ts``getLevelConfig` 中合并 JSON 加载
## 资源迁移
- 角色/载具贴图:放入 `assets/resources/textures/`,替换 `GameManager.attachVisual` 为 Sprite 渲染
- 音频:放入 `assets/resources/audio/`

9
tsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
/* Base configuration. Do not edit this field. */
"extends": "./temp/tsconfig.cocos.json",
/* Add your custom configuration here. */
"compilerOptions": {
"strict": false
}
}

View File

@@ -0,0 +1,43 @@
/**
* 主站页面对接层:在 Cocos 启动后提供与 Unity WebGL 相同的 unityInstance.SendMessage API
* 用法:构建产物 index.html 末尾引入本脚本,或在主站模板中引入
*/
(function (global) {
function processData(json) { global.__tfrhOnProcessData?.(json); console.log('[processData]', JSON.parse(json)); }
function processVehicleData(json) { global.__tfrhOnProcessVehicleData?.(json); console.log('[processVehicleData]', JSON.parse(json)); }
function externalResult(json) { global.__tfrhOnExternalResult?.(json); console.log('[externalResult]', JSON.parse(json)); }
function externalLevelInfo(json) { global.__tfrhOnExternalLevelInfo?.(json); console.log('[externalLevelInfo]', JSON.parse(json)); }
function coinsData(json) { global.__tfrhOnCoinsData?.(json); console.log('[coinsData]', JSON.parse(json)); }
global.processData = processData;
global.processVehicleData = processVehicleData;
global.externalResult = externalResult;
global.externalLevelInfo = externalLevelInfo;
global.coinsData = coinsData;
// 动态 processPlayerA1 等
['A1','A2','A3','B1','B2','B3'].forEach(function (id) {
global['processPlayer' + id] = function (json) { console.log('[processPlayer' + id + ']', JSON.parse(json)); };
global['processVehicle' + id] = function (json) { console.log('[processVehicle' + id + ']', JSON.parse(json)); };
});
function waitInstance(maxMs) {
maxMs = maxMs || 30000;
return new Promise(function (resolve, reject) {
var t0 = Date.now();
(function tick() {
if (global.cocosIns || global.unityInstance) return resolve(global.cocosIns || global.unityInstance);
if (Date.now() - t0 > maxMs) return reject(new Error('Cocos instance timeout'));
requestAnimationFrame(tick);
})();
});
}
global.tfrhReady = waitInstance;
document.addEventListener('DOMContentLoaded', function () {
waitInstance().then(function () {
console.log('[tfrh] Cocos bridge ready, use unityInstance.SendMessage(...)');
}).catch(function (e) { console.warn('[tfrh]', e.message); });
});
})(typeof window !== 'undefined' ? window : globalThis);

24
web-template/index.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>主站 Cocos Web</title>
<style>
html, body { margin: 0; padding: 0; height: 100%; background: #1a1a2e; }
#GameDiv { width: 100%; height: 100%; }
#bar { color: #fff; font-family: sans-serif; padding: 8px; }
</style>
</head>
<body>
<div id="bar">加载中…</div>
<div id="GameDiv"></div>
<script>
function processData(json) { console.log('processData', JSON.parse(json)); }
function processVehicleData(json) { console.log('processVehicleData', JSON.parse(json)); }
function externalResult(json) { console.log('externalResult', JSON.parse(json)); }
function externalLevelInfo(json) { console.log('externalLevelInfo', JSON.parse(json)); }
function coinsData(json) { console.log('coinsData', JSON.parse(json)); }
</script>
<!-- 构建后将 Cocos 生成的 index.js 引入此页 -->
</body>
</html>

View File

@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>主站 · Cocos 关卡引擎</title>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; background: #1a1a2e; font-family: system-ui, sans-serif; }
#layout { display: flex; flex-direction: column; height: 100%; }
#toolbar { padding: 8px 12px; background: #16213e; color: #eee; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
#toolbar button { padding: 6px 12px; cursor: pointer; border: none; border-radius: 4px; background: #0f3460; color: #fff; }
#toolbar button:hover { background: #e94560; }
#game-wrap { flex: 1; position: relative; min-height: 0; }
#GameDiv, #Cocos3dGameContainer, #GameCanvas { width: 100% !important; height: 100% !important; }
#log { height: 120px; overflow: auto; background: #0d1117; color: #8b949e; font: 12px/1.4 monospace; padding: 8px; }
</style>
<!-- ① 先挂回调(与 Unity Template/index.html 一致) -->
<script src="cocos-bridge.js"></script>
</head>
<body>
<div id="layout">
<div id="toolbar">
<strong>主站 Cocos 联调</strong>
<button type="button" id="btn-l1">关卡 1</button>
<button type="button" id="btn-l21">关卡 21(载具)</button>
<button type="button" id="btn-move">前进 2 格</button>
<button type="button" id="btn-info">获取坐标</button>
<button type="button" id="btn-end">结束输入</button>
</div>
<div id="game-wrap">
<!-- ② Cocos 构建后替换为 build 目录中的容器,常见为 #GameDiv -->
<div id="GameDiv">
<div id="Cocos3dGameContainer">
<canvas id="GameCanvas" tabindex="0"></canvas>
</div>
</div>
</div>
<pre id="log"></pre>
</div>
<script>
const logEl = document.getElementById('log');
function log(msg) {
logEl.textContent += msg + '\n';
logEl.scrollTop = logEl.scrollHeight;
}
window.__tfrhOnExternalLevelInfo = (j) => log('关卡就绪: ' + j);
window.__tfrhOnProcessData = (j) => log('坐标: ' + j);
window.__tfrhOnExternalResult = (j) => log('结果: ' + j);
function send(obj, method, arg) {
const ins = window.unityInstance || window.cocosIns;
if (!ins) { log('实例未就绪'); return; }
if (arg === undefined) ins.SendMessage(obj, method);
else ins.SendMessage(obj, method, arg);
}
document.getElementById('btn-l1').onclick = () => send('GameController', 'SwitchLevel', 1);
document.getElementById('btn-l21').onclick = () => send('GameController', 'SwitchLevel', 21);
document.getElementById('btn-move').onclick = () => send('Player', 'CallMove', 2);
document.getElementById('btn-info').onclick = () => send('Player', 'CallPlayerInfo');
document.getElementById('btn-end').onclick = () => send('GameController', 'CallSetIsInputEnd', 1);
</script>
<!-- ③ 在 Cocos 构建输出的 index.html 中,将 systemjs / index.js 等脚本复制到本页 GameDiv 之后 -->
<!-- 示例(路径按实际构建目录修改):
<script src="src/polyfills.bundle.js"></script>
<script src="src/system.bundle.js"></script>
<script src="src/import-map.json" type="systemjs-importmap"></script>
<script>
System.import('./index.js').catch(function(e){ console.error(e); });
</script>
-->
</body>
</html>

View File

@@ -0,0 +1,204 @@
# 主站 Web 对接完整步骤Cocos tfrh001
## 一、在 Cocos Creator 中构建 Web Desktop
### 1. 打开工程
```
/Users/liuyufei/tfrh/cocos/tfrh001
```
确认已存在 `assets/scenes/main.scene`,且节点上挂了 **AppBootstrap**
### 2. 设置启动场景
**项目 → 项目设置 → 项目数据 → 启动场景** → 选择 `main`
### 3. 构建发布
1. 菜单 **项目 → 构建发布**(或顶部 **构建** 按钮)
2. **发布平台**:选 **Web Desktop**(不要选 Web Mobile除非只做手机
3. **构建路径**:默认 `build/web-desktop`(可保持默认)
4. **初始场景**`main`
5. 建议勾选:
- **内联所有 SpriteFrame**(减少请求数,可选)
- **MD5 Cache**(按需)
6. 点击 **构建**
7. 构建成功后点击 **运行** 可先本地测一遍
### 4. 构建产物位置
```
tfrh001/build/web-desktop/
├── index.html ← 含 Cocos 启动 script 列表
├── index.js
├── src/
│ ├── system.bundle.js
│ ├── polyfills.bundle.js
│ └── ...
├── assets/ ← 资源
└── ...
```
> 若 `build/web-desktop` 不存在,说明尚未在编辑器里点过「构建」。
---
## 二、合并到主站静态目录
### 方式 A自动脚本推荐
在终端执行(先完成上一节构建):
```bash
cd /Users/liuyufei/tfrh/cocos/tfrh001
# 部署到主站 Template 下(与 Unity 模板并列,便于对比)
bash tools/deploy-to-main.sh \
--build build/web-desktop \
--target "/Users/liuyufei/tfrh/主站文件/主站/Template/cocos"
```
部署后目录:
```
主站/Template/
├── index.html ← 原 Unity 页(参考)
├── cocos-bridge.js ← 新增JS 桥
├── cocos-demo.html ← 新增:联调页
└── cocos/ ← Cocos 构建产物
├── index.html
└── src/ ...
```
### 方式 B手动复制
| 复制源 | 复制到 |
|--------|--------|
| `build/web-desktop/*` | 主站静态目录下的 `cocos/` |
| `web-template/cocos-bridge.js` | 与主站 `index.html` **同级** |
---
## 三、修改主站 index.html
### 1. 引入 cocos-bridge在 Unity 脚本之前)
```html
<script src="cocos-bridge.js"></script>
```
`cocos-bridge.js` 会注册:
- `processData` / `processVehicleData` / `externalResult` / `externalLevelInfo` / `coinsData`
- 等待 `window.unityInstance` 就绪
### 2. 删除 Unity 加载段
删除类似内容:
```html
<script src="Build/build.loader.js"></script>
<script>
createUnityInstance(canvas, config, ...).then((unityInstance) => {
window.unityInstance = unityInstance;
});
</script>
```
### 3. 粘贴 Cocos 启动脚本
构建完成后运行(自动生成粘贴清单):
```bash
node tools/patch-main-index.js build/web-desktop
```
将输出的 `<script src="cocos/...">` 按主站实际路径写入 `index.html`
**路径规则**
- 主站页面与 `cocos/` 同级 → `src="cocos/src/system.bundle.js"`
- 游戏在根目录 → 不加 `cocos/` 前缀
### 4. 游戏容器
保留或添加 Cocos 构建页中的结构:
```html
<div id="GameDiv">
<div id="Cocos3dGameContainer">
<canvas id="GameCanvas"></canvas>
</div>
</div>
```
若主站原用 `#unity-canvas`,可只改 id 为 `GameCanvas`,或保留原 id 并在构建模板里改 canvas id需改构建配置一般直接用 Cocos 默认即可)。
### 5. unityInstance 无需手写
游戏启动后 `GameController` 会自动执行:
```javascript
window.unityInstance = { SendMessage: ... };
window.cocosIns = 同上;
```
主站 Blockly 代码 **不用改**
```javascript
unityInstance.SendMessage("Player", "CallMove", n);
unityInstance.SendMessage("GameController", "SwitchLevel", levelId);
```
---
## 四、本地预览
### 仅预览 Cocos 构建(未合并主站)
用 Cocos 构建面板里的 **运行**,或:
```bash
cd build/web-desktop
npx --yes serve . -p 8080
# 打开 http://localhost:8080
```
### 预览合并后的主站目录
```bash
cd "/Users/liuyufei/tfrh/主站文件/主站/Template"
npx --yes serve . -p 8080
```
- 联调页:`http://localhost:8080/cocos-demo.html`
- 若已改主站 index`http://localhost:8080/index.html`
---
## 五、联调自检清单
| 步骤 | 期望 |
|------|------|
| 打开页面 | 控制台无 404检查 cocos/*.js 路径) |
| 加载完成 | `[GameController] unityInstance.SendMessage 已就绪` |
| | `[tfrh] Cocos bridge ready` |
| `SwitchLevel(1)` | 日志 `externalLevelInfo` |
| `CallMove(2)` | 角色移动 |
| `CallPlayerInfo` | 日志 `processData` |
---
## 六、生产环境注意
1. **HTTPS**:与 Unity 相同WebGL/Canvas 在混合内容下可能受限
2. **缓存**:构建带 MD5 时,更新后清 CDN 缓存
3. **跨域**`cocos/` 与主站同域部署最省事
4. **回退**:可保留 Unity `Build/` 目录,通过配置开关切换加载 Unity 或 Cocos
---
## 七、关卡
已导入 Levels600 共 **600** 关,`SwitchLevel(1..600)` 与 Unity 一致。