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,206 @@
import {
_decorator, Color, Component, Graphics, Layers, Node, UITransform, Vec3, view,
} from 'cc';
import { CELL_PIXEL, CAMERA_ORTHO_HALF, DESIGN_HEIGHT } from '../core/GridConstants';
import { cellToWorldCenter, getHalfCellSize } from '../core/GridCoords';
import { EventManager, EventType } from '../core/EventManager';
import { GameManager } from '../manager/GameManager';
import { GridSnapHelper } from '../level/GridSnapHelper';
const { ccclass } = _decorator;
const UI_LAYER = Layers.Enum.UI_2D;
/** 对齐 Unity LineGridRenderer导航按钮切换的满屏等距辅助网格 */
@ccclass('LineGridRenderer')
export class LineGridRenderer extends Component {
static instance: LineGridRenderer | null = null;
/** 以中心向外的格子半径(菱形个数 ≈ (2r+1)²) */
radius = 24;
cellSize = CELL_PIXEL;
private graphics: Graphics | null = null;
private visible = false;
static ensure(parent: Node): LineGridRenderer {
LineGridRenderer.purgeDuplicates(parent);
let comp = LineGridRenderer.instance?.isValid ? LineGridRenderer.instance : null;
if (!comp) {
const found = LineGridRenderer.findUnder(parent);
comp = found?.getComponent(LineGridRenderer) ?? null;
}
if (!comp) {
const node = new Node('LineGrid');
node.parent = parent;
comp = node.addComponent(LineGridRenderer);
} else if (comp.node.parent !== parent) {
comp.node.parent = parent;
}
comp.setupRuntime();
comp.sendToBack();
return comp;
}
/** 移除误挂在关卡内的重复 LineGrid会导致叠线无法关闭 */
private static purgeDuplicates(entrance: Node) {
const keep = LineGridRenderer.instance?.isValid
? LineGridRenderer.instance.node
: LineGridRenderer.findUnder(entrance);
const walk = (node: Node) => {
for (let i = node.children.length - 1; i >= 0; i--) {
const ch = node.children[i];
if (ch.name === 'LineGrid' && ch !== keep) {
ch.destroy();
continue;
}
walk(ch);
}
};
walk(entrance);
}
private static findUnder(root: Node): Node | null {
if (root.name === 'LineGrid') return root;
for (const ch of root.children) {
const hit = LineGridRenderer.findUnder(ch);
if (hit) return hit;
}
return null;
}
onLoad() {
if (LineGridRenderer.instance && LineGridRenderer.instance !== this) {
this.node.destroy();
return;
}
LineGridRenderer.instance = this;
this.setupRuntime();
EventManager.register(EventType.LevelInit, this.onLevelInit);
}
onDestroy() {
EventManager.remove(EventType.LevelInit, this.onLevelInit);
if (LineGridRenderer.instance === this) LineGridRenderer.instance = null;
}
isGridVisible(): boolean {
return this.visible;
}
private setupRuntime() {
this.node.layer = UI_LAYER;
if (!this.node.getComponent(UITransform)) {
this.node.addComponent(UITransform).setContentSize(1, 1);
}
const g = this.getComponent(Graphics) ?? this.addComponent(Graphics);
this.graphics = g;
g.lineWidth = 1.4;
g.strokeColor = new Color(0, 0, 0, 255);
if (!this.visible) {
g.clear();
this.node.active = false;
}
}
private onLevelInit = () => {
this.purgeEditorGrid();
this.syncLevelOffset();
this.updateGridRadius();
if (this.visible) {
this.rebuild();
this.node.active = true;
this.sendToBack();
} else {
this.graphics?.clear();
this.node.active = false;
}
};
toggleGridVisibility() {
this.setupRuntime();
this.visible = !this.visible;
if (this.visible) {
this.purgeEditorGrid();
this.syncLevelOffset();
this.updateGridRadius();
this.rebuild();
this.node.active = true;
this.sendToBack();
} else {
this.graphics?.clear();
this.node.active = false;
}
}
/** 移除编辑器 GridSnapHelper 遗留的蓝色/灰色参考格 */
private purgeEditorGrid() {
const entrance = this.node.parent;
if (entrance?.isValid) {
GridSnapHelper.purgeRuntimeGrids(entrance);
}
const scene = this.node.scene;
if (scene?.isValid) {
GridSnapHelper.purgeRuntimeGrids(scene);
}
}
/** 保持在 MainLevelEntrance 最底层,不遮挡关卡内砖块/角色 */
private sendToBack() {
const parent = this.node.parent;
if (parent?.isValid) {
this.node.setSiblingIndex(0);
}
}
/** 与关卡根节点对齐,网格覆盖当前可见砖块区域 */
private syncLevelOffset() {
const parent = this.node.parent;
if (!parent) return;
const levelRoot = parent.children.find(
(c) => c?.isValid && c !== this.node && (c.name ?? '').startsWith('Level'),
);
if (levelRoot?.isValid) {
const p = levelRoot.position;
this.node.setPosition(p.x, p.y, 0);
} else {
this.node.setPosition(0, 0, 0);
}
}
private updateGridRadius() {
const b = GameManager.instance?.getCurLevel()?.boundary;
const vs = view.getVisibleSize();
const halfH = CAMERA_ORTHO_HALF;
const halfW = halfH * (vs.width / Math.max(vs.height, DESIGN_HEIGHT));
const cover = Math.ceil(Math.max(halfW, halfH) / (CELL_PIXEL * 0.35)) + 6;
if (b) {
this.radius = Math.max(cover, b.x + 8, b.y + 8);
} else {
this.radius = cover;
}
}
private rebuild() {
const g = this.graphics;
if (!g) return;
g.clear();
const { halfW, halfH } = getHalfCellSize();
const r = this.radius;
for (let x = -r; x <= r; x++) {
for (let y = -r; y <= r; y++) {
const c = cellToWorldCenter(new Vec3(x, y, 0));
g.moveTo(c.x, c.y + halfH);
g.lineTo(c.x + halfW, c.y);
g.lineTo(c.x, c.y - halfH);
g.lineTo(c.x - halfW, c.y);
g.close();
}
}
g.stroke();
}
}