Files
MatchMaster/assets/Script/prop/LaserView.ts
T
2026-10-20 11:24:09 +08:00

187 lines
8.8 KiB
TypeScript

const { ccclass, property } = cc._decorator;
export type LaserDirection = "right" | "up" | "left" | "down";
/** Laser artwork only. Reception and gate collision state belong to the map. */
@ccclass
export default class LaserView extends cc.Component {
@property(cc.SpriteAtlas)
atlas: cc.SpriteAtlas = null;
private colorIndex = 1;
private verticalGate = false;
private gateHalfLengths: number[] = [];
private gateOpen: boolean = null;
private slicedFrames: cc.SpriteFrame[] = [];
private beamFrames: { [name: string]: cc.SpriteFrame } = {};
private beamStartOffset = 0;
initBeam(color: number, direction: LaserDirection, startOffset = 0): void {
this.show("beam");
this.colorIndex = color;
this.beamStartOffset = startOffset;
this.node.getChildByName("beam").angle = this.angle(direction);
this.setFrame("beam/start", "laser_start" + color);
this.setFrame("beam/end", "laser_over" + color);
this.setBeam(0, false);
}
setBeam(length: number, connected: boolean): void {
length = Math.max(0, length);
const beam = this.node.getChildByName("beam");
const body = this.setFrame("beam/body", (connected ? "laser_win" : "laser_") + this.colorIndex);
const start = beam.getChildByName("start");
const end = beam.getChildByName("end");
start.x = Math.min(this.beamStartOffset, length);
start.width = Math.min(56, length - start.x);
end.width = Math.min(64, length);
// The tail already contains the thin beam; join it instead of drawing both on top.
const bodyLength = connected ? length : Math.max(0, length - end.width);
body.node.setContentSize(bodyLength, connected ? 94 : 42);
body.node.x = bodyLength / 2;
end.x = length;
end.active = !connected && length > 0;
body.node.active = bodyLength > 0;
start.active = length > start.x;
}
/** Place the hardware inside the wall's solid face, with its colored tip on the beam. */
initHead(role: "emitter" | "receiver", color: number, direction: LaserDirection, inset = 0, retraction = 0): void {
this.show("head");
const head = this.setFrame("head", (role === "emitter" ? "laser_send" : "laser_accept") + color);
head.node.setAnchorPoint(role === "emitter" ? 1 : 0, 0.5);
head.node.angle = this.angle(direction) + (role === "receiver" ? 180 : 0);
const x = direction === "right" ? 1 : direction === "left" ? -1 : 0;
const y = direction === "up" ? 1 : direction === "down" ? -1 : 0;
head.node.setPosition(x * (inset - retraction), y * (inset - retraction));
}
getBeamOrigin(): cc.Vec2 {
return this.node.convertToWorldSpaceAR(cc.v2(0, 0));
}
getVisibleBeamOrigin(): cc.Vec2 {
return this.node.getChildByName("head").convertToWorldSpaceAR(cc.v2(0, 0));
}
/** Attach at the first wall segment's laser socket. The door grows right/up. */
initGate(length: number, direction: LaserDirection, laserColor: number, centerY?: number, shadowEdge?: number): void {
this.show("gate");
this.verticalGate = direction === "left" || direction === "right";
const gate = this.node.getChildByName("gate");
gate.setPosition(this.verticalGate ? 0 : (length - 1) * 60,
centerY == null ? (this.verticalGate ? (length - 1) * 60 : 0) : centerY);
// The 2/3-cell vertical assets name the halves opposite to the 1-cell pair.
const names = this.verticalGate ? (length === 1 ? ["up", "down"] : ["down", "up"]) : ["left", "right"];
this.clearSlicedFrames();
names.forEach((name, index) => {
const sprite = this.setFrame("gate/" + (index === 0 ? "first" : "second"), "laser_" + name + "_" + length);
const originalSize = sprite.spriteFrame.getOriginalSize();
const halfLength = this.verticalGate ? originalSize.height : originalSize.width;
this.gateHalfLengths[index] = halfLength;
const frame = sprite.spriteFrame.clone();
// Both ends retain their original pixels, including the silver strip.
frame.insetLeft = frame.insetRight = this.verticalGate ? 2 : 8;
frame.insetTop = frame.insetBottom = this.verticalGate ? 8 : 2;
this.slicedFrames.push(frame);
sprite.spriteFrame = frame;
sprite.type = cc.Sprite.Type.SLICED;
// Cover the three transparent pixels at the inner edges without resizing the art.
const outerEdge = halfLength - 1.5;
if (this.verticalGate) {
sprite.node.setAnchorPoint(0.5, index === 0 ? 1 : 0);
sprite.node.setPosition(0, index === 0 ? outerEdge : -outerEdge);
} else {
sprite.node.setAnchorPoint(index === 0 ? 0 : 1, 0.5);
sprite.node.setPosition(index === 0 ? -outerEdge : outerEdge, 0);
}
});
// Center the unequal vertical halves, then lift the whole gate slightly to expose more base below.
if (this.verticalGate) gate.y += 3 - (this.gateHalfLengths[0] - this.gateHalfLengths[1]) / 2;
if (shadowEdge != null) {
const first = gate.getChildByName("first");
// Native artwork: the face/shadow boundary is at column 39 or row 42.
if (this.verticalGate) gate.x = shadowEdge - (39 - first.width / 2);
else gate.y = shadowEdge - (first.height / 2 - 42);
}
["firstDot", "secondDot"].forEach(name => {
const dot = this.setFrame("gate/" + name, "laser_yuan" + laserColor);
dot.node.setContentSize(14, 14);
});
this.gateOpen = null;
this.setGateOpen(false, true);
}
setGateOpen(open: boolean, immediate = false): void {
if (this.gateOpen === open && !immediate) return;
this.gateOpen = open;
const gate = this.node.getChildByName("gate");
["first", "second"].forEach((name, index) => {
const halfLength = this.gateHalfLengths[index];
const size = open ? 16 : halfLength;
const half = gate.getChildByName(name);
const dot = gate.getChildByName(name + "Dot");
const offset = (open ? halfLength - size - 1.5 : 0) * (index === 0 ? 1 : -1);
const dotPosition = this.verticalGate ? { x: 0, y: offset } : { x: -offset, y: 0 };
cc.Tween.stopAllByTarget(half);
cc.Tween.stopAllByTarget(dot);
if (immediate) {
if (this.verticalGate) half.height = size;
else half.width = size;
dot.setPosition(dotPosition.x, dotPosition.y);
} else {
cc.tween(half).to(0.2, this.verticalGate ? { height: size } : { width: size }).start();
cc.tween(dot).to(0.2, dotPosition).start();
}
});
}
private show(name: string): void {
this.node.children.forEach(child => child.active = child.name === name);
}
private setFrame(path: string, name: string): cc.Sprite {
const node = path.split("/").reduce((parent, part) => parent.getChildByName(part), this.node);
const sprite = node.getComponent(cc.Sprite);
let frame = this.atlas.getSpriteFrame(name + ".png") || this.atlas.getSpriteFrame(name);
if (!frame) throw new Error("Missing laser artwork: " + name);
if (path === "beam/end" || (path === "beam/body" && name === "laser_" + this.colorIndex)) {
if (!this.beamFrames[name]) {
const joinedFrame = frame.clone();
const rect = joinedFrame.getRect();
// Sample inside the adjoining edges, avoiding the atlas's transparent padding.
if (path === "beam/end") {
if (joinedFrame.isRotated()) rect.y += 0.5;
else rect.x += 0.5;
}
rect.width -= 0.5;
joinedFrame.setRect(rect);
this.beamFrames[name] = joinedFrame;
}
frame = this.beamFrames[name];
}
if (sprite.spriteFrame !== frame) {
sprite.spriteFrame = frame;
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
node.setContentSize(frame.getOriginalSize());
}
return sprite;
}
private angle(direction: LaserDirection): number {
return { right: 0, up: 90, left: 180, down: -90 }[direction];
}
private clearSlicedFrames(): void {
this.slicedFrames.forEach(frame => frame.destroy());
this.slicedFrames.length = 0;
}
onDestroy(): void {
const gate = this.node.getChildByName("gate");
if (gate) gate.children.forEach(child => cc.Tween.stopAllByTarget(child));
this.clearSlicedFrames();
Object.keys(this.beamFrames).forEach(name => this.beamFrames[name].destroy());
}
}