360 lines
13 KiB
TypeScript
360 lines
13 KiB
TypeScript
const { ccclass, property } = cc._decorator;
|
|
|
|
export interface SpawnGateConfig {
|
|
id?: string;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
spawnQueue: Array<{ members: any[] }>;
|
|
}
|
|
|
|
export interface SpawnGateBatchResult {
|
|
nodes: cc.Node[];
|
|
opacities: number[];
|
|
}
|
|
|
|
enum SpawnGateState {
|
|
Waiting,
|
|
Opening,
|
|
Revealing,
|
|
Closing,
|
|
}
|
|
|
|
@ccclass
|
|
export default class SpawnGate extends cc.Component {
|
|
@property(cc.SpriteFrame)
|
|
leftTileFrame: cc.SpriteFrame = null;
|
|
|
|
@property(cc.SpriteFrame)
|
|
rightTileFrame: cc.SpriteFrame = null;
|
|
|
|
@property(cc.SpriteFrame)
|
|
middleFrame: cc.SpriteFrame = null;
|
|
|
|
@property(cc.SpriteFrame)
|
|
counterFrame: cc.SpriteFrame = null;
|
|
|
|
@property(cc.SpriteFrame)
|
|
innerEdgeFrame: cc.SpriteFrame = null;
|
|
|
|
@property(cc.SpriteFrame)
|
|
innerCornerFrame: cc.SpriteFrame = null;
|
|
|
|
@property(cc.SpriteFrame)
|
|
middleEdgeFrame: cc.SpriteFrame = null;
|
|
|
|
@property(cc.SpriteFrame)
|
|
outerEdgeFrame: cc.SpriteFrame = null;
|
|
|
|
@property(cc.SpriteFrame)
|
|
outerCornerFrame: cc.SpriteFrame = null;
|
|
|
|
@property(sp.SkeletonData)
|
|
revealSmokeData: sp.SkeletonData = null;
|
|
|
|
private map: any = null;
|
|
private config: SpawnGateConfig = null;
|
|
private queueIndex = 0;
|
|
private state = SpawnGateState.Waiting;
|
|
private elapsed = 0;
|
|
private finished = false;
|
|
private batch: SpawnGateBatchResult = null;
|
|
private leftDoor: cc.Node = null;
|
|
private rightDoor: cc.Node = null;
|
|
private cornerFrames: cc.SpriteFrame[] = [];
|
|
private countLabel: cc.Label = null;
|
|
|
|
init(map: any, config: SpawnGateConfig) {
|
|
this.map = map;
|
|
this.config = config;
|
|
this.queueIndex = 0;
|
|
this.state = SpawnGateState.Waiting;
|
|
this.elapsed = 0;
|
|
this.finished = false;
|
|
this.batch = null;
|
|
|
|
const left = this.node.getChildByName("left");
|
|
const right = this.node.getChildByName("right");
|
|
this.leftDoor = left;
|
|
this.rightDoor = right;
|
|
const count = this.node.getChildByName("count");
|
|
this.countLabel = count && count.getComponent(cc.Label);
|
|
this.configureArtwork(left, right);
|
|
this.updateRemainingCount();
|
|
if (this.countLabel) this.countLabel.node.active = true;
|
|
|
|
const bottomLeft = map.mapBlocksWall[config.x][config.y];
|
|
this.node.setPosition(
|
|
bottomLeft.x + (config.width - 1) * 60,
|
|
bottomLeft.y + (config.height - 1) * 60,
|
|
);
|
|
const gateWidth = config.width * 120;
|
|
const gateHeight = config.height * 120;
|
|
this.node.setContentSize(gateWidth, gateHeight);
|
|
const bg = this.node.getChildByName("bg");
|
|
if (bg) bg.setContentSize(gateWidth, gateHeight);
|
|
this.node.zIndex = -10;
|
|
const line = this.node.getChildByName("line");
|
|
if (line) line.active = false;
|
|
this.setClosedAmount(1);
|
|
}
|
|
|
|
hasOutstandingWork(): boolean {
|
|
return !this.finished;
|
|
}
|
|
|
|
private configureArtwork(left: cc.Node, right: cc.Node) {
|
|
const width = this.config.width * 120;
|
|
const height = this.config.height * 120;
|
|
const viewport = new cc.Node("doorViewport");
|
|
viewport.parent = this.node;
|
|
viewport.setSiblingIndex(1);
|
|
viewport.setContentSize(width, height);
|
|
viewport.addComponent(cc.Mask).type = cc.Mask.Type.RECT;
|
|
const innerCorner = this.createCornerFrame(this.innerCornerFrame);
|
|
this.configureDoorPart(left, viewport, -1, width / 2, height, innerCorner);
|
|
this.configureDoorPart(right, viewport, 1, width / 2, height, innerCorner);
|
|
|
|
const frame = this.node.getChildByName("kuang");
|
|
const sprite = frame.getComponent(cc.Sprite);
|
|
if (sprite) sprite.enabled = false;
|
|
frame.setContentSize(width, height);
|
|
this.drawFrame(frame, width, height, this.outerEdgeFrame,
|
|
this.createCornerFrame(this.outerCornerFrame));
|
|
}
|
|
|
|
private createCornerFrame(source: cc.SpriteFrame): cc.SpriteFrame {
|
|
const frame = source.clone();
|
|
// These assets are top/right L corners, not complete rectangular frames.
|
|
frame.insetTop = frame.insetRight = 16;
|
|
frame.insetLeft = frame.insetBottom = 0;
|
|
this.cornerFrames.push(frame);
|
|
return frame;
|
|
}
|
|
|
|
private addArtwork(parent: cc.Node, name: string, frame: cc.SpriteFrame,
|
|
x: number, y: number, width?: number, height?: number): cc.Node {
|
|
const node = new cc.Node(name);
|
|
node.parent = parent;
|
|
node.setPosition(x, y);
|
|
const sprite = node.addComponent(cc.Sprite);
|
|
sprite.spriteFrame = frame;
|
|
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
|
|
const size = frame.getOriginalSize();
|
|
node.setContentSize(width == null ? size.width : width, height == null ? size.height : height);
|
|
return node;
|
|
}
|
|
|
|
private configureDoorPart(node: cc.Node, viewport: cc.Node, side: number,
|
|
width: number, height: number, corner: cc.SpriteFrame) {
|
|
node.parent = viewport;
|
|
node.setContentSize(width, height);
|
|
const oldSprite = node.getComponent(cc.Sprite);
|
|
if (oldSprite) oldSprite.enabled = false;
|
|
const wood = new cc.Node("wood");
|
|
wood.parent = node;
|
|
wood.setContentSize(width, height);
|
|
wood.addComponent(cc.Mask).type = cc.Mask.Type.RECT;
|
|
const seam = -side * width / 2;
|
|
|
|
// Centre a 120px band on the circle; even heights crop half a row at each outer edge.
|
|
for (let row = -Math.ceil(height / 240); row <= Math.ceil(height / 240); row++) {
|
|
const y = row * 120;
|
|
if (Math.abs(y) - 60 >= height / 2) continue;
|
|
const tile = Math.abs(row) % 2 ? this.leftTileFrame : this.rightTileFrame;
|
|
for (let col = 0; col * 120 < width; col++) {
|
|
const plank = this.addArtwork(wood, "plank", tile, seam + side * (col * 120 + 60), y);
|
|
plank.scaleX = side;
|
|
}
|
|
}
|
|
const middle = this.addArtwork(wood, "middle", this.middleFrame, seam + side * 30, 0);
|
|
middle.scaleX = side;
|
|
// Each leaf clips its own half of the circle, so it follows the opening door.
|
|
this.addArtwork(wood, "circle", this.counterFrame, seam, 0);
|
|
|
|
const border = new cc.Node("innerFrame");
|
|
border.parent = node;
|
|
const thickness = this.innerEdgeFrame.getOriginalSize().width;
|
|
border.x = -side * thickness / 4;
|
|
this.drawFrame(border, width + thickness / 2, height, this.innerEdgeFrame, corner, -side);
|
|
const arc = this.addArtwork(node, "middleFrame", this.middleEdgeFrame,
|
|
seam + side * (30 - thickness / 2), 0);
|
|
arc.scaleX = -side;
|
|
}
|
|
|
|
private drawFrame(parent: cc.Node, width: number, height: number,
|
|
edge: cc.SpriteFrame, corner: cc.SpriteFrame, gapSide = 0) {
|
|
const thickness = edge.getOriginalSize().width;
|
|
const cap = 16;
|
|
// The inner strip's light/dark sides face opposite to its L-corner asset.
|
|
const edgeDirection = edge === this.innerEdgeFrame ? -1 : 1;
|
|
for (const xSide of [-1, 1]) {
|
|
const x = xSide * (width - thickness) / 2;
|
|
if (xSide === gapSide) {
|
|
const length = Math.max(0, height / 2 - 60 - cap);
|
|
if (length > 0) for (const ySide of [-1, 1]) {
|
|
const line = this.addArtwork(parent, "innerSeam", edge,
|
|
x, ySide * (60 + length / 2), thickness, length);
|
|
line.scaleX = xSide * edgeDirection;
|
|
}
|
|
} else {
|
|
const line = this.addArtwork(parent, "verticalEdge", edge, x, 0, thickness, height - cap * 2);
|
|
line.scaleX = xSide * edgeDirection;
|
|
}
|
|
for (const ySide of [-1, 1]) {
|
|
const node = this.addArtwork(parent, "corner", corner,
|
|
xSide * (width - cap) / 2, ySide * (height - cap) / 2, cap, cap);
|
|
node.getComponent(cc.Sprite).type = cc.Sprite.Type.SLICED;
|
|
node.scaleX = xSide;
|
|
node.scaleY = ySide;
|
|
}
|
|
}
|
|
for (const ySide of [-1, 1]) {
|
|
const line = this.addArtwork(parent, "horizontalEdge", edge,
|
|
0, ySide * (height - thickness) / 2, thickness, width - cap * 2);
|
|
line.angle = 90 * ySide * edgeDirection;
|
|
}
|
|
}
|
|
|
|
update(dt: number) {
|
|
if (this.finished || !this.map || !cc.isValid(this.map, true)) return;
|
|
if (this.map.gameOver || this.map.gameWin) return;
|
|
|
|
if (this.state === SpawnGateState.Waiting) {
|
|
if (this.queueIndex >= this.config.spawnQueue.length) {
|
|
this.finishGate();
|
|
return;
|
|
}
|
|
// 冻结只暂停计时;真正的暂停或弹窗仍阻止下一批生成。
|
|
const scene = this.map.SceneManager;
|
|
const paused = this.map.pause && (!this.map.iceTrue() || this.map.shopPause
|
|
|| (scene && scene.isSpawnGateBlocked()));
|
|
if (paused || !this.map.isSpawnGateAreaEmpty(this.config)) return;
|
|
this.beginBatch();
|
|
return;
|
|
}
|
|
|
|
this.elapsed += dt;
|
|
if (this.state === SpawnGateState.Opening) {
|
|
const progress = Math.min(1, this.elapsed / 0.3);
|
|
this.setClosedAmount(1 - progress);
|
|
if (progress >= 1) this.enterState(SpawnGateState.Revealing);
|
|
}
|
|
else if (this.state === SpawnGateState.Revealing) {
|
|
const progress = Math.min(1, this.elapsed / 0.15);
|
|
this.setBatchOpacity(progress);
|
|
if (progress >= 1) this.enterState(SpawnGateState.Closing);
|
|
}
|
|
else if (this.state === SpawnGateState.Closing) {
|
|
const progress = Math.min(1, this.elapsed / 0.2);
|
|
this.setClosedAmount(progress);
|
|
if (progress >= 1) this.completeBatch();
|
|
}
|
|
}
|
|
|
|
private beginBatch() {
|
|
const queueItem = this.config.spawnQueue[this.queueIndex];
|
|
if (this.countLabel) this.countLabel.node.active = false;
|
|
this.batch = this.map.createSpawnGateBatch(
|
|
queueItem.members,
|
|
this.config,
|
|
this.queueIndex,
|
|
);
|
|
this.queueIndex++;
|
|
this.setBatchOpacity(0);
|
|
this.enterState(SpawnGateState.Opening);
|
|
}
|
|
|
|
private completeBatch() {
|
|
this.setClosedAmount(1);
|
|
this.setBatchOpacity(1);
|
|
this.map.unlockSpawnGateBatch(this.batch.nodes);
|
|
this.batch = null;
|
|
if (this.queueIndex >= this.config.spawnQueue.length) {
|
|
this.finishGate();
|
|
}
|
|
else {
|
|
if (this.countLabel) this.countLabel.node.active = true;
|
|
this.enterState(SpawnGateState.Waiting);
|
|
}
|
|
}
|
|
|
|
private finishGate() {
|
|
this.finished = true;
|
|
const map = this.map;
|
|
this.map = null;
|
|
this.node.destroy();
|
|
if (map && cc.isValid(map, true)) map.onSpawnGateFinished();
|
|
}
|
|
|
|
private enterState(state: SpawnGateState) {
|
|
this.state = state;
|
|
this.elapsed = 0;
|
|
if (state === SpawnGateState.Revealing) {
|
|
this.updateRemainingCount();
|
|
this.playRevealSmoke();
|
|
}
|
|
}
|
|
|
|
private updateRemainingCount() {
|
|
if (!this.countLabel) return;
|
|
const remaining = Math.max(0, this.config.spawnQueue.length - this.queueIndex);
|
|
this.countLabel.string = remaining.toString();
|
|
}
|
|
|
|
private playRevealSmoke() {
|
|
if (!this.batch || !this.revealSmokeData) return;
|
|
const smoke = new cc.Node("spawnGateSmoke");
|
|
smoke.parent = this.node.parent;
|
|
smoke.zIndex = 1100;
|
|
|
|
const gateWidth = this.config.width * 120;
|
|
const gateHeight = this.config.height * 120;
|
|
const scale = Math.max(gateWidth / 430.62, gateHeight / 373.6) * 0.8;
|
|
smoke.scale = scale;
|
|
smoke.setPosition(
|
|
this.node.x - 8.78 * scale,
|
|
this.node.y + 9.49 * scale,
|
|
);
|
|
|
|
const skeleton = smoke.addComponent(sp.Skeleton);
|
|
skeleton.skeletonData = this.revealSmokeData;
|
|
skeleton.premultipliedAlpha = true;
|
|
skeleton.loop = false;
|
|
skeleton.timeScale = 1.5625;
|
|
const track = skeleton.setAnimation(0, "animation", false);
|
|
skeleton.setAttachment("锤子", null);
|
|
if (track) track.trackTime = 0.78;
|
|
skeleton.setCompleteListener(() => smoke.destroy());
|
|
}
|
|
|
|
private setClosedAmount(value: number) {
|
|
if (!this.leftDoor || !this.rightDoor) return;
|
|
const halfWidth = this.config.width * 60;
|
|
const offset = halfWidth * (1.5 - value);
|
|
this.leftDoor.setPosition(-offset, 0);
|
|
this.rightDoor.setPosition(offset, 0);
|
|
this.leftDoor.active = this.rightDoor.active = value > 0;
|
|
}
|
|
|
|
private setBatchOpacity(progress: number) {
|
|
if (!this.batch) return;
|
|
for (let i = 0; i < this.batch.nodes.length; i++) {
|
|
const node = this.batch.nodes[i];
|
|
if (node && cc.isValid(node)) {
|
|
node.opacity = Math.round(this.batch.opacities[i] * progress);
|
|
}
|
|
}
|
|
}
|
|
|
|
onDestroy() {
|
|
this.cornerFrames.forEach(frame => frame.destroy());
|
|
this.cornerFrames = [];
|
|
this.map = null;
|
|
this.batch = null;
|
|
this.countLabel = null;
|
|
this.leftDoor = this.rightDoor = null;
|
|
}
|
|
}
|