更新迭代动画效果
This commit is contained in:
parent
5d408e3434
commit
2029441aae
|
|
@ -1,4 +1,5 @@
|
|||
import { applyRainbowCut } from "./RainbowCut";
|
||||
import RainbowTrail from "./RainbowTrail";
|
||||
import { chooseRainbowTarget, rainbowPriority, RainbowEnergy, RAINBOW_CUTS, RAINBOW_CAPACITY, RAINBOW_CAST_SECONDS } from "./RainbowRules";
|
||||
|
||||
/** 每关独立持有;原地复活不重建,不占用暂停或道具的状态。 */
|
||||
|
|
@ -10,7 +11,8 @@ export default class RainbowStreak {
|
|||
private credited = new Set<cc.Node>();
|
||||
private bottle: cc.Node;
|
||||
private shield: cc.Node = null;
|
||||
private flight: cc.Node = null;
|
||||
private flight: RainbowTrail = null;
|
||||
private fadingFlights: RainbowTrail[] = [];
|
||||
private elapsed = 0;
|
||||
private selection: any = null;
|
||||
private from: cc.Vec2;
|
||||
|
|
@ -112,14 +114,28 @@ export default class RainbowStreak {
|
|||
}
|
||||
|
||||
update(dt: number): void {
|
||||
if (this.map.gameOver || this.map.gameWin) { if (this.casting) this.finish(false); return; }
|
||||
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) this.flight.setPosition(this.from.x + (this.to.x - this.from.x) * t,
|
||||
this.from.y + (this.to.y - this.from.y) * t + 100 * 4 * t * (1 - t));
|
||||
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();
|
||||
|
|
@ -146,15 +162,12 @@ export default class RainbowStreak {
|
|||
canvas.addChild(this.shield);
|
||||
this.shield.zIndex = cc.macro.MAX_ZINDEX;
|
||||
if (this.map.magics && this.bottle) {
|
||||
this.flight = cc.instantiate(this.map.magics);
|
||||
this.flight.parent = this.map.magics.parent;
|
||||
this.flight.active = true;
|
||||
this.flight.zIndex = cc.macro.MAX_ZINDEX;
|
||||
this.from = this.flight.parent.convertToNodeSpaceAR(this.bottle.convertToWorldSpaceAR(cc.v2(0, 0)));
|
||||
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 = this.flight.parent.convertToNodeSpaceAR(cell.parent.convertToWorldSpaceAR(cc.v2(cell.x, cell.y)));
|
||||
this.flight.setPosition(this.from);
|
||||
this.flight.getComponentsInChildren(cc.ParticleSystem).forEach(particle => particle.resetSystem());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +185,11 @@ export default class RainbowStreak {
|
|||
this.casting = false;
|
||||
this.selection = null;
|
||||
if (this.shield) { this.shield.active = false; this.shield.destroy(); this.shield = null; }
|
||||
if (this.flight) { this.flight.destroy(); this.flight = 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) {
|
||||
|
|
@ -194,7 +211,13 @@ export default class RainbowStreak {
|
|||
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
144
assets/Script/RainbowTrail.ts
Normal file
144
assets/Script/RainbowTrail.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// 彩虹飞行专用粒子,纹理在内存中生成,避免改动魔法棒的共享特效。
|
||||
export default class RainbowTrail {
|
||||
readonly node = new cc.Node("rainbowFlight");
|
||||
private mist: cc.ParticleSystem;
|
||||
private stars: cc.ParticleSystem;
|
||||
private head: cc.Node;
|
||||
private frames: cc.SpriteFrame[] = [];
|
||||
private textures: cc.Texture2D[] = [];
|
||||
private stopped = false;
|
||||
|
||||
constructor(parent: cc.Node, duration: number) {
|
||||
const glow = this.texture(false);
|
||||
const star = this.texture(true);
|
||||
this.mist = this.emitter("rainbowMist", glow);
|
||||
this.mist.totalParticles = 200;
|
||||
this.mist.emissionRate = 170;
|
||||
this.mist.life = Math.max(0.45, duration * 0.6);
|
||||
this.mist.lifeVar = 0.08;
|
||||
this.mist.startSize = 58;
|
||||
this.mist.startSizeVar = 10;
|
||||
this.mist.endSize = 24;
|
||||
this.mist.endSizeVar = 6;
|
||||
this.mist.posVar = cc.v2(3, 3);
|
||||
this.mist.speed = 6;
|
||||
this.mist.speedVar = 4;
|
||||
|
||||
this.stars = this.emitter("rainbowStars", star);
|
||||
this.stars.totalParticles = 30;
|
||||
this.stars.emissionRate = 26;
|
||||
this.stars.life = 0.55;
|
||||
this.stars.lifeVar = 0.1;
|
||||
this.stars.startSize = 15;
|
||||
this.stars.startSizeVar = 5;
|
||||
this.stars.endSize = 3;
|
||||
this.stars.endSizeVar = 2;
|
||||
this.stars.posVar = cc.v2(13, 13);
|
||||
this.stars.speed = 16;
|
||||
this.stars.speedVar = 8;
|
||||
this.stars.startSpinVar = 180;
|
||||
this.stars.endSpinVar = 180;
|
||||
this.stars.startColor = cc.color(255, 244, 178, 255);
|
||||
this.stars.startColorVar = cc.color(0, 10, 45, 0);
|
||||
this.stars.endColor = cc.color(255, 225, 130, 0);
|
||||
|
||||
this.head = new cc.Node("rainbowHead");
|
||||
const sprite = this.head.addComponent(cc.Sprite);
|
||||
sprite.spriteFrame = glow;
|
||||
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
|
||||
this.head.setContentSize(42, 42);
|
||||
this.head.color = cc.color(255, 252, 224);
|
||||
this.node.addChild(this.head);
|
||||
this.setProgress(0);
|
||||
parent.addChild(this.node);
|
||||
this.node.zIndex = cc.macro.MAX_ZINDEX;
|
||||
this.mist.resetSystem();
|
||||
this.stars.resetSystem();
|
||||
}
|
||||
|
||||
private emitter(name: string, frame: cc.SpriteFrame): cc.ParticleSystem {
|
||||
const node = new cc.Node(name);
|
||||
const particles = node.addComponent(cc.ParticleSystem);
|
||||
particles.custom = true;
|
||||
particles.playOnLoad = false;
|
||||
particles.autoRemoveOnFinish = false;
|
||||
particles.spriteFrame = frame;
|
||||
particles.duration = -1;
|
||||
particles.positionType = cc.ParticleSystem.PositionType.FREE;
|
||||
particles.emitterMode = cc.ParticleSystem.EmitterMode.GRAVITY;
|
||||
particles.gravity = cc.v2(0, 0);
|
||||
particles.sourcePos = cc.v2(0, 0);
|
||||
particles.angle = 0;
|
||||
particles.angleVar = 360;
|
||||
particles.radialAccel = particles.radialAccelVar = 0;
|
||||
particles.tangentialAccel = particles.tangentialAccelVar = 0;
|
||||
particles.startSpin = particles.endSpin = 0;
|
||||
particles.startSpinVar = particles.endSpinVar = 0;
|
||||
particles.startColorVar = particles.endColorVar = cc.color(0, 0, 0, 0);
|
||||
particles.srcBlendFactor = cc.macro.SRC_ALPHA;
|
||||
particles.dstBlendFactor = cc.macro.ONE_MINUS_SRC_ALPHA;
|
||||
this.node.addChild(node);
|
||||
return particles;
|
||||
}
|
||||
|
||||
setProgress(progress: number): void {
|
||||
// 只改变新发射粒子的颜色,已经发射的粒子在原位置保留颜色并渐隐。
|
||||
const palette = [[255, 72, 93], [255, 166, 57], [255, 232, 80], [77, 220, 112],
|
||||
[57, 215, 245], [85, 130, 255], [187, 99, 244], [255, 72, 93]];
|
||||
const phase = ((progress * 2) % 1) * (palette.length - 1);
|
||||
const index = Math.floor(phase);
|
||||
const fraction = phase - index;
|
||||
const rgb = palette[index].map((value, channel) => Math.round(value
|
||||
+ (palette[index + 1][channel] - value) * fraction));
|
||||
this.mist.startColor = cc.color(rgb[0], rgb[1], rgb[2], 185);
|
||||
this.mist.endColor = cc.color(rgb[0], rgb[1], rgb[2], 0);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.stopped = true;
|
||||
this.head.active = false;
|
||||
this.mist.stopSystem();
|
||||
this.stars.stopSystem();
|
||||
}
|
||||
|
||||
update(paused: boolean): boolean {
|
||||
// 禁用节点同时停止粒子模拟,恢复时不 reset,保留既有尾迹。
|
||||
this.node.active = !paused;
|
||||
if (this.stopped && !paused && this.mist.particleCount === 0 && this.stars.particleCount === 0) {
|
||||
this.destroy();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.node.active = false;
|
||||
this.node.destroy();
|
||||
this.frames.forEach(frame => frame.destroy());
|
||||
this.textures.forEach(texture => texture.destroy());
|
||||
this.frames = [];
|
||||
this.textures = [];
|
||||
}
|
||||
|
||||
private texture(star: boolean): cc.SpriteFrame {
|
||||
const size = 64;
|
||||
const pixels = new Uint8Array(size * size * 4);
|
||||
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
||||
const nx = (x + 0.5 - size / 2) / (size / 2);
|
||||
const ny = (y + 0.5 - size / 2) / (size / 2);
|
||||
const radius = Math.sqrt(nx * nx + ny * ny);
|
||||
const glow = Math.max(0, 1 - radius) * Math.exp(-3 * radius * radius);
|
||||
const rays = Math.max(0, Math.min(1, (1 - Math.sqrt(Math.abs(nx)) - Math.sqrt(Math.abs(ny))) * 16));
|
||||
const alpha = star ? Math.max(rays, glow * 0.2) : glow;
|
||||
const offset = (y * size + x) * 4;
|
||||
pixels[offset] = pixels[offset + 1] = pixels[offset + 2] = 255;
|
||||
pixels[offset + 3] = Math.round(alpha * 255);
|
||||
}
|
||||
const texture = new cc.Texture2D();
|
||||
texture.initWithData(pixels, cc.Texture2D.PixelFormat.RGBA8888, size, size);
|
||||
const frame = new cc.SpriteFrame(texture);
|
||||
this.textures.push(texture);
|
||||
this.frames.push(frame);
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
10
assets/Script/RainbowTrail.ts.meta
Normal file
10
assets/Script/RainbowTrail.ts.meta
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"ver": "1.1.0",
|
||||
"uuid": "ef28cfd2-02d6-442e-9e5f-9d7c004f3474",
|
||||
"importer": "typescript",
|
||||
"isPlugin": false,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
"loadPluginInEditor": false,
|
||||
"subMetas": {}
|
||||
}
|
||||
53
assets/Script/prop/RainbowScroll.ts
Normal file
53
assets/Script/prop/RainbowScroll.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
const { ccclass, property } = cc._decorator;
|
||||
|
||||
/** 两张镜像彩虹沿旋转后的纵轴循环,完全离开遮罩后接到另一张后面。 */
|
||||
@ccclass
|
||||
export default class RainbowScroll extends cc.Component {
|
||||
@property(cc.Node)
|
||||
color1: cc.Node = null;
|
||||
|
||||
@property(cc.Node)
|
||||
color2: cc.Node = null;
|
||||
|
||||
@property({ tooltip: "彩虹向左上滚动的速度(每秒像素)", min: 0 })
|
||||
speed: number = 60;
|
||||
|
||||
private colors: cc.Node[] = [];
|
||||
private direction = cc.v2(0, 0);
|
||||
private exits: number[] = [];
|
||||
private loopLength = 0;
|
||||
|
||||
onLoad(): void {
|
||||
// 预制体重存后引用可能为空,按既有节点层级恢复。
|
||||
const mask = this.node.getChildByName("mask");
|
||||
this.color1 = this.color1 || mask.getChildByName("color1");
|
||||
this.color2 = this.color2 || mask.getChildByName("color2");
|
||||
this.colors = [this.color1, this.color2];
|
||||
const radians = this.color1.angle * Math.PI / 180;
|
||||
this.direction = cc.v2(-Math.sin(radians), Math.cos(radians));
|
||||
const centers = this.colors.map(node => mask.convertToNodeSpaceAR(node.convertToWorldSpaceAR(
|
||||
cc.v2((0.5 - node.anchorX) * node.width, (0.5 - node.anchorY) * node.height))));
|
||||
const offsets = centers.map((center, i) => cc.v2(center.x - this.colors[i].x, center.y - this.colors[i].y));
|
||||
const lengths = this.colors.map(node => node.height * Math.abs(node.scaleY));
|
||||
this.loopLength = lengths[0] + lengths[1];
|
||||
|
||||
const left = -mask.width * mask.anchorX;
|
||||
const bottom = -mask.height * mask.anchorY;
|
||||
const maskEnd = Math.max(left * this.direction.x, (left + mask.width) * this.direction.x)
|
||||
+ Math.max(bottom * this.direction.y, (bottom + mask.height) * this.direction.y);
|
||||
this.exits = offsets.map((offset, i) => maskEnd + lengths[i] / 2
|
||||
- offset.x * this.direction.x - offset.y * this.direction.y);
|
||||
}
|
||||
|
||||
update(dt: number): void {
|
||||
const distance = (this.speed * dt) % this.loopLength;
|
||||
this.colors.forEach((node, i) => {
|
||||
node.x += this.direction.x * distance;
|
||||
node.y += this.direction.y * distance;
|
||||
if (node.x * this.direction.x + node.y * this.direction.y > this.exits[i]) {
|
||||
node.x -= this.direction.x * this.loopLength;
|
||||
node.y -= this.direction.y * this.loopLength;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
10
assets/Script/prop/RainbowScroll.ts.meta
Normal file
10
assets/Script/prop/RainbowScroll.ts.meta
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"ver": "1.1.0",
|
||||
"uuid": "dc21a4b5-f370-4bcc-9653-0db6305573a8",
|
||||
"importer": "typescript",
|
||||
"isPlugin": false,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
"loadPluginInEditor": false,
|
||||
"subMetas": {}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"ver": "1.1.3",
|
||||
"uuid": "b5a2138e-afd2-4ee4-93b8-c62c8e43ea71",
|
||||
"importer": "folder",
|
||||
"isBundle": false,
|
||||
"bundleName": "",
|
||||
"priority": 1,
|
||||
"compressionType": {},
|
||||
"optimizeHotUpdate": {},
|
||||
"inlineSpriteFrames": {},
|
||||
"isRemoteBundle": {},
|
||||
"subMetas": {}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"ver": "1.1.3",
|
||||
"uuid": "1339c469-1a46-4ab3-bbc9-80ff25a92373",
|
||||
"importer": "folder",
|
||||
"isBundle": false,
|
||||
"bundleName": "",
|
||||
"priority": 1,
|
||||
"compressionType": {},
|
||||
"optimizeHotUpdate": {},
|
||||
"inlineSpriteFrames": {},
|
||||
"isRemoteBundle": {},
|
||||
"subMetas": {}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"ver": "1.1.3",
|
||||
"uuid": "f3c80619-7ab4-4034-94af-2e183afdbc76",
|
||||
"importer": "folder",
|
||||
"isBundle": false,
|
||||
"bundleName": "",
|
||||
"priority": 1,
|
||||
"compressionType": {},
|
||||
"optimizeHotUpdate": {},
|
||||
"inlineSpriteFrames": {},
|
||||
"isRemoteBundle": {},
|
||||
"subMetas": {}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"ver": "1.1.3",
|
||||
"uuid": "2e586a33-44c3-470f-8458-36b0f8526927",
|
||||
"importer": "folder",
|
||||
"isBundle": false,
|
||||
"bundleName": "",
|
||||
"priority": 1,
|
||||
"compressionType": {},
|
||||
"optimizeHotUpdate": {},
|
||||
"inlineSpriteFrames": {},
|
||||
"isRemoteBundle": {},
|
||||
"subMetas": {}
|
||||
}
|
||||
|
|
@ -21,13 +21,17 @@
|
|||
"__id__": 2
|
||||
},
|
||||
{
|
||||
"__id__": 5
|
||||
"__id__": 11
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [],
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 14
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 11
|
||||
"__id__": 15
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -76,113 +80,6 @@
|
|||
"groupIndex": 0,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "bg",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 3
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 4
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 122,
|
||||
"height": 129
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 1,
|
||||
"y": 0
|
||||
},
|
||||
"_trs": {
|
||||
"__type__": "TypedArray",
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
-0.448,
|
||||
-7.608,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"_eulerAngles": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_skewX": 0,
|
||||
"_skewY": 0,
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Sprite",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
{
|
||||
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
|
||||
}
|
||||
],
|
||||
"_srcBlendFactor": 770,
|
||||
"_dstBlendFactor": 771,
|
||||
"_spriteFrame": {
|
||||
"__uuid__": "bf642998-76fc-40cb-a174-32ddf494bcdd"
|
||||
},
|
||||
"_type": 0,
|
||||
"_sizeMode": 1,
|
||||
"_fillType": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"_fillStart": 0,
|
||||
"_fillRange": 0,
|
||||
"_isTrimmedMode": true,
|
||||
"_atlas": null,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "d5/XKhIrNFkqj07F0HWvmm",
|
||||
"sync": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "mask",
|
||||
|
|
@ -191,6 +88,9 @@
|
|||
"__id__": 1
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 3
|
||||
},
|
||||
{
|
||||
"__id__": 6
|
||||
}
|
||||
|
|
@ -253,11 +153,118 @@
|
|||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "New Sprite",
|
||||
"_name": "color1",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 4
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 5
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 240,
|
||||
"height": 170
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 1,
|
||||
"y": 0
|
||||
},
|
||||
"_trs": {
|
||||
"__type__": "TypedArray",
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
19.327,
|
||||
136.339,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.3826834323650898,
|
||||
0.9238795325112867,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"_eulerAngles": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 45
|
||||
},
|
||||
"_skewX": 0,
|
||||
"_skewY": 0,
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Sprite",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 3
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
{
|
||||
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
|
||||
}
|
||||
],
|
||||
"_srcBlendFactor": 770,
|
||||
"_dstBlendFactor": 771,
|
||||
"_spriteFrame": {
|
||||
"__uuid__": "ed9c85c6-2c98-456d-b747-5da1bfe909ae"
|
||||
},
|
||||
"_type": 0,
|
||||
"_sizeMode": 1,
|
||||
"_fillType": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"_fillStart": 0,
|
||||
"_fillRange": 0,
|
||||
"_isTrimmedMode": true,
|
||||
"_atlas": null,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "d5L0nbgkpD4ZyDf+Us5amd",
|
||||
"sync": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "color2",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
|
|
@ -290,15 +297,15 @@
|
|||
"__type__": "TypedArray",
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
81.431,
|
||||
74.235,
|
||||
-149.165,
|
||||
-32.153,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.3826834323650898,
|
||||
0.9238795325112867,
|
||||
1,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
1
|
||||
]
|
||||
},
|
||||
|
|
@ -355,7 +362,7 @@
|
|||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "d5L0nbgkpD4ZyDf+Us5amd",
|
||||
"fileId": "faN8DteCxEmYpUHBBQ+W4v",
|
||||
"sync": false
|
||||
},
|
||||
{
|
||||
|
|
@ -363,7 +370,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 5
|
||||
"__id__": 2
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -391,6 +398,130 @@
|
|||
"fileId": "f1CTpBauNJgKypuXhaOEje",
|
||||
"sync": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "bg",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 12
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 13
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 122,
|
||||
"height": 129
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 1,
|
||||
"y": 0
|
||||
},
|
||||
"_trs": {
|
||||
"__type__": "TypedArray",
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
-0.448,
|
||||
-7.608,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"_eulerAngles": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_skewX": 0,
|
||||
"_skewY": 0,
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Sprite",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 11
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
{
|
||||
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
|
||||
}
|
||||
],
|
||||
"_srcBlendFactor": 770,
|
||||
"_dstBlendFactor": 771,
|
||||
"_spriteFrame": {
|
||||
"__uuid__": "bf642998-76fc-40cb-a174-32ddf494bcdd"
|
||||
},
|
||||
"_type": 0,
|
||||
"_sizeMode": 1,
|
||||
"_fillType": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"_fillStart": 0,
|
||||
"_fillRange": 0,
|
||||
"_isTrimmedMode": true,
|
||||
"_atlas": null,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "d5/XKhIrNFkqj07F0HWvmm",
|
||||
"sync": false
|
||||
},
|
||||
{
|
||||
"__type__": "dc21aS183BLzJZTDbYwVXOo",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_enabled": true,
|
||||
"color1": {
|
||||
"__id__": 3
|
||||
},
|
||||
"color2": {
|
||||
"__id__": 6
|
||||
},
|
||||
"speed": 60,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ class Node {
|
|||
get position() { return v2(this.x, this.y); }
|
||||
setPosition(x, y) { this.x = typeof x === 'object' ? x.x : x; this.y = typeof x === 'object' ? x.y : y; }
|
||||
getContentSize() { return { width: this.width, height: this.height }; }
|
||||
setContentSize(size) { Object.assign(this, size); }
|
||||
setContentSize(size, height) { Object.assign(this, typeof size === 'number' ? { width: size, height } : size); }
|
||||
getAnchorPoint() { return v2(this.anchorX, this.anchorY); }
|
||||
setAnchorPoint(p) { this.anchorX = p.x; this.anchorY = p.y; }
|
||||
getChildByName(name) { return this.children.find(n => n.name === name); }
|
||||
|
|
@ -153,10 +153,28 @@ class Node {
|
|||
convertToWorldSpaceAR(p) { return p.clone(); }
|
||||
}
|
||||
class Sprite { constructor() { this.fillRange = 1; } }
|
||||
Sprite.SizeMode = { CUSTOM: 0 };
|
||||
class Texture2D {
|
||||
initWithData(pixels, format, width, height) { Object.assign(this, { pixels, format, width, height }); }
|
||||
destroy() { this.destroyed = true; }
|
||||
}
|
||||
Texture2D.PixelFormat = { RGBA8888: 6 };
|
||||
class SpriteFrame {
|
||||
constructor(texture) { this.texture = texture; }
|
||||
destroy() { this.destroyed = true; }
|
||||
}
|
||||
class ParticleSystem {
|
||||
constructor() { this.resets = 0; this.particleCount = 0; }
|
||||
resetSystem() { this.resets++; this.active = true; }
|
||||
stopSystem() { this.active = false; }
|
||||
}
|
||||
ParticleSystem.PositionType = { FREE: 0 };
|
||||
ParticleSystem.EmitterMode = { GRAVITY: 0 };
|
||||
class PolygonCollider {}
|
||||
const engine = { Node, Sprite, PolygonCollider, BlockInputEvents: class BlockInputEvents {}, v2, color: (r, g, b) => ({r,g,b}),
|
||||
Color: { WHITE: {} }, macro: { MAX_ZINDEX: 99999 }, isValid: node => !!node && !node.destroyed,
|
||||
const engine = { Node, Sprite, Texture2D, SpriteFrame, ParticleSystem, PolygonCollider, BlockInputEvents: class BlockInputEvents {}, v2, color: (r, g, b, a = 255) => ({r,g,b,a}),
|
||||
Color: { WHITE: {} }, macro: { MAX_ZINDEX: 99999, SRC_ALPHA: 770, ONE_MINUS_SRC_ALPHA: 771 }, isValid: node => !!node && !node.destroyed,
|
||||
game: { on() {}, off() {}, EVENT_HIDE: 'hide', EVENT_SHOW: 'show' }, find: () => new Node('Canvas') };
|
||||
const trailModule = load('assets/Script/RainbowTrail.ts', engine);
|
||||
function block(shape = 1, type = 0, info = {}) {
|
||||
const node = new Node('block' + shape);
|
||||
const b = { node, block_Info: { block: shape, type, color: 2, ...info }, type, color: 2, posX: 5, posY: 5,
|
||||
|
|
@ -169,7 +187,7 @@ function block(shape = 1, type = 0, info = {}) {
|
|||
function controllerFixture(blocks = [block(), block()]) {
|
||||
const applied = [];
|
||||
const Controller = load('assets/Script/RainbowStreak.ts', engine,
|
||||
{ './RainbowRules': rules, './RainbowCut': { applyRainbowCut: (...args) => applied.push(args) } }).default;
|
||||
{ './RainbowRules': rules, './RainbowCut': { applyRainbowCut: (...args) => applied.push(args) }, './RainbowTrail': trailModule }).default;
|
||||
const map = { blocks: blocks.map(b => b.node), gameStart: true, scheduleCallback() {}, timeNumber: 100,
|
||||
hasPendingSpawnGateWork: () => false, isPauseOpen: () => false, iceTrue: () => false, magicMask: { active: false } };
|
||||
map.node = new Node('Map'); map.node.children = map.blocks;
|
||||
|
|
@ -194,6 +212,53 @@ test('first press excludes the whole dragged group; 1.5 s flight keeps input loc
|
|||
assert.equal(first.touchDelta.mag(), 0); assert.equal(first.isTouch, true);
|
||||
});
|
||||
|
||||
test('rainbow trail pauses without restarting, fades after arrival even when bottle stops, and releases textures', () => {
|
||||
const f = controllerFixture();
|
||||
const parent = new Node('effects');
|
||||
f.map.magics = new Node('magics'); parent.addChild(f.map.magics);
|
||||
f.map.mapBlocksWall = Array.from({ length: 10 }, () => Array.from({ length: 10 }, () => {
|
||||
const cell = new Node('cell'); parent.addChild(cell); return cell;
|
||||
}));
|
||||
f.controller.onPress(f.blocks[0], { getLocation: () => v2() });
|
||||
const trail = f.controller.flight;
|
||||
const particles = trail.node.children.map(node => node.getComponent(ParticleSystem)).filter(Boolean);
|
||||
const frames = particles.map(p => p.spriteFrame);
|
||||
particles.forEach(p => p.particleCount = 8);
|
||||
f.controller.update(.4);
|
||||
const position = plain(trail.node.position);
|
||||
f.map.pause = true; f.controller.update(10);
|
||||
assert.equal(f.applied.length, 0); assert.equal(f.controller.shield.active, true);
|
||||
assert.deepEqual(plain(trail.node.position), position);
|
||||
assert.equal(trail.node.active, false);
|
||||
f.map.pause = false; f.controller.update(1.1);
|
||||
assert.equal(f.applied.length, 1); assert.equal(f.controller.shield, null);
|
||||
assert.equal(trail.node.active, true); assert.equal(trail.node.destroyed, undefined);
|
||||
assert.ok(particles.every(p => !p.active && p.resets === 1));
|
||||
assert.equal(trail.node.getChildByName('rainbowHead').active, false);
|
||||
f.controller.energy.stopped = true;
|
||||
f.controller.hide(); f.controller.update(10);
|
||||
assert.equal(trail.node.destroyed, undefined);
|
||||
f.controller.show(); f.controller.update(1);
|
||||
assert.equal(trail.node.destroyed, undefined);
|
||||
particles.forEach(p => p.particleCount = 0);
|
||||
f.controller.update(.1);
|
||||
assert.equal(trail.node.destroyed, true);
|
||||
assert.ok(frames.every(frame => frame.destroyed && frame.texture.destroyed));
|
||||
assert.equal(f.controller.fadingFlights.length, 0);
|
||||
});
|
||||
|
||||
test('scene exit disposes an in-flight rainbow effect and input shield immediately', () => {
|
||||
const f = controllerFixture();
|
||||
f.controller.onPress(f.blocks[0], { getLocation: () => v2() });
|
||||
const trail = f.controller.flight = new trailModule.default(new Node('effects'), rules.RAINBOW_CAST_SECONDS);
|
||||
const shield = f.controller.shield;
|
||||
const textures = trail.node.children.map(n => n.getComponent(ParticleSystem)).filter(Boolean).map(p => p.spriteFrame.texture);
|
||||
f.controller.dispose();
|
||||
assert.equal(f.applied.length, 0); assert.equal(shield.active, false); assert.equal(shield.destroyed, true);
|
||||
assert.equal(trail.node.destroyed, true); assert.ok(textures.every(texture => texture.destroyed));
|
||||
assert.equal(f.controller.casting, false);
|
||||
});
|
||||
|
||||
test('release during flight settles once; lost target consumes cast; only dragged group consumes without locking', () => {
|
||||
const f = controllerFixture(); const first = f.blocks[0];
|
||||
first.isTouch = true; first.node.parent = new Node(); let ends = 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user