diff --git a/assets/Script/RainbowStreak.ts b/assets/Script/RainbowStreak.ts index 2aed51e..c189816 100644 --- a/assets/Script/RainbowStreak.ts +++ b/assets/Script/RainbowStreak.ts @@ -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(); 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 = []; + } } diff --git a/assets/Script/RainbowTrail.ts b/assets/Script/RainbowTrail.ts new file mode 100644 index 0000000..4f94603 --- /dev/null +++ b/assets/Script/RainbowTrail.ts @@ -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; + } +} diff --git a/assets/Script/RainbowTrail.ts.meta b/assets/Script/RainbowTrail.ts.meta new file mode 100644 index 0000000..45a16cc --- /dev/null +++ b/assets/Script/RainbowTrail.ts.meta @@ -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": {} +} \ No newline at end of file diff --git a/assets/Script/prop/RainbowScroll.ts b/assets/Script/prop/RainbowScroll.ts new file mode 100644 index 0000000..9b5fb34 --- /dev/null +++ b/assets/Script/prop/RainbowScroll.ts @@ -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; + } + }); + } +} diff --git a/assets/Script/prop/RainbowScroll.ts.meta b/assets/Script/prop/RainbowScroll.ts.meta new file mode 100644 index 0000000..db26d1d --- /dev/null +++ b/assets/Script/prop/RainbowScroll.ts.meta @@ -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": {} +} \ No newline at end of file diff --git a/assets/gacha_bundle/img/cat.meta b/assets/gacha_bundle/img/cat.meta deleted file mode 100644 index 7d15560..0000000 --- a/assets/gacha_bundle/img/cat.meta +++ /dev/null @@ -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": {} -} \ No newline at end of file diff --git a/assets/libs/dn-sdk-minigame.meta b/assets/libs/dn-sdk-minigame.meta deleted file mode 100644 index 5824607..0000000 --- a/assets/libs/dn-sdk-minigame.meta +++ /dev/null @@ -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": {} -} \ No newline at end of file diff --git a/assets/passCheck/script.meta b/assets/passCheck/script.meta deleted file mode 100644 index b9a7d5c..0000000 --- a/assets/passCheck/script.meta +++ /dev/null @@ -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": {} -} \ No newline at end of file diff --git a/assets/pause/texture.meta b/assets/pause/texture.meta deleted file mode 100644 index 7680ca3..0000000 --- a/assets/pause/texture.meta +++ /dev/null @@ -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": {} -} \ No newline at end of file diff --git a/assets/prefab/prop/rainbow.prefab b/assets/prefab/prop/rainbow.prefab index 2ffbf1e..a453a58 100644 --- a/assets/prefab/prop/rainbow.prefab +++ b/assets/prefab/prop/rainbow.prefab @@ -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": { diff --git a/tools/test-rainbow-streak.cjs b/tools/test-rainbow-streak.cjs index 804b2dc..42a3eeb 100644 --- a/tools/test-rainbow-streak.cjs +++ b/tools/test-rainbow-streak.cjs @@ -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;