/** * @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 / HeroList(heroSet)—— 英雄静态配置与全量 UUID 列表 * - CardLiteComp —— 轻量卡片组件 * - buildSkillDesc(HeroSkillDesc)—— 技能描述生成器 */ import { _decorator, Animation, AnimationClip, Label, Node, Prefab, Sprite, UITransform, 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 { oops } from "db://oops-framework/core/Oops"; import { mLogger } from "../common/Logger"; import { HeroInfo, HeroList } from "../common/config/heroSet"; import { buildSkillDesc } from "../common/config/HeroSkillDesc"; import { CardLiteComp } from "./CardLiteComp"; const { property, ccclass } = _decorator; @ccclass('HerosListComp') @ecs.register('HerosListComp', false) export class HerosListComp extends CCComp { // ======================== 编辑器绑定节点 ======================== @property(Node) hero_icon = null! @property(Node) 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! @property(Node) lv_node = null! @property(Node) type_node = null! // ======================== 运行时状态 ======================== huuid: number = 0 private iconVisualToken: number = 0 private selectNode: Node | null = null debugMode: boolean = false start() { this.initCardList() if (HeroList.length > 0) { this.onCardSelect(HeroList[0]) } } protected onEnable(): void { if (this.cards_node && this.cards_node.children.length > 0) { this.onCardSelect(this.huuid || HeroList[0]) } } closeHeros() { this.node.active = false } // ======================== 卡片列表 ======================== private initCardList() { mLogger.log(this.debugMode, "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(this.debugMode, "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(this.debugMode, "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(this.debugMode, "HerosListComp", "initCardList done", { totalChildren: this.cards_node.children.length, }) this.updateContentSize() } private updateContentSize() { const total = this.cards_node.children.length if (total === 0) return const cols = 4 const rows = Math.ceil(total / cols) const cardH = 200 const spacingY = 10 const contentH = Math.max(1000, rows * (cardH + spacingY) + 50) const uiTrans = this.cards_node.getComponent(UITransform) if (uiTrans) { uiTrans.setContentSize(uiTrans.width, contentH) } mLogger.log(this.debugMode, "HerosListComp", "updateContentSize", { total, cols, rows, contentH, }) } 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 } } reset() { this.node.destroy() } }