cb/assets/Script/Test.ts
COMPUTER\EDY c9045e3211 更新
2026-08-12 15:46:44 +08:00

327 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { ccclass, property } = cc._decorator;
@ccclass
export default class Test extends cc.Component {
@property({ tooltip: "整体动画倍速1为原速2为两倍速" })
animationSpeed: number = 3;
@property({ tooltip: "每段完整动画结束后的观察间隔(秒)" })
nextStepDelay: number = 1.5;
private slotPositions: cc.Vec3[] = [];
private path: cc.Node[] = [];
private retractGlow: cc.Node = null;
start(): void {
// Test 下的藤蔓节点按 Hierarchy 顺序组成从起点到终点的路径。
this.path = this.node.children.filter(node => node.active && !!this.getBodySprite(node));
if (this.path.length < 2) {
cc.warn("Test 节点下至少需要两个有效藤蔓节点");
return;
}
const lastIndex = this.path.length - 1;
if (this.path[0].name.indexOf("end_") === 0
&& this.path[lastIndex].name.indexOf("start_") === 0) {
this.path.reverse();
}
this.slotPositions = this.path.map(node => node.position.clone());
if (!this.validatePath() || !this.validateSprites()) return;
for (let index = 0; index < lastIndex; index++) {
const sprite = this.getFirstChildSprite(this.path[index]);
if (sprite) {
sprite.fillRange = 0;
sprite.node.opacity = 0;
}
}
const tailPosition = this.slotPositions[lastIndex];
const previousPosition = this.slotPositions[lastIndex - 1];
const glowStart = cc.v3(
tailPosition.x + (tailPosition.x - previousPosition.x) / 2,
tailPosition.y + (tailPosition.y - previousPosition.y) / 2
);
this.retractGlow = this.createRetractGlow(glowStart);
this.playRound(lastIndex);
}
private playRound(tailIndex: number): void {
const tail = this.path[tailIndex];
const lastIndex = this.path.length - 1;
const duration = this.getStepDuration();
if (tailIndex < lastIndex) {
this.retractGlow.setPosition(this.slotPositions[tailIndex]);
}
this.retractGlow.active = true;
this.moveRetractGlowThroughRound(tailIndex);
if (tailIndex === lastIndex) {
this.animateFillRange(tail);
} else {
this.animateFirstChildFillRange(tail);
}
cc.tween(this.node)
.delay(duration)
.call(() => this.playNextSegment(tailIndex - 1))
.start();
}
private playNextSegment(nextIndex: number): void {
if (nextIndex >= 0) {
const nextSegment = this.path[nextIndex];
const duration = this.getStepDuration();
this.animateFirstChildAppear(nextSegment);
this.animateFillRange(nextSegment);
cc.tween(this.node)
.delay(duration)
.call(() => this.finishRound(nextIndex))
.start();
return;
}
this.retractGlow.active = false;
}
private finishRound(nextTailIndex: number): void {
this.retractGlow.active = false;
this.scheduleOnce(() => this.playRound(nextTailIndex), this.nextStepDelay);
}
private animateFillRange(segment: cc.Node): void {
const sprite = this.getBodySprite(segment);
if (!sprite) {
cc.warn(`藤蔓 ${segment.name} 缺少用于消失动画的 Sprite 子节点`);
return;
}
const duration = this.getStepDuration();
const fullRange = sprite.fillRange < 0 ? -1 : 1;
const targetRange = sprite.fillType === cc.Sprite.FillType.RADIAL
? fullRange * 0.8
: 0;
sprite.fillRange = fullRange;
cc.tween(sprite)
.to(duration, { fillRange: targetRange }, { easing: "linear" })
.start();
}
private animateFirstChildAppear(segment: cc.Node): void {
const sprite = this.getFirstChildSprite(segment);
if (!sprite) return;
const duration = this.getStepDuration();
sprite.fillRange = 0;
sprite.node.opacity = 0;
cc.tween(sprite)
.delay(duration / 3)
.to(duration / 2, { fillRange: 1 }, { easing: "linear" })
.start();
cc.tween(sprite.node)
.delay(duration / 3)
.to(duration / 2, { opacity: 255 }, { easing: "linear" })
.start();
}
private animateFirstChildFillRange(segment: cc.Node): void {
const sprite = this.getFirstChildSprite(segment);
if (!sprite) return;
const duration = this.getStepDuration();
cc.tween(sprite)
.to(duration, { fillRange: 0 }, { easing: "linear" })
.start();
}
private getFirstChildSprite(segment: cc.Node): cc.Sprite {
const firstChild = segment.children[0];
const sprite = firstChild && firstChild.getComponent(cc.Sprite);
if (!sprite) {
cc.warn(`藤蔓 ${segment.name} 缺少第一个 Sprite 子节点`);
}
return sprite;
}
private getBodyNode(segment: cc.Node): cc.Node {
return segment.children[2] || segment.children[1];
}
private getBodySprite(segment: cc.Node): cc.Sprite {
const bodyNode = this.getBodyNode(segment);
return bodyNode && bodyNode.getComponent(cc.Sprite);
}
private getStepDuration(): number {
return 1 / Math.max(this.animationSpeed, 0.01);
}
private validatePath(): boolean {
for (let index = 1; index < this.slotPositions.length; index++) {
const previous = this.slotPositions[index - 1];
const current = this.slotPositions[index];
const deltaX = Math.abs(current.x - previous.x);
const deltaY = Math.abs(current.y - previous.y);
if ((deltaX < 0.01 && deltaY < 0.01)
|| (deltaX >= 0.01 && deltaY >= 0.01)) {
cc.warn(`藤蔓路径 ${this.path[index - 1].name} -> ${this.path[index].name} 不是有效的上下左右相邻关系`);
return false;
}
}
return true;
}
private validateSprites(): boolean {
const lastIndex = this.path.length - 1;
for (let index = 0; index <= lastIndex; index++) {
const segment = this.path[index];
const bodySprite = this.getBodySprite(segment);
if (!bodySprite || bodySprite.type !== cc.Sprite.Type.FILLED) {
cc.warn(`藤蔓 ${segment.name} 的主体 Sprite 必须使用 FILLED 类型`);
return false;
}
if (this.isCorner(index) && bodySprite.fillType !== cc.Sprite.FillType.RADIAL) {
cc.warn(`拐角藤蔓 ${segment.name} 的主体 Sprite 必须使用 RADIAL 填充`);
return false;
}
if (index < lastIndex) {
const firstSprite = this.getFirstChildSprite(segment);
if (!firstSprite || firstSprite.type !== cc.Sprite.Type.FILLED) {
cc.warn(`藤蔓 ${segment.name} 的第一个 Sprite 子节点必须使用 FILLED 类型`);
return false;
}
}
}
return true;
}
private getTowardStartPosition(index: number): cc.Vec3 {
if (index > 0) return this.slotPositions[index - 1];
const start = this.slotPositions[0];
const next = this.slotPositions[1];
return cc.v3(start.x + start.x - next.x, start.y + start.y - next.y);
}
private isCorner(index: number): boolean {
if (index <= 0 || index >= this.slotPositions.length - 1) return false;
const current = this.slotPositions[index];
const towardEnd = this.slotPositions[index + 1];
const towardStart = this.slotPositions[index - 1];
const endX = towardEnd.x - current.x;
const endY = towardEnd.y - current.y;
const startX = towardStart.x - current.x;
const startY = towardStart.y - current.y;
return Math.abs(endX * startY - endY * startX) > 0.01;
}
private moveRetractGlowThroughRound(tailIndex: number): void {
const tween = cc.tween(this.retractGlow);
const lastMovementIndex = Math.max(0, tailIndex - 1);
const duration = this.getStepDuration();
for (let index = tailIndex; index >= lastMovementIndex; index--) {
const segment = this.path[index];
const from = this.slotPositions[index];
const toward = this.getTowardStartPosition(index);
const isCorner = this.isCorner(index);
const fillNode = this.getBodyNode(segment);
const stopsAtCenter = index !== tailIndex
&& fillNode
&& fillNode.name.indexOf("start_") === 0;
if (stopsAtCenter) {
tween.to(duration, { position: from }, { easing: "linear" });
} else if (isCorner && index !== tailIndex) {
tween
.to(duration / 2, { position: from }, { easing: "linear" })
.to(duration / 2, {
position: this.getBoundaryPosition(from, toward)
}, { easing: "linear" });
} else {
tween.to(duration, {
position: this.getBoundaryPosition(from, toward)
}, { easing: "linear" });
}
}
tween.start();
}
private getBoundaryPosition(from: cc.Vec3, toward: cc.Vec3): cc.Vec3 {
return cc.v3((from.x + toward.x) / 2, (from.y + toward.y) / 2);
}
private createRetractGlow(position: cc.Vec3): cc.Node {
const root = new cc.Node("retract_glow");
const pulse = new cc.Node("pulse");
const graphics = pulse.addComponent(cc.Graphics);
const orbit = new cc.Node("orbit");
graphics.fillColor = cc.color(145, 255, 45, 45);
graphics.circle(0, 0, 30);
graphics.fill();
graphics.fillColor = cc.color(190, 255, 70, 95);
graphics.circle(0, 0, 19);
graphics.fill();
graphics.fillColor = cc.color(245, 255, 170, 240);
graphics.circle(0, 0, 7);
graphics.fill();
for (let index = 0; index < 4; index++) {
const spark = new cc.Node("spark");
const sparkGraphics = spark.addComponent(cc.Graphics);
const angle = Math.PI * index / 2;
spark.setPosition(Math.cos(angle) * 24, Math.sin(angle) * 24);
sparkGraphics.fillColor = cc.color(220, 255, 95, 210);
sparkGraphics.circle(0, 0, 3);
sparkGraphics.fill();
orbit.addChild(spark);
}
root.setPosition(position);
root.zIndex = 2000;
root.addChild(pulse);
root.addChild(orbit);
this.node.addChild(root);
cc.tween(pulse)
.repeatForever(
cc.tween()
.to(0.4, { scale: 1.18, opacity: 150 }, { easing: "sineInOut" })
.to(0.4, { scale: 0.9, opacity: 255 }, { easing: "sineInOut" })
)
.start();
cc.tween(orbit)
.by(1.2, { angle: 360 })
.repeatForever()
.start();
return root;
}
onDestroy(): void {
this.unscheduleAllCallbacks();
cc.Tween.stopAllByTarget(this.node);
if (this.retractGlow && cc.isValid(this.retractGlow)) {
cc.Tween.stopAllByTarget(this.retractGlow);
}
}
}