import { _decorator, Component, Label, Tween, tween, UITransform, Vec3, randomRange } from 'cc'; const { ccclass } = _decorator; /** * 伤害飘字:随机左右 → 解析抛物线(升→落,速度连续)+ 显隐/缩放。 */ @ccclass('HurtScore') export class HurtScore extends Component { scoreLabel: Label = null; /** 轨迹时间代理(与节点 tween 分开,销毁时一并停掉) */ private _pathMotion: { t: number } | null = null; protected onDisable(): void { this.unscheduleAllCallbacks(); Tween.stopAllByTarget(this.node); if (this._pathMotion) { Tween.stopAllByTarget(this._pathMotion); this._pathMotion = null; } } protected onLoad(): void { this.scoreLabel = this.node.getComponent(Label); } /** 显示伤害值 */ showHurtNum( hurtNum: number, isCrit: boolean, bRateAtk: boolean, opts?: { hideBaoBadge?: boolean }, ) { this.unscheduleAllCallbacks(); Tween.stopAllByTarget(this.node); if (this._pathMotion) { Tween.stopAllByTarget(this._pathMotion); this._pathMotion = null; } if (!this.scoreLabel?.isValid) { return; } let numStr = ''; if (hurtNum >= 1000000) { numStr = `${Math.floor(hurtNum / 100000) / 10}M`; } else if (hurtNum >= 1000) { numStr = `${Math.floor(hurtNum / 100) / 10}K`; } else { numStr = `${Math.floor(hurtNum)}`; } this.scoreLabel.string = `-${numStr}`; const baoNode = this.node.getChildByName('暴'); const showBao = !opts?.hideBaoBadge && isCrit && !bRateAtk; if (baoNode) { baoNode.active = showBao; } if (bRateAtk) { this.scheduleOnce(() => { const width = this.scoreLabel.node.getComponent(UITransform).width; const di = this.node.getChildByName('底'); if (di) { di.getComponent(UITransform).width = width; } }); } else if (showBao) { this.scheduleOnce(() => { const width = this.scoreLabel.node.getComponent(UITransform).width; if (baoNode?.isValid) { baoNode.position = new Vec3(width / 2 + 2, 32, 0); } }); } this._playTween7(() => { if (this.node?.isValid) { gg.res.putNode(this.node); } }); } /** * 解析抛物线插值(非贝塞尔):y = y0 + vy·t + ½g·t²,x 匀速侧移。 * 顶点 vy 只瞬间为 0,无两段缓动接缝,也无控制点二次上拱。 * 数值对齐原效果:升 +10、落 100~130、侧移 ±20。 */ private _playTween7(onComplete: () => void): void { const root = this.node; const isLeft = Math.random() < 0.5; const start = root.position.clone(); const baseSx = root.scale.x; const baseSy = root.scale.y; root.opacity = 1; root.setScale(baseSx * 0.8, baseSy * 0.8, 1); root.setPosition(start); const peakH = 10; const side = isLeft ? -1 : 1; const fallDist = randomRange(100, 130); // tPeak 决定弧形节奏;speed 只压缩播放时间(轨迹不变) const tPeak = 0.35; const speed = 1.6; // 加快 60% const baseDuration = tPeak * (1 + Math.sqrt(fallDist / peakH)); const duration = baseDuration / speed; const tPeakPlay = tPeak / speed; const g = -2 * peakH / (tPeakPlay * tPeakPlay); const vy = -g * tPeakPlay; const endX = start.x + side * 20; const vx = (endX - start.x) / duration; const fadeIn = Math.min(0.25 / speed, duration * 0.4); const motion = { t: 0 }; this._pathMotion = motion; tween(root) .parallel( tween(root) .to(fadeIn, { opacity: 255, scale: new Vec3(baseSx, baseSy, 1), }, { easing: 'quadOut' }) .to(Math.max(0.01, duration - fadeIn), { opacity: 0 }, { easing: 'sineIn' }), tween(motion).to(duration, { t: duration }, { onUpdate: () => { if (!root.isValid) return; const t = motion.t; root.setPosition( start.x + vx * t, start.y + vy * t + 0.5 * g * t * t, start.z, ); }, }), ) .call(() => { this._pathMotion = null; onComplete(); }) .start(); } }