feat: 新增技能装备详情弹窗IBox,替换旧英雄信息弹窗逻辑

1. 重构IBoxComp组件,支持技能和装备详情展示
2. 为EquipBoxComp和SkillBoxComp添加长按弹窗功能
3. 更新UI配置,修正IBox弹窗层级
4. 新增富文本工具函数生成技能装备详情文案
This commit is contained in:
pan
2026-07-31 10:55:46 +08:00
parent bdb4602fe6
commit 8c421c42d4
11 changed files with 2008 additions and 4222 deletions

View File

@@ -1,75 +1,68 @@
/**
* @file IBoxComp.ts
* @description 英雄信息弹窗组件IBoxUI 视图层)
* @description 技能/装备详情弹窗组件IBoxUI 视图层)
*
* 职责:
* 1. 作为全局弹窗,展示某个英雄的 **详细技能信息**。
* 2. 通过 onAdded(args) 接收英雄 UUID、等级和运行时技能数据。
* 3. 自动计算技能行数并动态调整弹窗背景高度和名称位置
* 1. 作为详情弹窗,展示单个技能或装备的 **功能描述详情**。
* 2. 通过 init(args) 接收技能 UUID、装备 UUID 或运行时技能数据。
* 3. 使用单个 RichText 以 BBCode 展示名称蓝色、CD/持续时间(亮绿色)、结构化效果描述
* 4. 点击弹窗任意区域关闭自身。
*
* 关键设计:
* - Line1~Line5 为预设的 5 行技能节点,按需显示/隐藏
* - 每行包含技能名、等级、CD、描述文本和类型图标近战/远程/辅助)。
* - 背景高度 = baseHeight + (行数 - 1) × extraLineHeight
* - 若传入 args.skills运行时技能优先使用否则回退到英雄静态配置
* - 复用 HeroSkillDesc 的结构化模板渲染,数值实时从配置读取,杜绝漂移
* - 技能展示名称、等级、CD、效果描述SkillDescSet 模板)。
* - 装备展示名称、全部驻场光环词条FieldDescSet 模板)
* - 不走 oops.gui由调用方手动实例化并添加到场景通过 init() 初始化
*
* 依赖:
* - HeroInfoheroSet—— 英雄静态配置
* - SkillSet / ITypeSkillSet—— 技能静态配置
* - UIID —— 在 oops.gui 系统中注册的弹窗 ID
* - HeroSkillDescbuildSingleSkillRich / buildEquipRich—— 描述生成
* - SkillSet / FieldSkillSetSkillSet—— 技能/光环静态配置
* - EquipSetfindEquipByUuid—— 装备静态配置
*/
import { _decorator, Label, Node, NodeEventType, UITransform } from "cc";
import { _decorator, Label, NodeEventType, RichText } 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 } from "../common/config/heroSet";
import { IType, SkillSet } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { SkillSet, SkillOverrides } from "../common/config/SkillSet";
import { buildEquipRich, buildSingleSkillRich } from "../common/config/HeroSkillDesc";
import { findEquipByUuid } from "../common/config/EquipSet";
const { ccclass, property } = _decorator;
/** IBox 初始化参数 */
export interface IBoxInitArgs {
/** SkillSet 技能 UUID技能详情模式 */
skillUuid?: number;
/** 技能等级(默认 1 */
skillLv?: number;
/** 技能覆写参数(如药水卡自定义档位) */
overrides?: SkillOverrides;
/** EquipSet 装备 UUID装备详情模式 */
equipUuid?: number;
/** 运行时技能数据(运行时技能详情模式,取第一个技能展示) */
skills?: Record<number, { uuid: number; lv: number; cd: number; ccd: number }>;
}
/**
* IBoxComp —— 英雄信息弹窗视图组件
* IBoxComp —— 技能/装备详情弹窗视图组件
*
* 通过 oops.gui.open(UIID.IBox, { heroUuid, heroLv, skills? }) 打开
* 展示英雄技能列表,支持最多 5 行。
* 由调用方通过 instantiate + addChild 手动实例化,然后调用 init(args) 初始化
* 示例:
* const node = instantiate(this.iboxPrefab);
* this.node.addChild(node);
* node.getComponent(IBoxComp).init({ skillUuid: 6301, anchorPos: this.node.worldPosition });
*/
@ccclass('IBoxComp')
@ecs.register('IBoxComp', false)
export class IBoxComp extends CCComp {
// ======================== 编辑器绑定节点5 行技能) ========================
@property(Node)
Line1: Node = null!
@property(Node)
Line2: Node = null!
@property(Node)
Line3: Node = null!
@property(Node)
Line4: Node = null!
@property(Node)
Line5: Node = null!
// ======================== 布局常量 ========================
/** 弹窗背景基础高度(只有 1 行技能时的高度) */
private readonly baseHeight: number = 100;
/** 每增加一行技能,背景增加的高度 */
private readonly extraLineHeight: number = 50;
/** 英雄名称标签的基准 Y 坐标 */
private readonly nameBaseY: number = 50;
/** 每增加一行技能,名称标签额外上移的 Y 偏移 */
private readonly nameExtraLineOffsetY: number = 25;
// ======================== 编辑器绑定节点 ========================
@property(RichText)
massage_node: RichText = null!
/**
* ECS 实体挂载回调:接收外部传入的英雄参数并渲染
* @param args.heroUuid 英雄 UUID
* @param args.heroLv 英雄当前等级
* @param args.skills (可选)运行时技能数据,若无则使用英雄静态配置
* 初始化并渲染弹窗内容
* @param args 见 IBoxInitArgs
*/
onAdded(args: {
heroUuid?: number;
heroLv?: number;
skills?: Record<number, { uuid: number; lv: number; cd: number; ccd: number }>;
}) {
this.renderHeroInfo(args);
public init(args: IBoxInitArgs) {
this.render(args);
}
/** 绑定点击关闭事件 */
@@ -93,99 +86,73 @@ export class IBoxComp extends CCComp {
// ======================== 渲染逻辑 ========================
/**
* 主渲染方法:解析英雄数据并填充技能行
*
* 流程:
* 1. 从 args 中取英雄 UUID 和等级。
* 2. 优先使用运行时技能数据args.skills否则回退英雄静态配置。
* 3. 将每个技能映射为 { text, iType },过滤无效项。
* 4. 调用 applyLineData 渲染到 Line1~Line5。
* 主渲染方法:根据 args 模式分发到技能详情或装备详情
*/
private renderHeroInfo(args: {
heroUuid?: number;
heroLv?: number;
skills?: Record<number, { uuid: number; lv: number; cd: number; ccd: number }>;
}) {
const heroUuid = Math.floor(args?.heroUuid ?? 0);
const heroLv = Math.max(1, Math.floor(args?.heroLv ?? 1));
const hero = HeroInfo[heroUuid];
if (!hero) {
this.setHeroName("");
this.applyLineData([{ text: "暂无技能信息" }]);
private render(args: IBoxInitArgs) {
// 装备详情模式
if (args?.equipUuid !== undefined) {
this.renderEquip(args.equipUuid);
return;
}
this.setHeroName(hero.name);
// 运行时技能 vs 静态配置
const runtimeSkills = args?.skills ? Object.values(args.skills) : [];
const sourceSkills = runtimeSkills.length > 0 ? runtimeSkills : Object.values(hero.skills ?? {});
// 将每个技能转换为显示数据
const lineData = sourceSkills.map(skill => {
const skillId = Math.floor(skill?.uuid ?? 0);
if (!skillId) return null;
const config = SkillSet[skillId];
if (!config) return null;
// 运行时技能直接使用其等级;静态配置的技能等级需叠加英雄等级修正
const runtimeLv = runtimeSkills.length > 0 ? Math.max(0, Math.floor(skill.lv ?? 0)) : Math.max(0, Math.floor((skill.lv ?? 1) + heroLv - 2));
const cd = Number(skill?.cd ?? 0);
return {
text: `${config.name} Lv.${runtimeLv} CD:${cd}s ${config.info}`,
iType: config.IType
};
}).filter(item => !!item) as Array<{ text: string; iType: IType }>;
this.applyLineData(lineData.length > 0 ? lineData : [{ text: "暂无技能信息" }]);
}
/**
* 将技能数据应用到 Line1~Line5
* 1. 按顺序填充文本和类型图标。
* 2. 超出的行隐藏。
* 3. 根据显示行数调整弹窗高度和名称位置。
*
* @param skillLines 技能行数据数组
*/
private applyLineData(skillLines: Array<{ text: string; iType?: IType }>) {
const lines = [this.Line1, this.Line2, this.Line3, this.Line4, this.Line5];
const showCount = Math.max(1, Math.min(lines.length, skillLines.length));
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line) continue;
const active = i < showCount;
line.active = active;
if (!active) continue;
const data = skillLines[i];
const text = data?.text ?? "";
// 查找技能文本节点并设置内容
const noteNode = line.getChildByName("note");
const label = noteNode?.getComponent(Label) || noteNode?.getComponentInChildren(Label) || line.getComponentInChildren(Label);
if (label) label.string = text;
// 更新类型图标
this.updateLineTypeIcon(line, data?.iType);
// 运行时技能详情模式(取第一个技能)
if (args?.skills) {
const first = Object.values(args.skills)[0];
if (first) {
this.renderSkill(first.uuid, first.lv);
return;
}
}
// 动态调整弹窗背景高度
const targetHeight = this.baseHeight + Math.max(0, showCount - 1) * this.extraLineHeight;
this.updateIBoxHeight(targetHeight);
// 动态调整名称位置
this.updateNamePosition(showCount);
// 技能详情模式
if (args?.skillUuid !== undefined) {
this.renderSkill(args.skillUuid, args.skillLv ?? 1, args.overrides);
return;
}
this.setHeroName("");
this.setRichText("暂无信息");
}
/**
* 设置弹窗背景 Bg 节点的高度
* @param height 目标高度
* 渲染技能详情名称、等级、CD、结构化效果描述。
* @param skillUuid SkillSet 技能 UUID
* @param lv 技能等级
* @param overrides 可选覆写参数
*/
private updateIBoxHeight(height: number) {
const bgNode = this.node.getChildByName("Bg");
const bgTransform = bgNode?.getComponent(UITransform);
if (bgTransform) {
bgTransform.setContentSize(bgTransform.contentSize.width, height);
private renderSkill(skillUuid: number, lv: number, overrides?: SkillOverrides) {
const skill = SkillSet[skillUuid];
if (!skill) {
this.setHeroName("");
this.setRichText("未知技能");
return;
}
this.setHeroName(skill.name);
this.setRichText(buildSingleSkillRich(skillUuid, overrides, lv));
}
/**
* 渲染装备详情:名称、全部驻场光环词条。
* @param equipUuid EquipSet 装备 UUID
*/
private renderEquip(equipUuid: number) {
const equip = findEquipByUuid(equipUuid);
if (!equip) {
this.setHeroName("");
this.setRichText("未知装备");
return;
}
this.setHeroName(equip.name);
this.setRichText(buildEquipRich(equip.field ?? []));
}
/** 将 BBCode 文本写入 RichText */
private setRichText(text: string) {
if (this.massage_node && this.massage_node.isValid) {
this.massage_node.string = text;
}
}
/**
* 设置英雄名称标签
* @param name 英雄名称
* 设置标题名称标签
* @param name 技能/装备名称
*/
private setHeroName(name: string) {
const bgNode = this.node.getChildByName("Bg");
@@ -197,37 +164,10 @@ export class IBoxComp extends CCComp {
}
}
/**
* 根据显示行数调整名称节点的 Y 坐标。
* 行数越多,名称越往上移,保持视觉居中。
*
* @param showCount 当前显示的技能行数
*/
private updateNamePosition(showCount: number) {
const bgNode = this.node.getChildByName("Bg");
const nameNode = bgNode?.getChildByName("name");
if (!nameNode) return;
const targetY = this.nameBaseY + Math.max(0, showCount - 1) * this.nameExtraLineOffsetY;
const current = nameNode.position;
nameNode.setPosition(current.x, targetY, current.z);
}
/**
* 更新技能行的类型图标(互斥显示)。
* @param line 技能行节点
* @param iType 技能类型(近战 / 远程 / 辅助)
*/
private updateLineTypeIcon(line: Node, iType?: IType) {
const meleeNode = line.getChildByName("Melee");
const remoteNode = line.getChildByName("remote");
const supportNode = line.getChildByName("support");
if (meleeNode) meleeNode.active = iType === IType.Melee;
if (remoteNode) remoteNode.active = iType === IType.remote;
if (supportNode) supportNode.active = iType === IType.support;
}
/** 点击弹窗任意区域关闭自身 */
private onTapClose() {
oops.gui.removeByNode(this.node);
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}