diff --git a/assets/resources/game/heros/top.prefab b/assets/resources/game/heros/top.prefab index b5a012c2..a7e237a4 100644 --- a/assets/resources/game/heros/top.prefab +++ b/assets/resources/game/heros/top.prefab @@ -38,10 +38,13 @@ "_components": [ { "__id__": 110 + }, + { + "__id__": 112 } ], "_prefab": { - "__id__": 112 + "__id__": 114 }, "_lpos": { "__type__": "cc.Vec3", @@ -1277,7 +1280,7 @@ "__id__": 65 } ], - "_active": true, + "_active": false, "_components": [ { "__id__": 73 @@ -2520,8 +2523,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 70, - "height": 20 + "width": 40, + "height": 11 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -2534,6 +2537,34 @@ "__type__": "cc.CompPrefabInfo", "fileId": "cdvV5hAkdEaY6AT6urHKuf" }, + { + "__type__": "c4181/FEWhKbY0D0PLdCyKi", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 113 + }, + "hpProgressBar": { + "__id__": 53 + }, + "lvLabel": null, + "shieldProgressBar": { + "__id__": 77 + }, + "hpBarSprite": { + "__id__": 36 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "3eCKBFOJhLHKqcnak/mi3d" + }, { "__type__": "cc.PrefabInfo", "root": { diff --git a/assets/script/game/hero/HeroTopBarComp.ts b/assets/script/game/hero/HeroTopBarComp.ts new file mode 100644 index 00000000..04c2b421 --- /dev/null +++ b/assets/script/game/hero/HeroTopBarComp.ts @@ -0,0 +1,184 @@ +import { _decorator, Label, ProgressBar, Sprite, UIOpacity, Vec3, v3, tween, Tween, Color, clamp } 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 { FacSet, FightSet } from "../common/config/GameSet"; + +const { ccclass, property } = _decorator; + +/** + * 英雄头顶状态栏组件 + * + * 挂载在英雄节点下的 "top" 子节点上,独立管理: + * - 血条进度(ProgressBar) + * - 护盾数值条(ProgressBar,top/shield 节点,显示 shield / SHIELD_MAX) + * - 等级数字(Label) + * - 透明度激活 / 闲置 + * - 受击抖动动画 + * + * 数据流: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: Label, tooltip: "等级数字" }) + private lvLabel: Label = 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(); + + // ==================== 配置 ==================== + 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 = true; + this.node.active = true; + + this.setTopBarOpacity(false); + this.hpShow(); + this.lvShow(); + 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; + } + + /** 更新等级数字 */ + public lvShow(): void { + if (!this.lvLabel || !this._model) return; + this.lvLabel.string = String(this._model.lv); + } + + /** 激活顶栏(高亮 + 延迟恢复闲置透明度) */ + 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(); + if (this.node && this.node.isValid) { + Tween.stopAllByTarget(this.node); + this.node.setPosition(this.basePos); + } + } +} diff --git a/assets/script/game/hero/HeroTopBarComp.ts.meta b/assets/script/game/hero/HeroTopBarComp.ts.meta new file mode 100644 index 00000000..dc8f7695 --- /dev/null +++ b/assets/script/game/hero/HeroTopBarComp.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "c4181fc5-1168-4a6d-8d03-d0f2dd0b22a2", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/hero/HeroViewComp.ts b/assets/script/game/hero/HeroViewComp.ts index f64bd1fb..b044550a 100644 --- a/assets/script/game/hero/HeroViewComp.ts +++ b/assets/script/game/hero/HeroViewComp.ts @@ -1,8 +1,9 @@ -import { Vec3, _decorator, v3, Collider2D, Contact2DType, Label, Node, Prefab, instantiate, ProgressBar, Component, Material, Sprite, math, clamp, Game, tween, Tween, Color, BoxCollider2D, UITransform, UIOpacity } from "cc"; +import { Vec3, _decorator, v3, Collider2D, Contact2DType, Node, Prefab, instantiate, tween, Tween, BoxCollider2D, UITransform } 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 { mLogger } from "../common/Logger"; import { HeroSpine } from "./HeroSpine"; +import { HeroTopBarComp } from "./HeroTopBarComp"; import { BoxSet, FacSet, FightSet, NumberFormatter, TooltipTypes } from "../common/config/GameSet"; import { smc } from "../common/SingletonModuleComp"; import { SkillSet, } from "../common/config/SkillSet"; @@ -37,21 +38,10 @@ export class HeroViewComp extends CCComp { realDeadTime: number = 0.1 deadCD: number = 0 monDeadTime: number = 0.1 - hp_height: number = 90 - boss_hp_height: number = 160 // 血条显示相关 lastBarUpdateTime: number = 0; // 最后一次血条/蓝条/护盾更新时间 // ==================== UI 节点引用 ==================== - private top_node: Node = null!; - private topOpacity: UIOpacity = null!; - private topBasePos: Vec3 = v3(); - private lvLabel: Label | null = null; // 等级显示文本 - private readonly barIdleOpacity: number = 153; - private readonly barActiveOpacity: number = 255; - private readonly idleOpacityDelay: number = 0.25; - private readonly restoreBarIdleOpacity = () => { - this.setTopBarOpacity(false); - }; + private topBar: HeroTopBarComp = null!; // ==================== 直接访问 HeroAttrsComp ==================== get model() { @@ -96,67 +86,26 @@ export class HeroViewComp extends CCComp { this.lastBarUpdateTime = 0; this.status_change("idle"); - // 初始化 UI 节点 - this.initUINodes(); + // 初始化头顶状态栏 + const topNode = this.node.getChildByName("top"); + this.topBar = topNode.getComponent(HeroTopBarComp) || topNode.addComponent(HeroTopBarComp); + this.topBar.init(this.model, this.scale); /** 方向 */ this.node.setScale(this.scale * Math.abs(this.node.scale.x), 1 * this.node.scale.y); // 确保 scale.x 为正后再乘方向 - // top_node 取 abs 避免对象池复用时 scale 累乘导致血条方向反转 - this.top_node.setScale(this.scale * Math.abs(this.top_node.scale.x), 1 * this.top_node.scale.y); - /* 显示角色血*/ - this.top_node.getChildByName("hp").active = true; - this.top_node.getChildByName("cd").active = false; - this.top_node.getChildByName("shield").active = false; - this.top_node.getChildByName("lv").active = true; - this.top_node.active = true; - this.setTopBarOpacity(false); - // 初始化等级显示 - this.lv_show(); - - // 🔥 重置血条 UI 显示状态 + // 🔥 重置描边 if (this.model) { - this.hp_show(); - - // 根据英雄模型中的等级数据设置描边 - // 注意:HeroViewComp 是挂在 node 上的,而 FlashSprite 可能挂在子节点(如 Sprite/anm 节点) let flashSprite = this.node.getComponent(FlashSprite); if (!flashSprite) { - // 如果当前节点没有,尝试在所有子节点中查找 flashSprite = this.node.getComponentInChildren(FlashSprite); } - if (flashSprite) { flashSprite.setOutlineByLevel(this.model.lv); } } } - /** 初始化 UI 节点引用 */ - private initUINodes() { - this.top_node = this.node.getChildByName("top"); - this.topOpacity = this.top_node.getComponent(UIOpacity) || this.top_node.addComponent(UIOpacity); - this.top_node.setPosition(0, this.hp_height, 0); - if (this.model.is_boss) this.top_node.setPosition(0, this.boss_hp_height, 0); - this.topBasePos = this.top_node.position.clone(); - const hpNode = this.top_node.getChildByName("hp"); - if (this.model.fac == FacSet.HERO) { - hpNode.getChildByName("Bar").getComponent(Sprite).color = new Color("#2ECC71") - } - - // 确保血条等UI始终在最上层显示 - this.top_node.setSiblingIndex(999); - - // 缓存等级显示 Label(lv 节点下的 Label 子节点) - const lvNode = this.top_node.getChildByName("lv"); - if (lvNode) { - this.lvLabel = lvNode.getChildByName("Label")?.getComponent(Label) ?? null; - } - } - - - - /** * View 层每帧更新 * 注意:数据更新逻辑已移到 HeroAttrSystem,这里只负责显示 @@ -181,18 +130,18 @@ export class HeroViewComp extends CCComp { // ✅ 按需更新 UI(脏标签模式)- 只在属性变化时更新 if (this.model.dirty_hp) { - this.hp_show(); + this.topBar.hpShow(); this.model.dirty_hp = false; } if (this.model.dirty_shield) { - this.show_shield(this.model.shield); + this.topBar.shieldShow(this.model.shield); this.model.dirty_shield = false; } if (this.model.dirty_lv) { - this.lv_show(); + this.topBar.lvShow(); this.model.dirty_lv = false; } } @@ -201,83 +150,6 @@ export class HeroViewComp extends CCComp { return; } - /** 显示护盾 */ - private show_shield(shield: number = 0) { - this.lastBarUpdateTime = Date.now() / 1000; - this.node.getChildByName("shielded").active = shield > 0; - } - - /** 显示血量 */ - private hp_show() { - this.lastBarUpdateTime = Date.now() / 1000; - // 不再基于血量是否满来决定显示状态,只更新进度条 - let hp = this.model.hp; - let hp_max = this.model.hp_max; - // mLogger.log(this.debugMode, 'HeroViewComp', "hp_show",hp,hp_max) - - let targetProgress = hp_max > 0 ? hp / hp_max : 0; - targetProgress = clamp(targetProgress, 0, 1); - let hpNode = this.top_node.getChildByName("hp"); - let hpProgressBar = hpNode.getComponent(ProgressBar); - - const prevProgress = hpProgressBar.progress; - if (targetProgress < hpProgressBar.progress) { - this.activateTopBar(); - this.playHpBarShake(); - } - hpProgressBar.progress = targetProgress; - if (targetProgress > prevProgress) { - this.setTopBarOpacity(false); - } - } - - private isFullHpAndNoShield(): boolean { - if (!this.model) return false; - if (this.model.hp_max <= 0) return false; - return this.model.hp >= this.model.hp_max; - } - - /** 更新等级显示 */ - private lv_show() { - if (!this.lvLabel || !this.model) return; - this.lvLabel.string = String(this.model.lv); - } - - private setTopBarOpacity(isActive: boolean) { - if (!this.top_node || !this.top_node.isValid) return; - if (this.topOpacity) { - this.topOpacity.opacity = this.barActiveOpacity; - } - if (isActive) { - this.top_node.active = true; - return; - } - this.top_node.active = true; - } - - private activateTopBar() { - this.setTopBarOpacity(true); - this.unschedule(this.restoreBarIdleOpacity); - this.scheduleOnce(this.restoreBarIdleOpacity, this.idleOpacityDelay); - } - - private playHpBarShake() { - if (!this.top_node || !this.top_node.isValid) return; - Tween.stopAllByTarget(this.top_node); - this.top_node.setPosition(this.topBasePos); - tween(this.top_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.top_node.setPosition(this.topBasePos); - }) - .start(); - } - - - /** 升级特效 */ public lv_up() { this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0); @@ -432,7 +304,7 @@ export class HeroViewComp extends CCComp { } add_shield(shield: number) { // 护盾数据更新由 Model 层处理,这里只负责视图表现 - if (this.model && this.model.shield > 0) this.show_shield(this.model.shield); + if (this.model && this.model.shield > 0) this.topBar.shieldShow(this.model.shield); } health(hp: number = 0) { @@ -458,7 +330,7 @@ export class HeroViewComp extends CCComp { } // 恢复UI - this.top_node.active = true; + this.topBar.show(); this.status_change("idle"); @@ -504,7 +376,7 @@ export class HeroViewComp extends CCComp { } // 隐藏UI - this.top_node.active = false; + this.topBar.hide(); // 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞 const collider = this.node.getComponent(Collider2D); @@ -528,7 +400,7 @@ export class HeroViewComp extends CCComp { /** 受击表现(伤害数字、受击动画、击退) */ do_atked(damage: number, isCrit: boolean, s_uuid: number, knockbackExtra: number = 0) { // 受到攻击时更新最后更新时间 - this.activateTopBar(); + this.topBar.activateTopBar(); this.lastBarUpdateTime = Date.now() / 1000; if (damage <= 0) return; @@ -630,7 +502,7 @@ export class HeroViewComp extends CCComp { if (!this.model) return; const damageText = NumberFormatter.formatNumber(Math.max(0, Math.floor(damage))); - this.hp_show(); + this.topBar.hpShow(); if (isCrit) { this.hp_tip(TooltipTypes.crit, damageText); } else { @@ -641,10 +513,7 @@ export class HeroViewComp extends CCComp { // 清理残留的定时器和缓动 this.unscheduleAllCallbacks(); Tween.stopAllByTarget(this.node); - if (this.top_node && this.top_node.isValid) { - Tween.stopAllByTarget(this.top_node); - this.top_node.setPosition(this.topBasePos); - } + this.topBar.reset(); // 清理碰撞器事件监听 const collider = this.getComponent(Collider2D); @@ -653,7 +522,6 @@ export class HeroViewComp extends CCComp { } this.deadCD = 0 this.lastBarUpdateTime = 0 - this.unschedule(this.restoreBarIdleOpacity); // 清理伤害队列 this.damageQueue.length = 0;