Complete Cocos Creator port with level bundles, themes, and tooling.

Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 15:30:58 +08:00
parent cba5105908
commit d393302388
6248 changed files with 17322729 additions and 11036 deletions

View File

@@ -0,0 +1,366 @@
import {
_decorator, Component, Vec3, Graphics, Color, Node, UITransform, Layers, Prefab,
} from 'cc';
import { EDITOR, PREVIEW, BUILD } from 'cc/env';
import {
cellToWorldCenter, tileNodeName, parseTileNodeName, getHalfCellSize,
} from '../core/GridCoords';
import { CommonDefine } from '../core/Define';
import { LevelMapData } from './LevelMapData';
import { alignTileNode, setupLayerContainer, sortIsoTiles } from './TileLayout';
const { ccclass, property, executeInEditMode, executionOrder } = _decorator;
const UI_LAYER = Layers.Enum.UI_2D;
/** 与关卡编辑器 canvas 一致(仅网格线,无填充) */
const GRID_STROKE = new Color(85, 85, 85, 255);
const CELL_STROKE = new Color(74, 158, 255, 255);
const RUNTIME_GRID_NODE_NAMES = new Set(['_GridSnapGizmo']);
/**
* 挂在关卡预制体根节点LevelN。编辑器中
* - 显示与关卡地图编辑面板一致的等距菱形参考格(无灰底,不遮挡主题背景)
* - 拖动 Ground/Border/Tiles 下瓦片时吸附到格子中心
*/
@ccclass('GridSnapHelper')
@executeInEditMode(true)
@executionOrder(-200)
export class GridSnapHelper extends Component {
@property({ displayName: '吸附格子', tooltip: '场景内移动瓦片时自动对齐格子中心' })
snapEnabled = true;
@property({ displayName: '显示参考网格' })
showGrid = false;
@property({ displayName: '网格半径', min: 4, max: 24, step: 1, tooltip: '无地图数据时的备用半径' })
gridRadius = 12;
@property({ displayName: '地图周围额外格数', min: 0, max: 6, step: 1 })
gridPadding = 2;
@property({ displayName: '高亮已有砖块的格子' })
highlightOccupied = true;
@property({ displayName: '吸附时同步节点名' })
syncNodeNames = true;
private _gridHost: Node | null = null;
private _graphics: Graphics | null = null;
private _alignedOnce = false;
/** 仅「编辑器内编辑预制体/场景、未点预览」时为 true */
static isEditingInEditor(): boolean {
return EDITOR && !PREVIEW && !BUILD;
}
/** 预览、构建、真机运行 */
static isRuntime(): boolean {
return !GridSnapHelper.isEditingInEditor();
}
/**
* 实例化前关闭 prefab 内 showGrid仅当 prefab.data 可安全访问时)。
* 编辑器预览若脚本未编译完成,访问 prefab.data 可能抛错;失败时由实例化后 stripBeforePlay 兜底。
*/
static stripBeforePlayFromPrefab(prefab: Prefab) {
try {
const root = prefab.data as Node | null;
if (!root?.isValid) return;
for (const snap of root.getComponentsInChildren(GridSnapHelper)) {
if (!snap?.isValid) continue;
snap.showGrid = false;
snap.enabled = false;
}
} catch (e) {
console.warn('[GridSnapHelper] stripBeforePlayFromPrefab 跳过(将在实例化后清理)', e);
}
}
/** 关卡实例化后立即剥离编辑器网格 */
static stripBeforePlay(levelRoot: Node) {
if (!levelRoot?.isValid) return;
for (const snap of levelRoot.getComponents(GridSnapHelper)) {
if (!snap.isValid) continue;
snap.showGrid = false;
snap.enabled = false;
snap.removeGridGizmo();
}
GridSnapHelper.destroyGridGizmoNodes(levelRoot);
}
/** 预览/构建/真机运行:彻底移除一切网格节点与组件(无条件,由游戏逻辑调用) */
static purgeRuntimeGrids(root: Node) {
if (!root?.isValid) return;
try {
for (const snap of [...root.getComponentsInChildren(GridSnapHelper)]) {
if (!snap?.isValid) continue;
snap.showGrid = false;
snap.enabled = false;
snap.removeGridGizmo();
snap.destroy();
}
GridSnapHelper.destroyGridGizmoNodes(root);
} catch (e) {
console.warn('[GridSnapHelper] purgeRuntimeGrids 部分失败', e);
}
}
/** 递归销毁编辑器参考格节点 */
static destroyGridGizmoNodes(root: Node) {
const walk = (node: Node) => {
for (let i = node.children.length - 1; i >= 0; i--) {
const ch = node.children[i];
if (RUNTIME_GRID_NODE_NAMES.has(ch.name)) {
ch.getComponent(Graphics)?.clear();
ch.destroy();
continue;
}
walk(ch);
}
};
walk(root);
}
static purgeScene(scene: Node) {
GridSnapHelper.purgeRuntimeGrids(scene);
}
/** @deprecated 使用 purgeRuntimeGrids */
static stripRuntimeGrid(levelRoot: Node) {
GridSnapHelper.purgeRuntimeGrids(levelRoot);
}
onLoad() {
if (!GridSnapHelper.isEditingInEditor()) {
this.showGrid = false;
this.enabled = false;
this.removeGridGizmo();
this.destroy();
return;
}
this.ensureGridGraphics();
this.alignAllTiles();
this._alignedOnce = true;
}
onEnable() {
if (!GridSnapHelper.isEditingInEditor()) {
this.showGrid = false;
this.removeGridGizmo();
return;
}
if (!this.showGrid) return;
this.ensureGridGraphics();
if (!this._alignedOnce) this.alignAllTiles();
this.drawReferenceGrid();
}
start() {
if (!GridSnapHelper.isEditingInEditor()) this.removeGridGizmo();
}
removeGridGizmo() {
this.showGrid = false;
const host = this.node.getChildByName('_GridSnapGizmo');
if (host) {
host.getComponent(Graphics)?.clear();
host.destroy();
}
this._gridHost = null;
this._graphics = null;
}
lateUpdate() {
if (!GridSnapHelper.isEditingInEditor()) {
this.removeGridGizmo();
return;
}
if (!this.showGrid) return;
this.drawReferenceGrid();
}
/** 供扩展场景脚本或外部调用 */
snapNode(node: Node | null) {
if (!node || !node.isValid) return;
const layer = this.resolveLayer(node);
if (!layer) return;
const { ground, border } = this.readMapJson();
this.applySnap(node, layer, ground, border);
sortIsoTiles(this.node);
}
private resolveLayer(node: Node): 'ground' | 'border' | null {
let cur: Node | null = node;
while (cur) {
if (cur.name === 'Ground') return 'ground';
if (cur.name === 'Border') return 'border';
if (cur === this.node) break;
cur = cur.parent;
}
const parsed = parseTileNodeName(node.name);
return parsed?.layer ?? null;
}
private readMapJson(): {
ground: Record<string, string>;
border: Record<string, boolean | string>;
} {
const md = this.node.getComponent(LevelMapData);
if (!md) return { ground: {}, border: {} };
try {
return {
ground: JSON.parse(md.groundJson || '{}'),
border: JSON.parse(md.borderJson || '{}'),
};
} catch {
return { ground: {}, border: {} };
}
}
private occupiedKeys(ground: Record<string, string>, border: Record<string, boolean | string>): Set<string> {
return new Set([...Object.keys(ground), ...Object.keys(border)]);
}
/** 编辑器打开预制体时对齐全部瓦片并合并深度排序 */
alignAllTiles() {
const { ground, border } = this.readMapJson();
if (this.snapEnabled) {
this.snapLayerChildren('Ground', 'ground', ground, border);
this.snapLayerChildren('Border', 'border', ground, border);
this.snapLayerChildren('Tiles', null, ground, border);
}
sortIsoTiles(this.node);
}
private snapLayerChildren(
layerName: string,
layer: 'ground' | 'border' | null,
groundMap: Record<string, string>,
borderMap: Record<string, boolean | string>,
) {
const layerNode = this.node.getChildByName(layerName);
if (!layerNode) return;
setupLayerContainer(layerNode);
for (const ch of layerNode.children) {
const parsed = parseTileNodeName(ch.name);
if (!parsed) continue;
const useLayer = layer ?? parsed.layer;
this.applySnap(ch, useLayer, groundMap, borderMap);
}
}
private inferTileName(
layer: 'ground' | 'border',
key: string,
groundMap: Record<string, string>,
borderMap: Record<string, boolean | string>,
): string {
if (layer === 'ground') return groundMap[key] ?? CommonDefine.BlockBase;
const v = borderMap[key];
if (typeof v === 'string') return v;
return 'WallBlock';
}
private applySnap(
node: Node,
layer: 'ground' | 'border',
groundMap: Record<string, string>,
borderMap: Record<string, boolean | string>,
) {
const parsed = parseTileNodeName(node.name);
if (!parsed) return;
const key = `${parsed.x},${parsed.y}`;
const tileName = this.inferTileName(layer, key, groundMap, borderMap);
alignTileNode(node, parsed.x, parsed.y, tileName);
if (this.syncNodeNames) {
const expected = tileNodeName(layer, parsed.x, parsed.y);
if (node.name !== expected) node.name = expected;
}
}
private ensureGridGraphics() {
if (!GridSnapHelper.isEditingInEditor()) return;
let host = this.node.getChildByName('_GridSnapGizmo');
if (!host) {
host = new Node('_GridSnapGizmo');
host.parent = this.node;
}
host.layer = UI_LAYER;
host.setSiblingIndex(0);
let ui = host.getComponent(UITransform);
if (!ui) ui = host.addComponent(UITransform);
ui.setAnchorPoint(0, 0);
ui.setContentSize(1, 1);
let g = host.getComponent(Graphics);
if (!g) g = host.addComponent(Graphics);
this._gridHost = host;
this._graphics = g;
}
private iterGridCells(): { x: number; y: number }[] {
const { ground, border } = this.readMapJson();
const keys = [...Object.keys(ground), ...Object.keys(border)];
if (!keys.length) {
const out: { x: number; y: number }[] = [];
const r = this.gridRadius;
for (let x = -r; x <= r; x++) {
for (let y = -r; y <= r; y++) out.push({ x, y });
}
return out;
}
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
for (const k of keys) {
const [xs, ys] = k.split(',');
const x = parseInt(xs, 10);
const y = parseInt(ys, 10);
if (Number.isNaN(x) || Number.isNaN(y)) continue;
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
}
const pad = this.gridPadding;
const out: { x: number; y: number }[] = [];
for (let x = minX - pad; x <= maxX + pad; x++) {
for (let y = minY - pad; y <= maxY + pad; y++) out.push({ x, y });
}
return out;
}
private traceDiamond(g: Graphics, cx: number, cy: number, halfW: number, halfH: number) {
g.moveTo(cx, cy + halfH);
g.lineTo(cx + halfW, cy);
g.lineTo(cx, cy - halfH);
g.lineTo(cx - halfW, cy);
g.close();
}
private drawReferenceGrid() {
if (!GridSnapHelper.isEditingInEditor() || !this.showGrid) return;
this.ensureGridGraphics();
const g = this._graphics;
if (!g) return;
const { halfW, halfH } = getHalfCellSize();
const cells = this.iterGridCells();
const { ground, border } = this.readMapJson();
const occupied = this.highlightOccupied ? this.occupiedKeys(ground, border) : new Set<string>();
g.clear();
for (const { x, y } of cells) {
const w = cellToWorldCenter(new Vec3(x, y, 0));
const key = `${x},${y}`;
const isOcc = occupied.has(key);
g.lineWidth = isOcc ? 1.5 : 1;
g.strokeColor = isOcc ? CELL_STROKE : GRID_STROKE;
this.traceDiamond(g, w.x, w.y, halfW, halfH);
g.stroke();
}
}
}