Files
pixelheros/assets/script/game/hero/HeroTopBarComp.ts
panFD e8c6a7ca6a feat: 实现DoT灼烧技能与debuff图标系统
本次提交完成了以下核心改动:
1.  调整fb-1~fb-6技能预制件的攻击偏移坐标,优化技能弹道位置
2.  新增DoT伤害机制,支持周期伤害/治疗的独立结算
3.  为Buff系统添加施法者属性快照,让DoT暴击、控制判定更准确
4.  新增顶栏debuff图标显示功能,支持灼烧等负面状态可视化
5.  为破晓先知等英雄替换为新的灼烧火球技能6104
6.  修复fb-2预制件的配置字段顺序与多余属性问题
2026-08-15 14:36:38 +08:00

240 lines
9.2 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 } 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!;
// ==================== 内部引用 ====================
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 = 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<number, string> = {
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<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 = 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);
}
}
}