187 lines
8.8 KiB
TypeScript
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());
|
|
}
|
|
}
|