Files
pixelheros/assets/script/game/hero/HeroTopBarComp.ts
pan d6e8cb4828 feat(hero): 添加技能CD进度条显示功能
1. 为英雄顶栏组件新增CD进度条属性与显示逻辑
2. 重构HeroViewComp的cd_show方法,转发调用顶栏CD更新逻辑
3. 调整顶栏默认透明度为全不透明,优化显示体验
4. 根据英雄是否拥有第二技能动态显示CD进度条
2026-08-19 16:05:58 +08:00

257 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, DEBUFF_ICON_MAP } from "../common/config/BuffSet";
import { FacSet, FightSet } from "../common/config/GameSet";
const { ccclass, property } = _decorator;
/**
* 英雄头顶状态栏组件
*
* 挂载在英雄节点下的 "top" 子节点上,独立管理:
* - 血条进度ProgressBar
* - 护盾数值条ProgressBartop/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!;
@property({ type: ProgressBar, tooltip: "能量条top 下的 cd 节点,反映 skills[1] 技能 CD" })
private cdProgressBar: ProgressBar = null!;
// ==================== 内部引用 ====================
private topOpacity: UIOpacity = null!;
private basePos: Vec3 = v3();
/** debuff 容器节点top 下的 debuff 子节点,按名字管理各 debuff 图标) */
private debuffNode: Node = null!;
/** 当前点亮的 debuff 图标节点名集合(用于 diff避免每帧重复 set active */
private activeDebuffNames: Set<string> = new Set();
// ==================== 配置 ====================
private hp_height: number = 90;
private boss_hp_height: number = 160;
// ==================== 透明度 ====================
/** 闲置透明度:取消默认半透明,顶栏常驻全不透明 */
private readonly barIdleOpacity: number = 255;
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("lv")!.active = false;
this.node.active = true;
// 能量条仅当英雄拥有第二技能skills[1])时显示
if (this.cdProgressBar) {
this.cdProgressBar.node.active = this._model.getSkillIds().length > 1;
}
this.cdShow();
// 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;
}
/**
* 更新能量条进度(= skills[1] 的 CD 充能比例,充满即技能就绪)。
* 由 SCastSystem 每帧 updateCD 后经 HeroViewComp.cd_show 驱动。
*/
public cdShow(): void {
if (!this._model || !this.cdProgressBar) return;
const skillIds = this._model.getSkillIds();
// 无第二技能时隐藏能量条
if (skillIds.length <= 1) {
this.cdProgressBar.node.active = false;
return;
}
this.cdProgressBar.node.active = true;
this.cdProgressBar.progress = clamp(this._model.getSkillCdProgress(skillIds[1]), 0, 1);
}
/**
* 刷新 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<string>();
if (buffComp) {
for (const [buffId, layers] of buffComp.buffs) {
if (layers.length === 0) continue;
const cfg = BuffList[buffId];
// 只显示伤害类 debufftick 负值)与控制类 debuff普通 Buff 不占用 debuff 栏
if (!cfg || cfg.category === BuffCategory.Buff) continue;
const nodeName = 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);
}
}
}