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:
206
assets/scripts/gameplay/LineGridRenderer.ts
Normal file
206
assets/scripts/gameplay/LineGridRenderer.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user