import { instantiate, Node, Tween, tween, UIOpacity, UITransform, v3, Vec3 } from 'cc'; /** icon 影子飞行时长(秒) */ const ICON_FLY_SEC = 0.45; /** 技能槽放大倍率 */ const SLOT_PULSE_SCALE = 1.1; /** 放大缩小循环次数 */ const SLOT_PULSE_TIMES = 2; /** 单次放大/缩小时长(秒) */ const SLOT_PULSE_HALF_SEC = 0.12; /** 影子透明度 */ const SHADOW_OPACITY = 200; /** * 每日商店购买:货架 icon 影子飞入槽位 + 技能放大缩小。 */ export class DailyShopBuySkillFlyPresenter { private _flyLayer: Node | null = null; private _shadowNode: Node | null = null; private _pulseNode: Node | null = null; /** 绑定飞行层(弹窗 ui_center) */ bind(flyLayer: Node | null | undefined): void { this._flyLayer = flyLayer?.isValid ? flyLayer : null; } /** 注销并中断动画 */ unbind(): void { this._stopShadow(); this._stopPulse(); this._flyLayer = null; } /** * 货架 icon 影子飞到槽位空模板中心。 * @param sourceIcon ui_shopItem 的 icon 节点 * @param targetSlotBase 空模板根节点 * @param onArrived 飞行结束 */ playIconFly(sourceIcon: Node, targetSlotBase: Node, onArrived: () => void): void { const flyLayer = this._flyLayer; if (!sourceIcon?.isValid || !targetSlotBase?.isValid || !flyLayer?.isValid) { console.log('[DailyShopBuySkillFly] 飞行参数无效,跳过'); onArrived(); return; } this._stopShadow(); const shadow = instantiate(sourceIcon); shadow.name = 'dailyShopBuyIconShadow'; flyLayer.addChild(shadow); shadow.setSiblingIndex(flyLayer.children.length - 1); shadow.worldPosition = sourceIcon.worldPosition.clone(); shadow.setWorldScale(sourceIcon.worldScale.clone()); const opacity = shadow.getOrAddComponent(UIOpacity); opacity.opacity = SHADOW_OPACITY; const endPos = this._resolveSlotFlyTargetWorld(targetSlotBase); this._shadowNode = shadow; tween(shadow) .to(ICON_FLY_SEC, { worldPosition: endPos }, { easing: 'sineInOut' }) .call(this._onIconFlyArrived.bind(this, onArrived)) .start(); } /** 技能槽放大 1.1 倍后回到 1,执行 2 次 */ playSlotPulse(skillItem: Node, onDone?: () => void): void { if (!skillItem?.isValid) { onDone?.(); return; } this._stopPulse(); this._pulseNode = skillItem; const sx = skillItem.scale.x; const sy = skillItem.scale.y; const sz = skillItem.scale.z; let tw = tween(skillItem); for (let i = 0; i < SLOT_PULSE_TIMES; i++) { tw = tw .to(SLOT_PULSE_HALF_SEC, { scale: v3(sx * SLOT_PULSE_SCALE, sy * SLOT_PULSE_SCALE, sz) }, { easing: 'sineOut' }) .to(SLOT_PULSE_HALF_SEC, { scale: v3(sx, sy, sz) }, { easing: 'sineIn' }); } tw.call(this._onSlotPulseDone.bind(this, onDone)).start(); } /** 槽位飞行落点(空模板中心世界坐标) */ private _resolveSlotFlyTargetWorld(targetSlotBase: Node): Vec3 { const ut = targetSlotBase.getComponent(UITransform); if (ut) return ut.convertToWorldSpaceAR(v3(0, 0, 0)); return targetSlotBase.worldPosition.clone(); } /** icon 影子飞行结束 */ private _onIconFlyArrived(onArrived: () => void): void { this._stopShadow(); onArrived(); } /** 技能槽脉冲结束 */ private _onSlotPulseDone(onDone?: () => void): void { this._pulseNode = null; onDone?.(); } /** 停止并销毁影子节点 */ private _stopShadow(): void { const shadow = this._shadowNode; this._shadowNode = null; if (!shadow?.isValid) return; Tween.stopAllByTarget(shadow); shadow.destroy(); } /** 停止技能槽脉冲并还原 scale */ private _stopPulse(): void { const node = this._pulseNode; this._pulseNode = null; if (!node?.isValid) return; Tween.stopAllByTarget(node); } }