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

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": {}
}