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()); } }