本次提交完成了以下核心改动: 1. 调整fb-1~fb-6技能预制件的攻击偏移坐标,优化技能弹道位置 2. 新增DoT伤害机制,支持周期伤害/治疗的独立结算 3. 为Buff系统添加施法者属性快照,让DoT暴击、控制判定更准确 4. 新增顶栏debuff图标显示功能,支持灼烧等负面状态可视化 5. 为破晓先知等英雄替换为新的灼烧火球技能6104 6. 修复fb-2预制件的配置字段顺序与多余属性问题
240 lines
9.2 KiB
TypeScript
240 lines
9.2 KiB
TypeScript
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<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];
|
||
// 只显示伤害类 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);
|
||
}
|
||
}
|
||
}
|