Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
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();
|
||
}
|
||
}
|