@@ -1,715 +0,0 @@
/**
* @file HInfoComp.ts
* @description 场上英雄信息卡片组件( UI 视图层)
*
* 职责:
* 1. 显示单个场上英雄的实时属性信息( AP / HP / 边框品质 / idle 动画)。
* 2. 由 MissionCardComp.ensureHeroInfoPanel() 在英雄上场时实例化并绑定数据。
* 3. 支持点击打开英雄详情弹窗( IBox) 、出售英雄等交互( 当前已注释) 。
*
* 关键设计:
* - 通过 bindData(eid, model) 将组件与某个英雄 ECS 实体绑定。
* - refresh() 被 MissionCardComp 定时调用以同步实时属性变化。
* - iconVisualToken 机制与 CardComp 一致,用于异步动画加载的竞态保护。
* - isModelAlive() 检测绑定的英雄实体是否仍存活( ECS ent 引用是否有效)。
*
* 依赖:
* - HeroAttrsComp —— 英雄属性数据模型
* - HeroInfo( heroSet) —— 英雄静态配置
* - Hero —— 英雄 ECS 实体类(用于出售删除)
* - UIID.IBox —— 英雄详情弹窗 ID
*/
import { _decorator , AnimationClip , Button , Event , EventTouch , Input , Label , Layout , Node , NodeEventType , Sprite , UITransform , input , resources , CCInteger , SpriteFrame } 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 { heroInfo , HeroInfo } from "../common/config/heroSet" ;
import { HeroAttrsComp } from "../hero/HeroAttrsComp" ;
import { smc } from "../common/SingletonModuleComp" ;
import { Hero } from "../hero/Hero" ;
import { FieldSkillType , FieldSkillSet } from "../common/config/SkillSet" ;
import { buildSkillDesc } from "../common/config/HeroSkillDesc" ;
import { GameEvent } from "../common/config/GameEvent" ;
import { oops } from "db://oops-framework/core/Oops" ;
import { UIID } from "../common/config/GameUIConfig" ;
import { mLogger } from "../common/Logger" ;
import { MissionHeroComp } from "./MissionHeroComp" ;
import { MoveComp } from "../hero/MoveComp" ;
import { FacSet , getLvColor } from "../common/config/GameSet" ;
import { CKind , CardPoolList , CardConfig , CardTriggerType } from "../common/config/CardSet" ;
import { SkillCardList } from "../common/config/SCardSet" ;
import { EquipPoolList } from "../common/config/EquipSet" ;
import { SkillSet } from "../common/config/SkillSet" ;
import { CardBgComp } from "./CardBgComp" ;
import { MissionEconomy } from "./MissionEconomy" ;
const { property , ccclass } = _decorator ;
/**
* HInfoComp —— 场上英雄信息面板视图组件
*
* 每个实例对应一个出战英雄,在卡牌面板下方横向排列。
* 实时显示英雄的 AP / HP 和 idle 动画图标。
*/
@ccclass ( 'HInfoComp' )
@ecs . register ( 'HInfoComp' , false )
export class HInfoComp extends CCComp {
/** 英雄 idle 动画图标节点 */
@property ( Node )
icon_node = null !
/** 技能图标节点 */
@property ( Node )
skill_icon_node = null !
/** 出售按钮节点 */
@property ( Node )
sell_node = null !
/** 卡牌背景底框节点(按卡池等级切换子节点显示) */
@property ( Node )
BG_node = null !
/** 关闭窗口按钮节点 */
@property ( Node )
close_node = null !
/** 英雄名字节点 */
@property ( Node )
Name_node = null !
/** 技能信息节点 */
@property ( Node )
info_node = null !
@property ( Label )
lv_node : Label = null !
@property ( Node )
ap_node = null !
@property ( Node )
hp_node = null !
@property ( Node )
cd_node = null !
/** 绑定的英雄 ECS 实体 ID */
private eid : number = 0 ;
/** 绑定的英雄属性数据模型引用 */
private model : HeroAttrsComp | null = null ;
/** 是否为静态预览模式(卡牌点击查看,非场上英雄) */
private isPreview : boolean = false ;
/** 静态预览数据 */
private previewUuid : number = 0 ;
private previewLv : number = 1 ;
private previewPoolLv : number = 1 ;
private isSkillCard : boolean = false ;
/** 是否为装备卡预览(决定从 EquipPoolList 查询配置) */
private isEquipCard : boolean = false ;
/** 英雄名字标签缓存引用 */
private nameLabel : Label | null = null ;
/** 技能信息标签缓存引用 */
private infoLabel : Label | null = null ;
/** infos 逐条展示的 Label 池( 索引0复用预制体 Label, 其余动态创建) */
private infoLineLabels : Label [ ] = [ ] ;
/** AP 标签缓存引用 */
private apLabel : Label | null = null ;
/** AP 加成标签缓存引用 */
private apPlusLabel : Label | null = null ;
/** HP 标签缓存引用 */
private hpLabel : Label | null = null ;
/** HP 加成标签缓存引用 */
private hpPlusLabel : Label | null = null ;
/** CD 标签缓存引用 */
private cdLabel : Label | null = null ;
/** 图标视觉令牌(异步加载竞态保护) */
private iconVisualToken : number = 0 ;
/** 当前显示的英雄 UUID( 避免相同 UUID 重复加载动画) */
private iconHeroUuid : number = 0 ;
/** 调试日志开关 */
private debugMode : boolean = true ;
onLoad() {
this . cacheLabels ( ) ;
this . bindEvents ( ) ;
}
onAdded ( args : { eid? : number ; heroUuid? : number ; heroLv? : number ; poolLv? : number ; isSkillCard? : boolean , isEquipCard? : boolean } ) {
if ( args ? . heroUuid ) {
this . bindPreviewData ( args . heroUuid , args . heroLv ? ? 1 , args . poolLv ? ? 1 , args . isSkillCard ? ? false , args . isEquipCard ? ? false ) ;
return ;
}
const eid = args ? . eid ? ? 0 ;
if ( ! eid ) return ;
let foundModel : HeroAttrsComp | null = null ;
ecs . query ( ecs . allOf ( HeroAttrsComp ) ) . forEach ( ( entity : ecs.Entity ) = > {
if ( entity . eid === eid ) {
foundModel = entity . get ( HeroAttrsComp ) ;
}
} ) ;
if ( foundModel ) {
this . bindData ( eid , foundModel ) ;
} else {
this . isClosing = true ;
oops . gui . remove ( UIID . HInfo ) ;
}
}
/**
* 静态预览模式刷新:从 HeroInfo 配置读取初始数据。
*/
private refreshPreview() {
const heroUuid = this . previewUuid ;
const heroLv = Math . max ( 1 , this . previewLv ) ;
// 技能卡无等级概念,隐藏等级节点;英雄卡显示等级
if ( this . lv_node ) {
if ( this . isSkillCard ) {
this . lv_node . node . active = false ;
} else {
this . lv_node . node . active = true ;
this . lv_node . string = ` Lv. ${ heroLv } ` ;
this . lv_node . color = getLvColor ( heroLv ) ;
}
}
const kindName = this . isSkillCard ? CKind [ CKind . Skill ] : CKind [ CKind . Hero ] ;
if ( this . BG_node ) {
this . BG_node . children . forEach ( child = > {
child . active = ( child . name === kindName ) ;
const bg = child . getComponent ( CardBgComp ) ;
if ( bg ) child . active ? bg . apply ( this . previewPoolLv ) : bg . clear ( ) ;
} ) ;
}
if ( this . isSkillCard ) {
// ================= 技能卡预览 =================
// 互斥:技能卡只显示技能图标,隐藏英雄图标
if ( this . icon_node ) this . icon_node . active = false ;
if ( this . skill_icon_node ) this . skill_icon_node . active = true ;
// 根据来源从对应卡池查询配置
const config = this . isEquipCard
? EquipPoolList . find ( c = > c . uuid === heroUuid )
: ( SkillCardList . find ( c = > c . uuid === heroUuid ) ? ? CardPoolList . find ( c = > c . uuid === heroUuid ) ) ;
if ( ! config ) return ;
if ( this . nameLabel ) this . nameLabel . string = config . name ? ? "" ;
if ( this . apLabel ) {
this . apLabel . string = "-" ;
if ( this . apPlusLabel ) this . apPlusLabel . node . active = false ;
}
if ( this . hpLabel ) {
this . hpLabel . string = "-" ;
if ( this . hpPlusLabel ) this . hpPlusLabel . node . active = false ;
}
if ( this . infoLabel ) {
this . renderFallbackLines ( config . info ? ? "" ) ;
}
if ( this . cdLabel ) {
this . cdLabel . string = config . t_inv ? ` ${ config . t_inv } s ` : "0s" ;
}
// 更新技能图标
if ( heroUuid !== this . iconHeroUuid ) {
this . iconHeroUuid = heroUuid ;
this . iconVisualToken += 1 ;
this . updateSkillAnimation ( this . skill_icon_node , config , this . iconVisualToken ) ;
}
} else {
// ================= 英雄卡预览 =================
// 互斥:英雄卡只显示英雄图标,隐藏技能图标
if ( this . icon_node ) this . icon_node . active = true ;
if ( this . skill_icon_node ) this . skill_icon_node . active = false ;
const hero = HeroInfo [ heroUuid ] ;
if ( ! hero ) return ;
if ( heroUuid !== this . iconHeroUuid ) {
this . iconHeroUuid = heroUuid ;
this . iconVisualToken += 1 ;
this . updateHeroAnimation ( this . icon_node , heroUuid , this . iconVisualToken ) ;
}
if ( this . nameLabel ) this . nameLabel . string = hero . name ? ? "" ;
if ( this . apLabel ) {
const ap = Math . max ( 0 , Math . floor ( ( hero . ap ? ? 0 ) * heroLv ) ) ;
this . apLabel . string = ` ${ ap } ` ;
if ( this . apPlusLabel ) this . apPlusLabel . node . active = false ;
}
if ( this . hpLabel ) {
const hp = Math . max ( 0 , Math . floor ( ( hero . hp ? ? 0 ) * heroLv ) ) ;
this . hpLabel . string = ` ${ hp } ` ;
if ( this . hpPlusLabel ) this . hpPlusLabel . node . active = false ;
}
if ( this . infoLabel ) {
this . renderHeroInfoLines ( hero ) ;
}
if ( this . cdLabel ) {
const skillKeys = hero . skills ? Object . keys ( hero . skills ) : [ ] ;
const displaySkill = skillKeys . length > 1 ? hero . skills [ skillKeys [ 1 ] ] : ( skillKeys . length > 0 ? hero . skills [ skillKeys [ 0 ] ] : null ) ;
this . cdLabel . string = displaySkill ? . cd ? ` ${ displaySkill . cd . toFixed ( 1 ) } s ` : "0s" ;
}
}
}
/** 是否正在关闭中,防止重复调用 remove */
private isClosing : boolean = false ;
update ( dt : number ) {
if ( this . isClosing ) return ;
if ( this . isPreview ) return ;
if ( ! this . isModelAlive ( ) ) {
this . isClosing = true ;
oops . gui . remove ( UIID . HInfo ) ;
return ;
}
this . refresh ( ) ;
}
onDestroy() {
super . onDestroy ( ) ;
this . unbindEvents ( ) ;
}
/**
* 绑定英雄数据:关联实体 ID 和属性模型,并立即刷新显示。
* @param eid 英雄 ECS 实体 ID
* @param model 英雄属性组件引用
*/
bindData ( eid : number , model : HeroAttrsComp ) {
this . eid = eid ;
this . model = model ;
this . isPreview = false ;
if ( this . sell_node ) this . sell_node . active = true ;
if ( this . close_node ) this . close_node . active = true ;
this . cacheLabels ( ) ;
this . refresh ( ) ;
}
/**
* 绑定静态预览数据(卡牌点击查看)。
* 通过 HeroInfo/CardPoolList 配置 × 等级计算初始值,不绑定 ECS 实体。
*/
bindPreviewData ( heroUuid : number , heroLv : number , poolLv : number , isSkillCard : boolean = false , isEquipCard : boolean = false ) {
this . isPreview = true ;
this . previewUuid = heroUuid ;
this . previewLv = heroLv ;
this . previewPoolLv = poolLv ;
this . isSkillCard = isSkillCard ;
this . isEquipCard = isEquipCard ;
if ( this . sell_node ) this . sell_node . active = false ;
if ( this . close_node ) this . close_node . active = true ;
this . cacheLabels ( ) ;
this . refresh ( ) ;
}
/**
* 刷新显示:
* 1. 根据英雄等级切换高级 / 普通边框。
* 2. 若英雄 UUID 发生变化,重新加载 idle 动画。
* 3. 更新 AP / HP 数值标签。
*/
refresh() {
if ( this . isPreview ) {
this . refreshPreview ( ) ;
return ;
}
if ( ! this . model ) return ;
// ---- 卡牌等级显示 ----
if ( this . lv_node ) {
const currentLv = this . model . lv ? ? 1 ;
this . lv_node . string = ` Lv. ${ currentLv } ` ;
this . lv_node . color = getLvColor ( currentLv ) ;
}
// ---- 卡池等级标识 ----
const kindName = CKind [ CKind . Hero ] ;
if ( this . BG_node ) {
this . BG_node . children . forEach ( child = > {
child . active = ( child . name === kindName ) ;
const bg = child . getComponent ( CardBgComp ) ;
if ( bg ) child . active ? bg . apply ( this . model . base_card_lv ? ? this . model . card_lv ? ? 1 ) : bg . clear ( ) ;
} ) ;
}
// ---- 图标动画(仅在 UUID 变化时重新加载) ----
const heroUuid = this . model . hero_uuid ? ? 0 ;
if ( heroUuid !== this . iconHeroUuid ) {
this . iconHeroUuid = heroUuid ;
this . iconVisualToken += 1 ;
this . updateHeroAnimation ( this . icon_node , heroUuid , this . iconVisualToken ) ;
}
// ---- 名字标签 ----
if ( this . nameLabel ) {
this . nameLabel . string = this . model . hero_name ? ? "" ;
}
// ---- 技能信息标签 ----
if ( this . infoLabel ) {
const heroData = HeroInfo [ heroUuid ] ;
if ( heroData ) {
this . renderHeroInfoLines ( heroData ) ;
} else {
this . renderFallbackLines ( "" ) ;
}
}
// ---- 数值标签 ----
// 攻击力显示最终值(含计时 buff 修饰),实时反映药水等限时强化
if ( this . apLabel ) {
const currentAp = Math . max ( 0 , Math . floor ( this . model . getFinalAp ( ) ) ) ;
const baseAp = Math . max ( 0 , Math . floor ( this . model . base_ap ? ? 0 ) ) ;
this . apLabel . string = ` ${ currentAp } ` ;
if ( this . apPlusLabel ) {
const diff = currentAp - baseAp ;
if ( diff !== 0 ) {
this . apPlusLabel . string = diff > 0 ? ` (+ ${ diff } ) ` : ` ( ${ diff } ) ` ;
this . apPlusLabel . node . active = true ;
} else {
this . apPlusLabel . node . active = false ;
}
}
}
if ( this . hpLabel ) {
const currentHp = Math . max ( 0 , Math . floor ( this . model . getFinalHpMax ( ) ) ) ;
const baseHp = Math . max ( 0 , Math . floor ( this . model . base_hp ? ? 0 ) ) ;
this . hpLabel . string = ` ${ currentHp } ` ;
if ( this . hpPlusLabel ) {
const diff = currentHp - baseHp ;
if ( diff !== 0 ) {
this . hpPlusLabel . string = diff > 0 ? ` (+ ${ diff } ) ` : ` ( ${ diff } ) ` ;
this . hpPlusLabel . node . active = true ;
} else {
this . hpPlusLabel . node . active = false ;
}
}
}
if ( this . cdLabel ) {
const skillIds = this . model . getSkillIds ( ) ;
const displaySkillId = skillIds [ 1 ] ? ? skillIds [ 0 ] ? ? 0 ;
if ( displaySkillId ) {
const effectiveCd = this . model . getEffectiveSkillCd ( displaySkillId ) ;
this . cdLabel . string = effectiveCd > 0 ? ` ${ effectiveCd . toFixed ( 1 ) } s ` : "0s" ;
} else {
this . cdLabel . string = "0s" ;
}
}
}
/**
* 检测绑定的英雄实体是否仍存活。
* 通过检查 model 上的 ent 引用判断 ECS 实体是否已被回收。
*/
isModelAlive ( ) : boolean {
return ! ! ( this . model as any ) ? . ent ;
}
/**
* 逐条展示英雄能力说明: infos 每项一行(自动加 Lv 前缀, Lv 着色)。
* 复用预制体首条 Label, 其余按需创建/隐藏,避免频繁增删节点。
* 未配置 infos 时(如怪物)回退为单行 buildSkillDesc 描述。
* @param hero 英雄静态配置
*/
private renderHeroInfoLines ( hero : heroInfo ) {
if ( ! this . infoLabel ) return ;
const hasInfos = ! ! ( hero . infos ? . length ) ;
const count = hasInfos ? hero . infos ! . length : 1 ;
// 按需扩容 Label 池(克隆首条保持样式一致)
for ( let i = this . infoLineLabels . length ; i < count ; i ++ ) {
const node = new Node ( ` info_ ${ i } ` ) ;
node . layer = this . info_node . layer ;
this . info_node . addChild ( node ) ;
const label = node . addComponent ( Label ) ;
label . fontSize = this . infoLabel . fontSize ;
label . lineHeight = this . infoLabel . lineHeight ;
label . horizontalAlign = Label . HorizontalAlign . LEFT ;
label . verticalAlign = Label . VerticalAlign . TOP ;
label . color = this . infoLabel . color ;
this . infoLineLabels . push ( label ) ;
}
for ( let i = 0 ; i < this . infoLineLabels . length ; i ++ ) {
const label = this . infoLineLabels [ i ] ;
if ( i < count ) {
label . node . active = true ;
if ( hasInfos ) {
label . string = ` Lv ${ i + 1 } . ${ hero . infos ! [ i ] } ` ;
// 无富文本时用白色突出 Lv 前缀外的正文,保持简洁
label . color = this . infoLabel . color ;
} else {
label . string = buildSkillDesc ( hero ) ;
}
} else {
label . node . active = false ;
}
}
}
/**
* 单行文本展示(技能卡/无配置英雄):隐藏逐条 Label 池,只保留首行。
* @param text 展示文本
*/
private renderFallbackLines ( text : string ) {
if ( ! this . infoLabel ) return ;
this . infoLabel . node . active = true ;
this . infoLabel . string = text ;
for ( let i = 1 ; i < this . infoLineLabels . length ; i ++ ) {
this . infoLineLabels [ i ] . node . active = false ;
}
}
// ======================== 内部工具方法 ========================
/** 缓存 AP / HP Label 引用,避免每次刷新都遍历节点树 */
private cacheLabels() {
if ( ! this . nameLabel && this . Name_node ) {
this . nameLabel = this . Name_node . getComponent ( Label ) || this . Name_node . getComponentInChildren ( Label ) ;
}
if ( ! this . infoLabel && this . info_node ) {
this . infoLabel = this . info_node . getComponentInChildren ( Label ) ;
if ( ! this . infoLabel ) {
const child = new Node ( "info_0" ) ;
child . layer = this . info_node . layer ;
this . info_node . addChild ( child ) ;
this . infoLabel = child . addComponent ( Label ) ;
this . infoLabel . fontSize = 20 ;
this . infoLabel . lineHeight = 28 ;
this . infoLabel . horizontalAlign = Label . HorizontalAlign . LEFT ;
this . infoLabel . verticalAlign = Label . VerticalAlign . TOP ;
}
// 垂直 Layout 让多条 Label 自动逐行排布
let layout = this . info_node . getComponent ( Layout ) ;
if ( ! layout ) layout = this . info_node . addComponent ( Layout ) ;
layout . type = Layout . Type . VERTICAL ;
layout . resizeMode = Layout . ResizeMode . CONTAINER ;
layout . spacingY = 4 ;
this . infoLineLabels = [ this . infoLabel ] ;
}
if ( ! this . apLabel && this . ap_node ) {
this . apLabel = this . ap_node . getChildByName ( "val" ) ? . getComponent ( Label ) || null ;
this . apPlusLabel = this . ap_node . getChildByName ( "plus" ) ? . getComponent ( Label ) || null ;
}
if ( ! this . hpLabel && this . hp_node ) {
this . hpLabel = this . hp_node . getChildByName ( "val" ) ? . getComponent ( Label ) || null ;
this . hpPlusLabel = this . hp_node . getChildByName ( "plus" ) ? . getComponent ( Label ) || null ;
}
if ( ! this . cdLabel && this . cd_node ) {
this . cdLabel = this . cd_node . getChildByName ( "val" ) ? . getComponent ( Label ) || null ;
}
}
/**
* 按节点路径查找 Label 组件
* @param path 从当前节点开始的子节点名称路径数组
* @returns 找到的 Label 组件,或 null
*/
private findLabelByPath ( path : string [ ] ) : Label | null {
let current : Node | null = this . node ;
for ( let i = 0 ; i < path . length ; i ++ ) {
current = current ? . getChildByName ( path [ i ] ) ? ? null ;
if ( ! current ) return null ;
}
return current . getComponent ( Label ) || current . getComponentInChildren ( Label ) ;
}
// ======================== 英雄动画 ========================
/**
* 为英雄图标加载 idle 动画剪辑,提取首帧 SpriteFrame 静止展示。
* 不经 Animation 组件采样,零运行时开销;帧名各异也无需关心。
* @param node 图标节点
* @param uuid 英雄 UUID
* @param token 视觉令牌
*/
private updateHeroAnimation ( node : Node , uuid : number , token : number ) {
if ( ! node ) return ;
const sprite = node . getComponent ( Sprite ) || node . getComponentInChildren ( Sprite ) ;
if ( sprite ) sprite . spriteFrame = null ;
const hero = HeroInfo [ uuid ] ;
if ( ! hero ) return ;
const path = ` game/heros/hero/ ${ hero . path } /idle ` ;
resources . load ( path , AnimationClip , ( err , clip ) = > {
if ( err || ! clip ) return ;
if ( token !== this . iconVisualToken ) return ;
if ( this . isPreview ) {
if ( this . previewUuid !== uuid ) return ;
} else {
if ( ! this . model || this . model . hero_uuid !== uuid ) return ;
}
// 异步加载期间面板可能已关闭销毁
if ( ! node || ! node . isValid ) return ;
const target = node . getComponent ( Sprite ) || node . getComponentInChildren ( Sprite ) ;
if ( ! target || ! target . isValid ) return ;
// 帧动画数据存于 spriteFrame 轨道的 _channel._curve._values( 裸 SpriteFrame 数组)。
// 注: _values 为引擎私有字段,但本版本公开 values getter 反序列化后为空,只能读私有字段取首帧。
let firstFrame : SpriteFrame | undefined ;
for ( const track of clip . tracks ) {
const curve = ( track as unknown as { channel ? : { curve ? : { _values? : unknown [ ] } } } ) . channel ? . curve ;
const frame = curve ? . _values ? . [ 0 ] ;
if ( frame instanceof SpriteFrame ) {
firstFrame = frame ;
break ;
}
}
if ( firstFrame ) {
target . spriteFrame = firstFrame ;
}
} ) ;
}
/**
* 为技能图标加载静态图片。
* 图标来源优先级: config.icon > 按 trigger_type 自动取( Interval→SkillSet, Field→FieldSkillSet) 。
* @param node 图标节点
* @param config 卡牌配置
* @param token 视觉令牌
*/
private updateSkillAnimation ( node : Node , config : CardConfig , token : number ) {
if ( ! node ) return ;
const sprite = node . getComponent ( Sprite ) || node . getComponentInChildren ( Sprite ) ;
if ( ! sprite ) return ;
sprite . spriteFrame = null ;
// 优先使用卡牌自定义 icon; 未定义则按 trigger_type 自动取
let iconId : string | undefined = config . icon ;
if ( ! iconId ) {
if ( config . trigger_type === CardTriggerType . Interval ) {
iconId = config . skill ? SkillSet [ config . skill ] ? . icon : undefined ;
} else if ( config . trigger_type === CardTriggerType . Field ) {
const fieldUuid = config . field ? . [ 0 ] ? . uuid ;
iconId = fieldUuid ? FieldSkillSet [ fieldUuid ] ? . icon : undefined ;
}
}
if ( ! iconId ) return ;
// 与 CardComp / SCardComp 一致:仅信任全局缓存的 uiconsAtlas
if ( smc . uiconsAtlas ) {
const frame = smc . uiconsAtlas . getSpriteFrame ( iconId ) ;
if ( token === this . iconVisualToken ) {
sprite . spriteFrame = frame || null ;
}
}
}
// ======================== 交互 ========================
private bindEvents() {
this . sell_node ? . on ( Button . EventType . CLICK , this . onSellHero , this ) ;
this . close_node ? . on ( Button . EventType . CLICK , this . onClosePanel , this ) ;
// this.node.on(NodeEventType.TOUCH_END, this.onOpenIBox, this);
// 监听全局触摸结束:点击本体节点以外区域时关闭面板
input . on ( Input . EventType . TOUCH_END , this . onGlobalTouchEnd , this ) ;
}
private unbindEvents() {
if ( this . sell_node && this . sell_node . isValid ) {
this . sell_node . off ( Button . EventType . CLICK , this . onSellHero , this ) ;
}
if ( this . close_node && this . close_node . isValid ) {
this . close_node . off ( Button . EventType . CLICK , this . onClosePanel , this ) ;
}
// if (this.node && this.node.isValid) {
// this.node.off(NodeEventType.TOUCH_END, this.onOpenIBox, this);
// }
input . off ( Input . EventType . TOUCH_END , this . onGlobalTouchEnd , this ) ;
}
/**
* 全局触摸结束处理:点击落点不在本体节点包围盒内时关闭面板。
* Why: 弹窗常见的"点击空白处关闭"交互,用全局 input + 包围盒命中检测实现,
* 无需额外遮罩节点,也不会误关面板内部(含子按钮)的点击。
* @param event 全局触摸事件
*/
private onGlobalTouchEnd ( event : EventTouch ) {
if ( this . isClosing ) return ;
const transform = this . node . getComponent ( UITransform ) ;
if ( ! transform ) {
// 缺少 UITransform 时保守处理为关闭
this . onClosePanel ( ) ;
return ;
}
const worldRect = transform . getBoundingBoxToWorld ( ) ;
const uiPos = event . getUILocation ( ) ;
if ( ! worldRect . contains ( uiPos ) ) {
this . onClosePanel ( ) ;
}
}
/**
* 点击关闭按钮时关闭英雄信息面板
*/
private onClosePanel() {
if ( this . isClosing ) return ;
this . isClosing = true ;
oops . gui . remove ( UIID . HInfo ) ;
}
/**
* 点击面板时打开英雄详情弹窗( IBox) 。
* 传入英雄 UUID、等级和技能列表。
*/
private onOpenIBox() {
// if (this.isClosing) return;
// if (!this.model) return;
// if (!this.isModelAlive()) return;
// const heroUuid = this.model.hero_uuid ?? 0;
// if (!heroUuid || !HeroInfo[heroUuid]) return;
// const heroLv = Math.max(1, Math.floor(this.model.lv ?? 1));
// this.isClosing = true;
// oops.gui.remove(UIID.HInfo); // 打开 IBox 前关闭自身
// oops.gui.remove(UIID.IBox);
// oops.gui.open(UIID.IBox, {
// heroUuid,
// heroLv,
// skills: this.model.skills
// });
}
/**
* 出售英雄:通过 Hero.removeByEid 移除 ECS 实体,
* 并关闭详情弹窗。
*/
private onSellHero ( event? : Event ) {
if ( this . isClosing ) return ;
if ( ! this . eid ) return ;
const heroLv = Math . max ( 1 , Math . floor ( this . model ? . lv ? ? 1 ) ) ;
const removed = Hero . removeByEid ( this . eid ) ;
mLogger . log ( this . debugMode , "HInfoComp" , "onSellHero" , {
eid : this.eid ,
heroLv ,
isAlive : this.isModelAlive ( ) ,
removed
} ) ;
if ( ! removed ) return ;
// 使用统一经济管理入口出售英雄(按等级计算卖价)
MissionEconomy . executeSellHero ( heroLv ) ;
// 派发英雄出售事件,通知 MissionCardComp 更新场上英雄数量
oops . message . dispatchEvent ( GameEvent . HeroSell , { eid : this.eid } ) ;
this . isClosing = true ;
oops . gui . remove ( UIID . HInfo ) ;
}
/** ECS 组件移除时的释放钩子:重置绑定状态 */
reset() {
this . iconVisualToken = 0 ;
this . iconHeroUuid = 0 ;
this . model = null ;
this . eid = 0 ;
this . isPreview = false ;
this . previewUuid = 0 ;
this . previewLv = 1 ;
this . previewPoolLv = 1 ;
}
}