feat(map): 重构英雄图鉴页面,实现完整的英雄卡片展示与详情功能
1. 重写HerosListComp组件,实现卡片动态生成、选中高亮、详情更新逻辑 2. 完善CardLiteComp组件,支持英雄卡渲染、点击交互与动画加载 3. 清理冗余的预制体绑定代码,修复异步加载竞态问题 4. 添加详细的日志与注释,优化可维护性
This commit is contained in:
@@ -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 / HeroList(heroSet)—— 英雄静态配置与全量 UUID 列表
|
||||
* - CardLiteComp —— 轻量卡片组件
|
||||
* - buildSkillDesc(HeroSkillDesc)—— 技能描述生成器
|
||||
*/
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user