MatchMaster/assets/Script/RainbowStreak.ts
WIN-GKKD951VVMJ\Administrator c18d47f4b0 更新配置
2026-09-16 14:55:47 +08:00

248 lines
12 KiB
TypeScript

import { applyRainbowCut } from "./RainbowCut";
import RainbowTrail from "./RainbowTrail";
import RainbowBottleWater from "./RainbowBottleWater";
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 flight: RainbowTrail = null;
private fadingFlights: RainbowTrail[] = [];
private elapsed = 0;
private selection: any = null;
private from: cc.Vec2;
private to: cc.Vec2;
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;
if (!bottle.getComponent(RainbowBottleWater)) bottle.addComponent(RainbowBottleWater);
}
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
&& !this.isButtonColor(block))
.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 isButtonColor(block: any): boolean {
const upper = block.type === 1 && block.block_Info.node && block.block_Info.node.getComponent("Block");
return (this.map.buttonBlock || []).some(node => {
const button = cc.isValid(node, true) && node.getComponent("MapBlock");
return button && button.color > 0
&& (button.color === block.color || (upper && button.color === upper.color));
});
}
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.allBlocks.length > 1 && (block.type === 10 || this.isButtonColor(block))));
});
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);
});
}
private linkedNodes(block: any): cc.Node[] {
const nodes = (block.teamBlocks || []).slice();
if ((block.type === 9 || block.type === 1 || block.type === 10) && block.block_Info.node) {
nodes.push(block.block_Info.node);
}
return nodes.filter(node => cc.isValid(node, true) && node.getComponent("Block"));
}
private linkedGroup(block: any): any[] {
if (!cc.isValid(block.node, true)) return [];
const members = this.map.node.children.filter(node => cc.isValid(node, true))
.map(node => node.getComponent("Block")).filter(Boolean)
.map(member => ({ block: member, links: this.linkedNodes(member) }));
const group = [block];
for (let i = 0; i < group.length; i++) {
const links = this.linkedNodes(group[i]);
members.forEach(member => {
// 地板先解除的成员可能只剩队友对它的反向引用,也属于同一联动组。
if (group.indexOf(member.block) < 0 && (links.indexOf(member.block.node) >= 0
|| member.links.indexOf(group[i].node) >= 0)) group.push(member.block);
});
}
return group;
}
private groupKey(group: any[]): string {
return group.map(block => block.node.uuid + ":" + this.linkedNodes(block).map(node => node.uuid).sort().join(",")
+ ":" + (block.block_Info.floorMove === true)).sort().join("|");
}
private groupBusy(group: any[]): boolean {
return group.some(block => block.isTouch || block.over || block.rainbowEliminating || block.spawnLocked
|| block.node.getNumberOfRunningActions() > 0);
}
private checkSelection(): void {
if (!this.selection || this.selection.missed) return;
const group = this.linkedGroup(this.selection.block);
if (this.groupKey(group) !== this.selection.groupKey || this.groupBusy(group)) this.selection.missed = true;
}
onPress(block: any): void {
// 先记录本轮操作,不能被“非首次点击”的提前返回跳过;松手后也不恢复本轮。
if (this.selection && (this.selection.group.indexOf(block) >= 0
|| this.linkedGroup(this.selection.block).indexOf(block) >= 0)) this.selection.missed = true;
if (!this.first || !this.counting()) return;
this.first = false;
// 首次点击立即施放,避开当前按住的整个联动组;后续仍在 update 中等松手。
const excluded = this.linkedGroup(block);
const candidates = this.refreshAvailability().filter(candidate => excluded.indexOf(candidate.block) < 0);
if (this.energy.consume()) this.begin(candidates);
this.render();
}
eliminated(node: cc.Node): void {
if (this.credited.has(node)) return;
this.credited.add(node);
const block = node.getComponent("Block");
if (block && !this.map.gameOver && !this.map.gameWin) 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();
this.render();
return;
}
this.checkSelection();
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.node.children.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;
const group = this.linkedGroup(block);
this.selection = { block, cut: choice.cut, shape: block.block_Info.block, x: block.posX, y: block.posY,
nodeX: block.node.x, nodeY: block.node.y, group, groupKey: this.groupKey(group), missed: this.groupBusy(group) };
this.casting = true;
this.elapsed = 0;
// 只记录目标格子的原位置;飞行期间目标和联动成员都照常接受操作。
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 {
this.checkSelection();
const target = this.selection;
try {
if (apply && target && !target.missed && cc.isValid(target.block.node, true)
&& this.map.blocks.indexOf(target.block.node) >= 0 && !target.block.over
&& !target.block.rainbowEliminating
&& !this.isButtonColor(target.block)
&& target.block.block_Info.block === target.shape
&& target.block.allBlocks.some(cell => cell.x === target.cut.cell.x && cell.y === target.cut.cell.y)
&& target.block.posX === target.x && target.block.posY === target.y
// 原节点、原形状、原局部格和实际原点必须一致,不能用移到落点上的另一格代替。
&& Math.abs(target.block.node.x - target.nodeX) < 0.01
&& Math.abs(target.block.node.y - target.nodeY) < 0.01) {
applyRainbowCut(this.map, target.block, target.cut);
}
} finally {
this.casting = false;
this.selection = null;
if (this.flight) {
if (apply) { this.flight.stop(); this.fadingFlights.push(this.flight); }
else this.flight.destroy();
this.flight = null;
}
}
}
private render(): void {
if (!this.bottle || !cc.isValid(this.bottle, true)) return;
const disabled = this.map.gameOver || this.map.gameWin || 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 = [];
}
}