MatchMaster/assets/Script/RainbowStreak.ts
WIN-GKKD951VVMJ\Administrator 2029441aae 更新迭代动画效果
2026-09-15 18:31:24 +08:00

224 lines
10 KiB
TypeScript

import { applyRainbowCut } from "./RainbowCut";
import RainbowTrail from "./RainbowTrail";
import { chooseRainbowTarget, rainbowPriority, RainbowEnergy, RAINBOW_CUTS, RAINBOW_CAPACITY, RAINBOW_CAST_SECONDS } from "./RainbowRules";
/** 每关独立持有;原地复活不重建,不占用暂停或道具的状态。 */
export default class RainbowStreak {
readonly energy = new RainbowEnergy();
casting = false;
private first = true;
private hidden = false;
private credited = new Set<cc.Node>();
private bottle: cc.Node;
private shield: cc.Node = null;
private flight: RainbowTrail = null;
private fadingFlights: RainbowTrail[] = [];
private elapsed = 0;
private selection: any = null;
private from: cc.Vec2;
private to: cc.Vec2;
private finger: any = null;
private location: cc.Vec2 = null;
private released = false;
private hide = () => { this.hidden = true; };
private show = () => { this.hidden = false; };
constructor(private map: any, bottle: cc.Node) {
this.bottle = bottle;
if (bottle) bottle.active = true;
cc.game.on(cc.game.EVENT_HIDE, this.hide);
cc.game.on(cc.game.EVENT_SHOW, this.show);
this.refreshAvailability();
this.render();
}
private candidates(): any[] {
return this.map.blocks.filter(node => cc.isValid(node, true)).map(node => node.getComponent("Block"))
.filter(block => block && !block.spawnLocked && !block.over && !block.rainbowEliminating
&& block.color !== 11 && block.type !== 11
&& block.type !== 22 && block.type !== 10 && block.allBlocks.length > 1)
.map(block => {
const upper = block.type === 1 && block.block_Info.node && block.block_Info.node.getComponent("Block");
return { block, cuts: RAINBOW_CUTS[block.block_Info.block],
priority: Math.max(rainbowPriority(block), upper ? rainbowPriority(upper) : 0) };
}).filter(candidate => candidate.cuts.length > 0);
}
private refreshAvailability(): any[] {
const candidates = this.candidates();
// 正在消除或露出上层的块仍可能改变可选池,等当前过程结束再永久停用。
const pending = this.map.hasPendingSpawnGateWork() || this.map.blocks.some(node => {
const block = cc.isValid(node, true) && node.getComponent("Block");
return block && (block.spawnLocked || block.over || block.rainbowEliminating
|| (block.type === 10 && block.allBlocks.length > 1));
});
this.energy.setAvailability(candidates.length > 0, pending);
return candidates;
}
private counting(): boolean {
return !this.hidden && this.map.gameStart && this.map.scheduleCallback != null && !this.map.pause
&& !this.map.gameOver && !this.map.gameWin && !this.map.shopPause && !this.map.isPauseOpen()
&& !this.map.iceTrue() && this.map.timeNumber > 0;
}
private settling(): boolean {
return this.map.node.children.some(node => {
const block = cc.isValid(node, true) && node.active && node.getComponent("Block");
return block && (block.over || block.rainbowEliminating
|| node.getNumberOfRunningActions() > 0);
});
}
onPress(block: any, event: any): void {
if (!this.first || !this.counting()) return;
this.first = false;
const excluded = new Set<any>([block.node]);
// 整组都跟随本次手势,不能裁到正在按住的联动成员。
this.addLinked(block.node, excluded);
const candidates = this.refreshAvailability().filter(candidate => !excluded.has(candidate.block.node));
if (!this.energy.consume()) return;
this.finger = block;
this.location = event.getLocation().clone();
this.released = false;
this.begin(candidates);
if (!this.casting) this.finger = null;
this.render();
}
private addLinked(node: cc.Node, excluded: Set<any>): void {
const block = node.getComponent("Block");
const linked = (block.teamBlocks || []).slice();
if ((block.type === 9 || block.type === 1 || block.type === 10) && block.block_Info.node) linked.push(block.block_Info.node);
linked.forEach(member => {
if (cc.isValid(member, true) && !excluded.has(member)) {
excluded.add(member);
this.addLinked(member, excluded);
}
});
}
capture(block: any, event: any, released: boolean): void {
if (this.finger !== block) return;
this.location = event.getLocation().clone();
this.released = this.released || released;
block.touchDelta = cc.v2(0, 0);
}
eliminated(node: cc.Node): void {
if (this.credited.has(node)) return;
this.credited.add(node);
const block = node.getComponent("Block");
if (block) this.energy.eliminated(block.allBlocks.length);
this.render();
}
update(dt: number): void {
if (this.map.gameOver || this.map.gameWin) {
if (this.casting) this.finish(false);
this.clearTrails();
return;
}
const paused = !this.counting();
if (this.flight) this.flight.update(paused);
this.fadingFlights = this.fadingFlights.filter(trail => trail.update(paused));
if (this.hidden) return;
if (this.energy.stopped && !this.casting) return;
if (this.casting && this.counting()) {
this.elapsed += dt;
const t = Math.min(1, this.elapsed / RAINBOW_CAST_SECONDS);
if (this.flight) {
// 先抛到目标上方,再竖直落入目标格;弧顶比两端高 180。
const top = Math.max(this.from.y, this.to.y) + 180;
const controlY = top + Math.sqrt((top - this.from.y) * (top - this.to.y));
const u = 1 - t;
this.flight.node.setPosition(this.to.x + (this.from.x - this.to.x) * u * u,
u * u * this.from.y + 2 * u * t * controlY + t * t * this.to.y);
this.flight.setProgress(t);
}
if (t >= 1) this.finish(true);
}
const candidates = this.refreshAvailability();
this.energy.tick(dt, !this.first && this.counting());
if (!this.first && !this.casting && this.counting() && !this.map.touchIng
&& !this.map.ishammer && !this.map.ismagic && !this.map.magicMask.active
&& !this.map.blocks.some(node => cc.isValid(node, true) && node.getComponent("Block").isTouch)
&& !this.settling()
&& this.energy.consume()) this.begin(candidates);
this.render();
}
private begin(candidates: any[]): void {
const choice = chooseRainbowTarget(candidates);
if (!choice) return;
const block = choice.target.block;
this.selection = { block, cut: choice.cut, shape: block.block_Info.block, x: block.posX, y: block.posY };
this.casting = true;
this.elapsed = 0;
const canvas = cc.find("Canvas");
this.shield = new cc.Node("rainbowIntercept");
this.shield.setContentSize(canvas.getContentSize());
this.shield.addComponent(cc.BlockInputEvents);
canvas.addChild(this.shield);
this.shield.zIndex = cc.macro.MAX_ZINDEX;
if (this.map.magics && this.bottle) {
const parent = this.map.magics.parent;
this.from = parent.convertToNodeSpaceAR(this.bottle.convertToWorldSpaceAR(cc.v2(0, 0)));
const cell = this.map.mapBlocksWall[block.posX + choice.cut.cell.x][block.posY + choice.cut.cell.y];
this.to = parent.convertToNodeSpaceAR(cell.parent.convertToWorldSpaceAR(cc.v2(cell.x, cell.y)));
this.flight = new RainbowTrail(parent, RAINBOW_CAST_SECONDS);
this.flight.node.setPosition(this.from);
}
}
private finish(apply: boolean): void {
const target = this.selection;
try {
if (apply && target && cc.isValid(target.block.node, true)
&& this.map.blocks.indexOf(target.block.node) >= 0 && !target.block.over
&& !target.block.rainbowEliminating
&& target.block.block_Info.block === target.shape
&& target.block.posX === target.x && target.block.posY === target.y) {
applyRainbowCut(this.map, target.block, target.cut);
}
} finally {
this.casting = false;
this.selection = null;
if (this.shield) { this.shield.active = false; this.shield.destroy(); this.shield = null; }
if (this.flight) {
if (apply) { this.flight.stop(); this.fadingFlights.push(this.flight); }
else this.flight.destroy();
this.flight = null;
}
const block = this.finger;
this.finger = null;
if (apply && block && cc.isValid(block.node, true) && block.isTouch) {
const point = this.location.clone();
const local = block.node.parent.convertToNodeSpaceAR(point);
block.relative_Position = cc.v2(block.node.x - local.x, block.node.y - local.y);
block.touchDelta = cc.v2(0, 0);
if (this.released) block.touchEnd({ getLocation: () => point });
}
}
}
private render(): void {
if (!this.bottle || !cc.isValid(this.bottle, true)) return;
const disabled = this.energy.stopped || this.energy.unavailable;
this.bottle.color = disabled ? cc.color(110, 110, 110) : cc.Color.WHITE;
this.bottle.getComponent(cc.Sprite).fillRange = disabled ? 1 : this.energy.value / RAINBOW_CAPACITY;
}
dispose(): void {
this.finish(false);
this.clearTrails();
cc.game.off(cc.game.EVENT_HIDE, this.hide);
cc.game.off(cc.game.EVENT_SHOW, this.show);
}
private clearTrails(): void {
this.fadingFlights.forEach(trail => trail.destroy());
this.fadingFlights = [];
}
}