486 lines
17 KiB
TypeScript
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();
|
|
}
|
|
}
|