Files
MatchMaster/assets/Script/prop/vine.ts
T
2026-08-20 12:36:50 +08:00

486 lines
17 KiB
TypeScript

// Learn TypeScript:
// - https://docs.cocos.com/creator/2.4/manual/en/scripting/typescript.html
// Learn Attribute:
// - https://docs.cocos.com/creator/2.4/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - https://docs.cocos.com/creator/2.4/manual/en/scripting/life-cycle-callbacks.html
const { ccclass, property } = cc._decorator;
@ccclass
export default class NewClass extends cc.Component {
@property(cc.Label)
label: cc.Label = null;
@property({ tooltip: "Vine shrink animation speed (3 = one third second per segment)" })
animationSpeed: number = 3;
@property(cc.SpriteFrame)
start_up: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
start_down: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
start_left: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
start_right: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
up: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
down: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
left: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
right: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
up_left: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
up_right: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
down_left: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
down_right: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
left_up: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
left_down: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
right_up: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
right_down: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
end_up: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
end_down: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
end_left: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
end_right: cc.SpriteFrame = null;
private iconName: string = "";
setIcon(iconName: string): boolean {
console.log("藤蔓圖片名字", iconName);
const iconNode = this.node.getChildByName("icon");
const iconSprite = iconNode && iconNode.getComponent(cc.Sprite);
const spriteFrame = (this as any)[iconName] as cc.SpriteFrame;
if (!iconSprite || !spriteFrame) {
cc.warn(`藤蔓图片 ${iconName} 配置不存在`);
return false;
}
console.log("藤蔓圖片", spriteFrame);
this.iconName = iconName;
iconSprite.spriteFrame = spriteFrame;
iconSprite.type = cc.Sprite.Type.FILLED;
iconSprite.fillRange = 1;
iconSprite.node.opacity = 255;
return true;
}
getShrinkStepDuration(): number {
return 1 / Math.max(this.animationSpeed, 0.01);
}
playShrinkTransition(
nextNode: cc.Node,
towardStartNode: cc.Node,
removeNext: boolean,
complete: () => void
): void {
let finished = false;
const finish = () => {
if (finished) return;
finished = true;
if (complete) complete();
};
if (!nextNode || !cc.isValid(nextNode)) {
this.playSingleShrink(finish);
return;
}
const nextVine = nextNode.getComponent(NewClass);
const parent = this.node.parent;
if (!nextVine || !parent) {
if (cc.isValid(this.node)) this.node.destroy();
if (removeNext && cc.isValid(nextNode)) nextNode.destroy();
finish();
return;
}
const duration = this.getShrinkStepDuration();
const tailPosition = this.copyPosition(this.node.position);
const nextPosition = this.copyPosition(nextNode.position);
const actualTowardStart = towardStartNode && cc.isValid(towardStartNode);
const towardStartPosition = actualTowardStart
? this.copyPosition(towardStartNode.position)
: cc.v3(
nextPosition.x + nextPosition.x - tailPosition.x,
nextPosition.y + nextPosition.y - tailPosition.y
);
const tailTowardStart = this.getDirection(tailPosition, nextPosition);
const tailTowardEnd = cc.v2(-tailTowardStart.x, -tailTowardStart.y);
const nextTowardEnd = this.getDirection(nextPosition, tailPosition);
const nextTowardStart = this.getDirection(nextPosition, towardStartPosition);
const initialGlowPosition = this.hasVisibleEnd()
? tailPosition
: cc.v3(
tailPosition.x + (tailPosition.x - nextPosition.x) / 2,
tailPosition.y + (tailPosition.y - nextPosition.y) / 2
);
const glow = this.createRetractGlow(parent, initialGlowPosition);
this.animateCurrentTail(tailTowardStart, tailTowardEnd, duration);
let tween = cc.tween(glow)
.to(duration, {
position: this.getBoundaryPosition(tailPosition, nextPosition)
}, { easing: "linear" })
.call(() => {
nextVine.animateBecomeTail(nextTowardStart, nextTowardEnd, duration);
});
if (actualTowardStart) {
if (this.isCorner(nextTowardEnd, nextTowardStart)) {
tween
.to(duration / 2, { position: nextPosition }, { easing: "linear" })
.to(duration / 2, {
position: this.getBoundaryPosition(nextPosition, towardStartPosition)
}, { easing: "linear" });
} else {
tween.to(duration, {
position: this.getBoundaryPosition(nextPosition, towardStartPosition)
}, { easing: "linear" });
}
} else {
tween.to(duration, { position: nextPosition }, { easing: "linear" });
}
if (removeNext) {
tween
.call(() => {
nextVine.animateCurrentTail(nextTowardStart, nextTowardEnd, duration);
nextVine.animateBackgroundDisappear(duration);
})
.to(duration, {
position: this.getBoundaryPosition(nextPosition, towardStartPosition)
}, { easing: "linear" });
}
tween
.call(() => {
this.destroyGlow(glow);
if (cc.isValid(this.node)) this.node.destroy();
if (removeNext && cc.isValid(nextNode)) nextNode.destroy();
finish();
})
.start();
}
private playSingleShrink(complete: () => void): void {
const duration = this.getShrinkStepDuration();
const position = this.copyPosition(this.node.position);
const towardStart = this.getSingleExitDirection();
const towardEnd = cc.v2(-towardStart.x, -towardStart.y);
const distance = Math.max(this.node.width, this.node.height, 120) / 2;
const target = cc.v3(
position.x + towardStart.x * distance,
position.y + towardStart.y * distance
);
const parent = this.node.parent;
this.animateCurrentTail(towardStart, towardEnd, duration);
this.animateBackgroundDisappear(duration);
if (!parent) {
if (cc.isValid(this.node)) this.node.destroy();
complete();
return;
}
const glow = this.createRetractGlow(parent, position);
cc.tween(glow)
.to(duration, { position: target }, { easing: "linear" })
.call(() => {
this.destroyGlow(glow);
if (cc.isValid(this.node)) this.node.destroy();
complete();
})
.start();
}
private animateBecomeTail(
towardStart: cc.Vec2,
towardEnd: cc.Vec2,
duration: number
): void {
this.animateBodyDisappear(towardStart, towardEnd, duration);
const endSprite = this.getEndSprite(true);
const spriteFrame = (this as any)[this.getEndIconName(towardStart)] as cc.SpriteFrame;
if (!endSprite || !spriteFrame) return;
endSprite.spriteFrame = spriteFrame;
endSprite.type = cc.Sprite.Type.FILLED;
const range = this.configureLinearFill(endSprite, towardStart);
endSprite.fillRange = 0;
endSprite.node.opacity = 0;
cc.Tween.stopAllByTarget(endSprite);
cc.Tween.stopAllByTarget(endSprite.node);
cc.tween(endSprite)
.delay(duration / 3)
.to(duration / 2, { fillRange: range.fullRange }, { easing: "linear" })
.start();
cc.tween(endSprite.node)
.delay(duration / 3)
.to(duration / 2, { opacity: 255 }, { easing: "linear" })
.start();
}
private animateCurrentTail(
towardStart: cc.Vec2,
towardEnd: cc.Vec2,
duration: number
): void {
const endSprite = this.getEndSprite(false);
if (endSprite && endSprite.spriteFrame && Math.abs(endSprite.fillRange) > 0.001) {
cc.Tween.stopAllByTarget(endSprite);
cc.tween(endSprite)
.to(duration, { fillRange: 0 }, { easing: "linear" })
.start();
return;
}
this.animateBodyDisappear(towardStart, towardEnd, duration);
}
private animateBodyDisappear(
towardStart: cc.Vec2,
towardEnd: cc.Vec2,
duration: number
): void {
const iconNode = this.node.getChildByName("icon");
const sprite = iconNode && iconNode.getComponent(cc.Sprite);
if (!sprite) return;
const range = this.configureBodyFill(sprite, towardStart, towardEnd);
sprite.fillRange = range.fullRange;
cc.Tween.stopAllByTarget(sprite);
cc.tween(sprite)
.to(duration, { fillRange: range.targetRange }, { easing: "linear" })
.call(() => {
// Keep a final visibility guard for pixels exactly on the radial boundary.
if (cc.isValid(iconNode)) iconNode.opacity = 0;
})
.start();
}
private animateBackgroundDisappear(duration: number): void {
const background = this.node.getChildByName("bg");
if (!background || !background.active) return;
cc.Tween.stopAllByTarget(background);
cc.tween(background)
.to(duration, { opacity: 0 }, { easing: "linear" })
.start();
}
private configureBodyFill(
sprite: cc.Sprite,
towardStart: cc.Vec2,
towardEnd: cc.Vec2
) {
const start = this.normalizeDirection(towardStart);
const end = this.normalizeDirection(towardEnd);
const cross = end.x * start.y - end.y * start.x;
sprite.type = cc.Sprite.Type.FILLED;
if (Math.abs(cross) < 0.01) {
return this.configureLinearFill(sprite, start);
}
sprite.fillType = cc.Sprite.FillType.RADIAL;
// Independent corner images are not atlas-rotated: their radial center is
// the actual corner shared by the two connected sides.
sprite.fillCenter = cc.v2(
0.5 + 0.5 * (end.x + start.x),
0.5 + 0.5 * (end.y + start.y)
);
// From the corner center, the endpoint on the end side points opposite
// to the start direction.
let fillStart = Math.atan2(-start.y, -start.x) / (Math.PI * 2);
if (fillStart < 0) fillStart += 1;
sprite.fillStart = fillStart;
const fullRange = cross > 0 ? 1 : -1;
return {
fullRange: fullRange,
targetRange: fullRange * 0.75
};
}
private configureLinearFill(sprite: cc.Sprite, direction: cc.Vec2) {
const towardStart = this.normalizeDirection(direction);
let fullRange = 1;
if (Math.abs(towardStart.x) >= Math.abs(towardStart.y)) {
sprite.fillType = cc.Sprite.FillType.HORIZONTAL;
sprite.fillStart = towardStart.x < 0 ? 0 : 1;
fullRange = towardStart.x < 0 ? 1 : -1;
} else {
sprite.fillType = cc.Sprite.FillType.VERTICAL;
sprite.fillStart = towardStart.y < 0 ? 0 : 1;
fullRange = towardStart.y < 0 ? 1 : -1;
}
return {
fullRange: fullRange,
targetRange: 0
};
}
private getEndSprite(create: boolean): cc.Sprite {
let endNode = this.node.getChildByName("end");
if (!endNode && create) {
endNode = new cc.Node("end");
this.node.addChild(endNode);
endNode.setSiblingIndex(0);
}
if (!endNode) return null;
let sprite = endNode.getComponent(cc.Sprite);
if (!sprite && create) sprite = endNode.addComponent(cc.Sprite);
if (!sprite) return null;
const icon = this.node.getChildByName("icon");
if (icon) {
endNode.width = icon.width;
endNode.height = icon.height;
}
endNode.active = true;
return sprite;
}
private hasVisibleEnd(): boolean {
const sprite = this.getEndSprite(false);
return !!sprite
&& !!sprite.spriteFrame
&& sprite.node.opacity > 0
&& Math.abs(sprite.fillRange) > 0.001;
}
private getEndIconName(direction: cc.Vec2): string {
const normalized = this.normalizeDirection(direction);
if (Math.abs(normalized.x) >= Math.abs(normalized.y)) {
return normalized.x < 0 ? "end_left" : "end_right";
}
return normalized.y < 0 ? "end_down" : "end_up";
}
private getSingleExitDirection(): cc.Vec2 {
const parts = this.iconName.split("_");
if (parts.length >= 2 && parts[0] === "start") {
const towardEnd = this.directionFromName(parts[1]);
return cc.v2(-towardEnd.x, -towardEnd.y);
}
if (parts.length >= 2 && parts[0] === "end") {
return this.directionFromName(parts[1]);
}
return cc.v2(0, -1);
}
private directionFromName(name: string): cc.Vec2 {
if (name === "left") return cc.v2(-1, 0);
if (name === "right") return cc.v2(1, 0);
if (name === "up") return cc.v2(0, 1);
return cc.v2(0, -1);
}
private normalizeDirection(direction: cc.Vec2): cc.Vec2 {
if (Math.abs(direction.x) >= Math.abs(direction.y) && Math.abs(direction.x) > 0.01) {
return cc.v2(direction.x < 0 ? -1 : 1, 0);
}
if (Math.abs(direction.y) > 0.01) {
return cc.v2(0, direction.y < 0 ? -1 : 1);
}
return cc.v2(0, -1);
}
private getDirection(from: cc.Vec3, to: cc.Vec3): cc.Vec2 {
return this.normalizeDirection(cc.v2(to.x - from.x, to.y - from.y));
}
private isCorner(first: cc.Vec2, second: cc.Vec2): boolean {
return Math.abs(first.x * second.y - first.y * second.x) > 0.01;
}
private getBoundaryPosition(from: cc.Vec3, toward: cc.Vec3): cc.Vec3 {
return cc.v3((from.x + toward.x) / 2, (from.y + toward.y) / 2);
}
private copyPosition(position: cc.Vec3): cc.Vec3 {
return cc.v3(position.x, position.y, position.z || 0);
}
private createRetractGlow(parent: cc.Node, 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);
parent.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;
}
private destroyGlow(glow: cc.Node): void {
if (!glow || !cc.isValid(glow)) return;
cc.Tween.stopAllByTarget(glow);
for (let i = 0; i < glow.children.length; i++) {
cc.Tween.stopAllByTarget(glow.children[i]);
}
glow.destroy();
}
}