feat(map): 重构英雄图鉴页面,实现完整的英雄卡片展示与详情功能

1.  重写HerosListComp组件,实现卡片动态生成、选中高亮、详情更新逻辑
2.  完善CardLiteComp组件,支持英雄卡渲染、点击交互与动画加载
3.  清理冗余的预制体绑定代码,修复异步加载竞态问题
4.  添加详细的日志与注释,优化可维护性
This commit is contained in:
panw
2026-05-27 15:24:40 +08:00
parent ff2785680d
commit 2f27bb7035
3 changed files with 650 additions and 586 deletions

View File

@@ -2,83 +2,275 @@
* @file HerosListComp.ts
* @description 英雄图鉴弹出页面UI 视图层)
*
* 职责:
* 1. 以卡片列表形式展示所有可用英雄(使用 CardLiteComp 预制体)。
* 2. 点击卡片选中英雄,右侧详情面板显示 idle 动画、名称、AP、HP、CD、技能描述。
* 3. 支持卡池等级筛选(全部 / Lv1 / Lv2 / Lv3
*
* 关键设计:
* - cards_node 为卡片容器,通过 instantiate(card_lite_prefab) 动态生成卡片。
* - 选中状态通过 selectNode 高亮管理,同一时间只有一张卡片高亮。
* - hero_icon 使用 Animation + iconVisualToken 机制防止异步加载竞态。
*
* 依赖:
* - HeroInfo / HeroListheroSet—— 英雄静态配置与全量 UUID 列表
* - CardLiteComp —— 轻量卡片组件
* - buildSkillDescHeroSkillDesc—— 技能描述生成器
*/
import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources, tween, Vec3, Widget, Prefab } from "cc";
import { _decorator, Animation, AnimationClip, Label, Node, Prefab, Sprite, Widget, instantiate, resources } 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, HeroList } from "../common/config/heroSet";
import { IType, SkillSet } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { mLogger } from "../common/Logger";
import { UIID } from "../common/config/GameUIConfig";
import { HeroInfo, HeroList } from "../common/config/heroSet";
import { buildSkillDesc } from "../common/config/HeroSkillDesc";
import { CardLiteComp } from "./CardLiteComp";
const {property, ccclass } = _decorator;
const { property, ccclass } = _decorator;
/**
* HerosListComp —— 英雄图鉴轮播视图组件
*
* 在任务主页展示所有可用英雄玩家可点击切换当前选中英雄的名称、AP、HP、CD、技能信息
*/
@ccclass('HerosListComp')
@ecs.register('HerosListComp', false)
export class HerosListComp extends CCComp {
// ======================== 编辑器绑定节点 ========================
/** 当前英雄 idle 图标节点 */
@property(Node)
hero_icon=null!
/** 攻击力标签节点 */
@property(Node)
ap_node=null!
/** 生命值标签节点 */
@property(Node)
hp_node=null!
/** 冷却时间标签节点 */
@property(Node)
cd_node=null!
/** 技能信息容器节点(包含 Line1~Line5 子节点) */
@property(Node)
info_node=null!
/** 英雄名称标签节点 */
@property(Node)
name_node=null!
hero_icon = null!
/** 英雄图鉴卡容器节点 */
@property(Node)
cards_node=null!
ap_node = null!
@property(Node)
hp_node = null!
@property(Node)
cd_node = null!
@property(Node)
info_node = null!
@property(Node)
name_node = null!
@property(Node)
cards_node = null!
/** 英雄图鉴卡预制体 */
@property(Prefab)
card_lite_prefab=null!
card_lite_prefab = null!
@property(Node)
lv_node = null!
@property(Node)
type_node = null!
// ======================== 运行时状态 ========================
/** 当前选中英雄在 HeroList 中的索引 */
huuid:number=null!
/** 当前选中英雄在 HeroList 数组中的下标 */
/** 调试日志开关 */
debugMode: boolean = false;
huuid: number = 0
private iconVisualToken: number = 0
private selectNode: Node | null = null
debugMode: boolean = false
onLoad() {
}
/** 预留:弹窗打开时接收参数 */
onAdded(args: any) {
}
/** 关闭英雄图鉴弹窗 */
closeHeros(){
start() {
this.initCardList()
if (HeroList.length > 0) {
this.onCardSelect(HeroList[0])
}
}
closeHeros() {
oops.gui.remove(UIID.Heros)
}
start() {
// ======================== 卡片列表 ========================
private initCardList() {
mLogger.log(true, "HerosListComp", "initCardList start", {
hasCardsNode: !!this.cards_node,
hasPrefab: !!this.card_lite_prefab,
heroListLen: HeroList.length,
})
if (!this.cards_node || !this.card_lite_prefab) return
mLogger.log(true, "HerosListComp", "cards_node info", {
name: this.cards_node.name,
active: this.cards_node.active,
childrenCount: this.cards_node.children.length,
pos: this.cards_node.position.toString(),
scale: this.cards_node.scale.toString(),
parentName: this.cards_node.parent?.name || "none",
})
this.cards_node.removeAllChildren()
for (const uuid of HeroList) {
const hero = HeroInfo[uuid]
if (!hero) continue
const cardNode = instantiate(this.card_lite_prefab)
cardNode.name = `card_${uuid}`
mLogger.log(true, "HerosListComp", `card instantiated ${uuid}`, {
cardActive: cardNode.active,
cardPos: cardNode.position.toString(),
cardScale: cardNode.scale.toString(),
cardChildren: cardNode.children.map(c => c.name),
cardWidth: cardNode.getComponent("cc.UITransform")?.width,
cardHeight: cardNode.getComponent("cc.UITransform")?.height,
})
const comp = cardNode.getComponent(CardLiteComp) || cardNode.addComponent(CardLiteComp)
comp.setHeroByUuid(uuid, hero.pool_lv ?? 1)
comp.onClickCallback = (cardComp: CardLiteComp) => {
this.onCardSelect(uuid)
this.highlightCard(cardNode)
}
this.cards_node.addChild(cardNode)
}
mLogger.log(true, "HerosListComp", "initCardList done", {
totalChildren: this.cards_node.children.length,
})
}
private highlightCard(cardNode: Node) {
if (this.selectNode) {
const oldWidget = this.selectNode.getComponent(Widget)
if (oldWidget) oldWidget.enabled = false
this.selectNode.setScale(1, 1, 1)
}
this.selectNode = cardNode
cardNode.setScale(1.1, 1.1, 1)
}
private onCardSelect(uuid: number) {
this.huuid = uuid
this.updateHeroDetail(uuid)
}
// ======================== 详情面板 ========================
private updateHeroDetail(uuid: number) {
const hero = HeroInfo[uuid]
if (!hero) return
const heroLv = Math.max(1, Math.floor(hero.lv ?? 1))
const suffix = heroLv >= 2 ? "★".repeat(heroLv - 1) : ""
this.setLabelText(this.name_node, `${suffix}${hero.name || ""}${suffix}`)
this.setLabelText(this.ap_node, `${(hero.ap ?? 0) * heroLv}`)
this.setLabelText(this.hp_node, `${(hero.hp ?? 0) * heroLv}`)
this.updateCdDisplay(hero)
if (this.info_node) {
const desc = buildSkillDesc(hero)
const infoLabel = this.info_node.getChildByName("info")?.getComponent(Label)
|| this.info_node.getComponent(Label)
|| this.info_node.getComponentInChildren(Label)
if (infoLabel) infoLabel.string = desc || hero.info || ""
}
this.updateLvDisplay(hero)
this.updateTypeDisplay(hero)
this.updateHeroAnimation(uuid)
}
private updateCdDisplay(hero: typeof HeroInfo[number]) {
if (!this.cd_node) return
const skillKeys = Object.keys(hero.skills)
if (skillKeys.length === 0) return
const firstSkill = hero.skills[Number(skillKeys[0])]
if (firstSkill) {
this.setLabelText(this.cd_node, `${firstSkill.cd}s`)
}
}
private updateLvDisplay(hero: typeof HeroInfo[number]) {
if (!this.lv_node) return
const cardLvStr = `lv${hero.pool_lv ?? 1}`
this.lv_node.active = true
this.lv_node.children.forEach(child => {
if (child.name === "light") {
child.active = false
} else if (child.name === "bg") {
child.active = true
} else {
child.active = (child.name === cardLvStr)
}
})
const widget = this.lv_node.getComponent(Widget)
if (widget) widget.updateAlignment()
this.lv_node.children.forEach(child => {
const childWidget = child.getComponent(Widget)
if (childWidget) childWidget.updateAlignment()
})
}
private updateTypeDisplay(hero: typeof HeroInfo[number]) {
if (!this.type_node) return
this.type_node.active = true
const typeStr = `${hero.type ?? 0}`
this.type_node.children.forEach(child => {
child.active = (child.name === typeStr)
})
}
// ======================== 英雄动画 ========================
private updateHeroAnimation(uuid: number) {
if (!this.hero_icon) return
const hero = HeroInfo[uuid]
if (!hero) return
const sprite = this.hero_icon.getComponent(Sprite) || this.hero_icon.getComponentInChildren(Sprite)
if (sprite) sprite.spriteFrame = null
const anim = this.hero_icon.getComponent(Animation) || this.hero_icon.addComponent(Animation)
this.clearAnimationClips(anim)
this.iconVisualToken += 1
const token = this.iconVisualToken
const path = `game/heros/hero/${hero.path}/idle`
resources.load(path, AnimationClip, (err, clip) => {
if (err || !clip) {
mLogger.log(this.debugMode, "HerosListComp", `load hero animation failed ${uuid}`, err)
return
}
if (token !== this.iconVisualToken) return
this.clearAnimationClips(anim)
anim.addClip(clip)
anim.play("idle")
})
}
private clearAnimationClips(anim: Animation) {
const clips = anim.clips
if (clips && clips.length > 0) {
for (let i = clips.length - 1; i >= 0; i--) {
const clip = clips[i]
if (clip) anim.removeClip(clip, true)
}
}
}
// ======================== UI 工具 ========================
private setLabelText(node: Node, text: string) {
if (!node) return
const label = node.getComponent(Label) || node.getComponentInChildren(Label)
if (label) {
label.string = text
}
}
/** ECS 组件移除时销毁节点 */
reset() {
this.node.destroy();
this.node.destroy()
}
}