import { _decorator, ProgressBar, Sprite, UIOpacity, Vec3, v3, tween, Tween, Color, clamp, Node } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { BuffComp } from "./BuffComp"; import { BuffList, BuffCategory } from "../common/config/BuffSet"; import { FacSet, FightSet } from "../common/config/GameSet"; const { ccclass, property } = _decorator; /** * 英雄头顶状态栏组件 * * 挂载在英雄节点下的 "top" 子节点上,独立管理: * - 血条进度(ProgressBar) * - 护盾数值条(ProgressBar,top/shield 节点,显示 shield / SHIELD_MAX) * - 透明度激活 / 闲置 * - 受击抖动动画 * * 数据流:HeroViewComp 将 HeroAttrsComp 引用传入,本组件通过 model 读取属性。 * 注意:英雄根节点下的 "shielded" 是护盾特效动画节点,由 HeroViewComp 管理,不在本组件职责内。 */ @ccclass('HeroTopBarComp') @ecs.register('HeroTopBar', false) export class HeroTopBarComp extends CCComp { // ==================== @property 绑定(编辑器拖拽) ==================== @property({ type: ProgressBar, tooltip: "血条进度条" }) private hpProgressBar: ProgressBar = null!; @property({ type: ProgressBar, tooltip: "护盾数值条(top 下的 shield 节点)" }) private shieldProgressBar: ProgressBar = null!; @property({ type: Sprite, tooltip: "血条填充 Sprite(用于设置阵营颜色)" }) private hpBarSprite: Sprite = null!; // ==================== 内部引用 ==================== private topOpacity: UIOpacity = null!; private basePos: Vec3 = v3(); /** debuff 容器节点(top 下的 debuff 子节点,按名字管理各 debuff 图标) */ private debuffNode: Node = null!; /** 当前点亮的 debuff 图标节点名集合(用于 diff,避免每帧重复 set active) */ private activeDebuffNames: Set = new Set(); // ==================== 配置 ==================== private hp_height: number = 90; private boss_hp_height: number = 160; // ==================== 透明度 ==================== private readonly barIdleOpacity: number = 153; private readonly barActiveOpacity: number = 255; private readonly idleOpacityDelay: number = 0.25; /** 外部赋值:英雄数据模型 */ private _model: HeroAttrsComp = null!; public get model(): HeroAttrsComp { return this._model; } public set model(value: HeroAttrsComp) { this._model = value; } private readonly restoreBarIdleOpacity = () => { this.setTopBarOpacity(false); }; /** * 初始化 / 对象池复用时重置 * @param model 英雄属性组件 * @param direction 显示方向(1 或 -1) */ public init(model: HeroAttrsComp, direction: number = 1): void { this._model = model; // UIOpacity 无 @property 绑定,运行时获取或添加 this.topOpacity = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity); // 方向取 abs,避免对象池复用时 scale 累乘导致血条反转 this.node.setScale(direction * Math.abs(this.node.scale.x), 1 * this.node.scale.y); // 位置 const height = model.is_boss ? this.boss_hp_height : this.hp_height; this.node.setPosition(0, height, 0); this.basePos = this.node.position.clone(); // 血条颜色(英雄阵营为绿色) if (model.fac === FacSet.HERO && this.hpBarSprite) { this.hpBarSprite.color = new Color("#2ECC71"); } // 确保血条等 UI 始终在最上层 this.node.setSiblingIndex(999); // 重置显示状态 this.node.getChildByName("hp")!.active = true; this.node.getChildByName("cd")!.active = false; this.node.getChildByName("lv")!.active = false; this.node.active = true; // debuff 图标容器(无该节点的旧 prefab 容错为 null) this.debuffNode = this.node.getChildByName("debuff")!; this.activeDebuffNames.clear(); if (this.debuffNode) { for (const child of this.debuffNode.children) { child.active = false; } } this.setTopBarOpacity(false); this.hpShow(); this.shieldShow(model.shield); } /** 更新血量进度条 */ public hpShow(): void { if (!this._model || !this.hpProgressBar) return; const hp = this._model.hp; const hpMax = this._model.hp_max; let targetProgress = hpMax > 0 ? hp / hpMax : 0; targetProgress = clamp(targetProgress, 0, 1); const prevProgress = this.hpProgressBar.progress; if (targetProgress < prevProgress) { this.activateTopBar(); this.playHpBarShake(); } this.hpProgressBar.progress = targetProgress; if (targetProgress > prevProgress) { this.setTopBarOpacity(false); } } /** 更新护盾数值条(百分比 = shield / SHIELD_MAX) */ public shieldShow(shield: number = 0): void { if (!this.shieldProgressBar) return; const progress = FightSet.SHIELD_MAX > 0 ? shield / FightSet.SHIELD_MAX : 0; this.shieldProgressBar.progress = clamp(progress, 0, 1); // 护盾节点显隐 this.shieldProgressBar.node.active = shield > 0; } /** * buffId → debuff 图标节点名映射(节点名为 top.prefab debuff 容器下的子节点名)。 * 仅伤害类 debuff 需要图标;未映射的 buff 不显示。 */ private static readonly DEBUFF_ICON_MAP: Record = { 7002: "FireDamage", // 灼烧 }; /** * 刷新 debuff 图标显隐:按 BuffComp 中当前生效的 debuff 种类点亮对应图标。 * 由 HeroViewComp 在 dirty_buffs 脏标记时调用;与上次状态做 diff,避免重复 set active。 * @param buffComp 目标的 buff 组件(无 BuffComp 时全部隐藏) */ public debuffShow(buffComp: BuffComp | null): void { if (!this.debuffNode) return; // 收集当前生效的、有图标映射的 debuff 节点名 const wantActive = new Set(); if (buffComp) { for (const [buffId, layers] of buffComp.buffs) { if (layers.length === 0) continue; const cfg = BuffList[buffId]; // 只显示伤害类 debuff(tick 负值)与控制类 debuff;普通 Buff 不占用 debuff 栏 if (!cfg || cfg.category === BuffCategory.Buff) continue; const nodeName = HeroTopBarComp.DEBUFF_ICON_MAP[buffId]; if (nodeName) wantActive.add(nodeName); } } // diff:先隐藏不再需要的,再点亮新增的 for (const name of this.activeDebuffNames) { if (!wantActive.has(name)) { const child = this.debuffNode.getChildByName(name); if (child) child.active = false; } } for (const name of wantActive) { if (!this.activeDebuffNames.has(name)) { const child = this.debuffNode.getChildByName(name); if (child) child.active = true; } } this.activeDebuffNames = wantActive; } /** 激活顶栏(高亮 + 延迟恢复闲置透明度) */ public activateTopBar(): void { this.setTopBarOpacity(true); this.unschedule(this.restoreBarIdleOpacity); this.scheduleOnce(this.restoreBarIdleOpacity, this.idleOpacityDelay); } /** 设置顶栏透明度 */ public setTopBarOpacity(isActive: boolean): void { if (!this.node || !this.node.isValid) return; if (this.topOpacity) { this.topOpacity.opacity = isActive ? this.barActiveOpacity : this.barIdleOpacity; } this.node.active = true; } /** 显示顶栏 */ public show(): void { if (this.node && this.node.isValid) this.node.active = true; } /** 隐藏顶栏 */ public hide(): void { if (this.node && this.node.isValid) this.node.active = false; } /** 血条受击抖动动画 */ private playHpBarShake(): void { if (!this.node || !this.node.isValid) return; Tween.stopAllByTarget(this.node); this.node.setPosition(this.basePos); tween(this.node) .by(0.04, { position: v3(-3, 0, 0) }) .by(0.04, { position: v3(6, 0, 0) }) .by(0.04, { position: v3(-5, 0, 0) }) .by(0.04, { position: v3(2, 0, 0) }) .call(() => { this.node.setPosition(this.basePos); }) .start(); } /** 对象池回收 / 组件重置时调用 */ public reset(): void { this.unscheduleAllCallbacks(); this.activeDebuffNames.clear(); if (this.debuffNode && this.debuffNode.isValid) { for (const child of this.debuffNode.children) { child.active = false; } } if (this.node && this.node.isValid) { Tween.stopAllByTarget(this.node); this.node.setPosition(this.basePos); } } }