激光版本

This commit is contained in:
COMPUTER\EDY
2026-10-20 11:24:09 +08:00
parent f9aa73f68a
commit 06ef53017d
66 changed files with 17961 additions and 2 deletions
+225
View File
@@ -0,0 +1,225 @@
import type MapController from "./Map";
import type Wall from "./Wall";
import { LQCollide } from "./lq_collide_system/lq_collide";
import { LQCollideSystem } from "./lq_collide_system/lq_collide_system";
import { LQCollideShape } from "./lq_base/data/lq_const";
import { LaserCollider, nearestLaserHit } from "./LaserRules";
import LaserView from "./prop/LaserView";
export interface LaserConfig {
id: string;
/** Artwork index (1..10), independent of the exit's normal color. */
color: number;
emitter: { num: number };
receiver: { num: number };
}
interface LaserGroup {
config: LaserConfig;
emitter: Wall;
receiver: Wall;
gates: Wall[];
source?: LaserView;
target?: LaserView;
beam?: LaserView;
gate?: LaserView;
connected?: boolean;
}
const DIRECTIONS = {
// Retraction excludes the wall texture's transparent edge/shadow and the beam's 1px clearance.
right: { x: 1, y: 0, opposite: "left", headRetraction: 8 },
left: { x: -1, y: 0, opposite: "right", headRetraction: 6 },
up: { x: 0, y: 1, opposite: "down", headRetraction: 1 },
down: { x: 0, y: -1, opposite: "up", headRetraction: 14 },
};
/** Keeps laser reception independent of door/beam animation. */
export default class LaserSystem {
private groups: LaserGroup[] = [];
private nodes: cc.Node[] = [];
private layer: cc.Node;
constructor(private map: MapController, slots: Wall[], configs: LaserConfig[], prefab: cc.Prefab) {
this.groups = this.validate(slots, configs);
if (!prefab) throw new Error("Map.Block_Prop[26] 缺少激光预制体");
try {
this.layer = new cc.Node("LaserLayer");
this.layer.parent = map.node;
this.layer.zIndex = 10;
for (const group of this.groups) {
group.source = this.attach(prefab, group.emitter.node.parent.getChildByName("laser"));
group.source.initHead("emitter", group.config.color, group.emitter.direction,
this.headInset(group.emitter), DIRECTIONS[group.emitter.direction].headRetraction);
group.target = this.attach(prefab, group.receiver.node.parent.getChildByName("laser"));
group.target.initHead("receiver", group.config.color, group.receiver.direction,
this.headInset(group.receiver), DIRECTIONS[group.receiver.direction].headRetraction);
const first = group.gates.find(wall => wall.wall_Info.length > 0);
group.gate = this.attach(prefab, first.node.parent.getChildByName("laser"));
const gateFace = first.node.getBoundingBox();
const socket = group.gate.node.parent;
// The base door's bottom 13px are shadow, not part of its solid colored face.
const gateCenterY = (gateFace.yMin + gateFace.yMax) / 2 + 6.5 - socket.y;
// Align the native face/shadow boundary; color 8's boundary is one pixel earlier.
const colorOffset = first.wall_Info.color === 8 ? 1 : 0;
const shadowEdge = first.direction === "left" || first.direction === "right"
? gateFace.xMin + 43 - colorOffset - socket.x
: gateFace.yMax - 38 + colorOffset - socket.y;
group.gate.initGate(first.wall_Info.length, first.direction, group.config.color, gateCenterY, shadowEdge);
group.beam = this.attach(prefab, this.layer);
// Start the body at the colored tip, but keep its flare clear of the wall shadow.
group.beam.initBeam(group.config.color, group.emitter.direction, DIRECTIONS[group.emitter.direction].headRetraction);
}
this.update(true);
} catch (error) {
this.dispose();
throw error;
}
}
private validate(slots: Wall[], configs: LaserConfig[]): LaserGroup[] {
const ids = new Set<string>();
const heads = new Set<number>();
const straight = (wall: Wall) => wall && DIRECTIONS[wall.direction]
&& this.map.mapBlocksWall[wall.posX][wall.posY].opacity === 250
&& wall.node.parent.getChildByName("laser");
const groups = configs.map(config => {
const fail = (message: string) => { throw new Error("激光组 " + config.id + ": " + message); };
if (typeof config.id !== "string" || !config.id || ids.has(config.id)) fail("编号缺失或重复");
ids.add(config.id);
if (!Number.isInteger(config.color) || config.color < 1 || config.color > 10) fail("激光色必须为 1~10");
const emitter = slots[config.emitter && config.emitter.num];
const receiver = slots[config.receiver && config.receiver.num];
if (!straight(emitter) || !straight(receiver)) fail("发射和接收必须放在完整直墙段");
for (const endpoint of [config.emitter, config.receiver]) {
if (!Number.isInteger(endpoint.num) || heads.has(endpoint.num) || slots[endpoint.num].wall_Info) {
fail("发射或接收墙位已被占用");
}
heads.add(endpoint.num);
}
const direction = DIRECTIONS[emitter.direction];
const dx = receiver.posX - emitter.posX, dy = receiver.posY - emitter.posY;
if (receiver.direction !== direction.opposite || dx * direction.y !== dy * direction.x
|| dx * direction.x + dy * direction.y <= 0) fail("发射和接收必须同一直线且相向");
const gates = slots.filter(wall => wall && wall.wall_Info && wall.wall_Info.laserId === config.id);
const starts = gates.filter(wall => wall.wall_Info.length > 0);
if (starts.length !== 1 || starts[0].wall_Info.length !== gates.length || gates.length > 3) {
fail("必须对应一扇完整的 1~3 格控制门");
}
const first = starts[0];
const vertical = first.direction === "left" || first.direction === "right";
const sorted = gates.slice().sort((a, b) => vertical ? a.posY - b.posY : a.posX - b.posX);
sorted.forEach((wall, index) => {
const info = wall.wall_Info;
if (!straight(wall) || wall.direction !== first.direction || info.color !== first.wall_Info.color
|| wall.posX !== first.posX + (vertical ? 0 : index)
|| wall.posY !== first.posY + (vertical ? index : 0)
|| info.special !== 0 || info.toggle != null || info.longAndShort || info.stickLength
|| info.colorArray || info.jump != null || info.lockTime != null) {
fail("控制门必须是连续同色直门,不能叠加其他门机制");
}
wall.open = false;
});
return { config, emitter, receiver, gates };
});
if (slots.some(wall => wall && wall.wall_Info && wall.wall_Info.laserId != null
&& !ids.has(wall.wall_Info.laserId))) throw new Error("激光控制门缺少对应组配置");
return groups;
}
private attach(prefab: cc.Prefab, parent: cc.Node): LaserView {
parent.active = true;
const node = cc.instantiate(prefab);
this.nodes.push(node);
node.parent = parent;
node.setPosition(0, 0);
const view = node.getComponent(LaserView);
if (!view) throw new Error("激光预制体缺少 LaserView 组件");
return view;
}
private headInset(wall: Wall): number {
const face = wall.node.getBoundingBox();
const socket = wall.node.parent.getChildByName("laser");
switch (wall.direction) {
case "right": return Math.max(0, face.xMax - socket.x) + 1;
case "left": return Math.max(0, socket.x - face.xMin) + 1;
case "up": return Math.max(0, face.yMax - socket.y) + 1;
case "down": return Math.max(0, socket.y - face.yMin) + 1;
}
return 0;
}
update(immediate = false): void {
const colliders = this.collectColliders();
for (const group of this.groups) {
const start = group.source.getBeamOrigin(), end = group.target.getBeamOrigin();
const obstacles = colliders.filter(item => item.wall !== group.emitter.node.parent
&& item.wall !== group.receiver.node.parent).map(item => item.shape);
const hit = nearestLaserHit(start, end, obstacles);
const connected = !hit;
// All segments change state before starting the visual tween.
group.gates.forEach(wall => wall.open = connected);
if (group.connected !== connected || immediate) {
group.connected = connected;
group.gate.setGateOpen(connected, immediate);
}
const localStart = this.layer.convertToNodeSpaceAR(group.source.getVisibleBeamOrigin());
const localEnd = this.layer.convertToNodeSpaceAR(hit ? cc.v2(hit.point.x, hit.point.y) : group.target.getVisibleBeamOrigin());
const direction = DIRECTIONS[group.emitter.direction];
const visibleLength = (localEnd.x - localStart.x) * direction.x + (localEnd.y - localStart.y) * direction.y;
group.beam.node.setPosition(localStart);
group.beam.setBeam(Math.max(0, visibleLength), connected);
}
}
private collectColliders(): { shape: LaserCollider; wall: cc.Node }[] {
const result: { shape: LaserCollider; wall: cc.Node }[] = [];
for (const collider of LQCollideSystem.collide_arr) {
const node = collider.node;
if (!cc.isValid(node, true) || !node.activeInHierarchy || !collider.is_enable
|| !collider.enabled || collider.data_string === "-1" || collider.data_string === "-2") continue;
if (node.name === "rise" && this.map.riseFallBlock.indexOf(node.parent) >= 0) continue;
let ancestor = node, wall: cc.Node = null, transparent = false;
while (ancestor && ancestor !== this.map.node) {
const block = ancestor.getComponent("Block");
if (block && (block.type === 22 || block.over)) transparent = true;
if (ancestor.getChildByName("wall") && ancestor.getChildByName("wall").getComponent("Wall")) wall = ancestor;
ancestor = ancestor.parent;
}
if (ancestor !== this.map.node || transparent) continue;
const shape = this.colliderShape(collider);
if (shape) result.push({ shape, wall });
}
return result;
}
private colliderShape(collider: LQCollide): LaserCollider {
// LQ scales dimensions/points at initialization. Transform only the center;
// applying the node's world scale again would enlarge the collision shape.
const center = collider.node.convertToWorldSpaceAR(collider.offset);
if (collider.collide_shape === LQCollideShape.Rect) {
return { type: "rect", minX: center.x - collider.size.width / 2,
maxX: center.x + collider.size.width / 2, minY: center.y - collider.size.height / 2,
maxY: center.y + collider.size.height / 2 };
}
if (collider.collide_shape === LQCollideShape.Circle) {
return { type: "circle", center, radius: collider.radius };
}
if (collider.collide_shape === LQCollideShape.Polygon) {
const angle = collider.node.angle * Math.PI / 180, cos = Math.cos(angle), sin = Math.sin(angle);
return { type: "polygon", points: collider.polygon_points.map(point => ({
x: center.x + point.x * cos - point.y * sin,
y: center.y + point.x * sin + point.y * cos,
})) };
}
return null;
}
dispose(): void {
this.nodes.forEach(node => { if (cc.isValid(node)) node.destroy(); });
this.nodes.length = 0;
if (this.layer && cc.isValid(this.layer)) this.layer.destroy();
this.groups.length = 0;
}
}