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

486 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

// 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("藤蔓圖片å<E280A1><C3A5>å­—", 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} é…<C3A9>ç½®ä¸<C3A4>存在`);
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();
}
}