224 lines
10 KiB
TypeScript
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 = [];
|
|
}
|
|
}
|