296 lines
9.7 KiB
TypeScript
296 lines
9.7 KiB
TypeScript
/**
|
||
* @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 => {
|
||
|
||
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()
|
||
}
|
||
}
|