/** * @file CoinFlyComp.ts * @description 金币飞行表现组件(表现层,纯视觉) * * 职责: * 1. 监听 CoinFly 事件(怪物死亡掉金币,账务已由 MissionEconomy 即时结算)。 * 2. 从怪物位置生成金币 icon,抛物线散开后加速飞入钱包 icon。 * 3. 独立 NodePool 管理金币节点(上限 30),不复用伤害飘字池。 * * 使用 oops-framework 模块:oops.message(事件解耦)。 * 编辑器绑定:flyLayer(全屏容器,最高 sibling)、coinIconSrc(钱包 coin/icon 节点,取其 spriteFrame)。 */ import { _decorator, Node, Sprite, UITransform, tween, v3, Vec3, NodePool } from "cc"; import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { GameEvent } from "../common/config/GameEvent"; const { ccclass, property } = _decorator; /** 普通怪金币枚数 */ const COIN_COUNT_NORMAL = 3; /** Boss 金币枚数 */ const COIN_COUNT_BOSS = 8; /** 对象池上限(防节点泄漏) */ const POOL_MAX = 30; @ccclass('CoinFlyComp') export class CoinFlyComp extends CCComp { @property({ type: Node, tooltip: "金币飞行容器(全屏节点,置于最高 sibling 盖在所有 UI 之上)" }) flyLayer: Node | null = null; @property({ type: Node, tooltip: "钱包金币 icon 节点(运行期取其 spriteFrame 克隆)" }) coinIconSrc: Node | null = null; /** 金币节点对象池 */ private pool: NodePool = new NodePool(); onLoad() { // oops.message 全局事件总线:金币飞行 oops.message.on(GameEvent.CoinFly, this.onCoinFly, this); } onDestroy() { oops.message.off(GameEvent.CoinFly, this.onCoinFly, this); this.pool.clear(); } private onCoinFly(event: string, data: { worldPos: Vec3 | null; gold: number; isBoss: boolean }) { if (!this.flyLayer || !this.coinIconSrc || !data.worldPos) return; const uiTransform = this.flyLayer.getComponent(UITransform); if (!uiTransform) return; const startPos = uiTransform.convertToNodeSpaceAR(data.worldPos); const endPos = uiTransform.convertToNodeSpaceAR(this.coinIconSrc.worldPosition); const count = data.isBoss ? COIN_COUNT_BOSS : COIN_COUNT_NORMAL; for (let i = 0; i < count; i++) { this.spawnCoin(startPos, endPos, i * 0.03); } } /** 生成一枚金币:上抛散开 → 加速飞向钱包 → 回收 */ private spawnCoin(startPos: Vec3, endPos: Vec3, delay: number) { const coin = this.pool.size() > 0 ? this.pool.get()! : this.createCoinNode(); coin.parent = this.flyLayer; coin.setPosition(startPos); coin.setScale(v3(1, 1, 1)); const scatter = v3( startPos.x + (Math.random() * 120 - 60), startPos.y + 60 + Math.random() * 40, 0); tween(coin) .delay(delay) .to(0.25, { position: scatter }, { easing: "quadOut" }) .to(0.4, { position: endPos }, { easing: "quadIn" }) .call(() => this.recycleCoin(coin)) .start(); } private createCoinNode(): Node { const node = new Node("coin_fly"); node.addComponent(UITransform).setContentSize(32, 32); const sp = node.addComponent(Sprite); const srcSp = this.coinIconSrc?.getComponent(Sprite); if (srcSp?.spriteFrame) { sp.spriteFrame = srcSp.spriteFrame; } return node; } private recycleCoin(coin: Node) { if (this.pool.size() >= POOL_MAX) { coin.destroy(); return; } this.pool.put(coin); } }