Files
pixelheros/assets/script/game/map/SinfoComp.ts
panFD 10a6d18836 refactor(skill-text): 重构技能文案系统,拆分英雄/卡牌语境
1.  将原HeroSkillDesc拆分为CardSkillText(卡牌/道具通用)和HeroSkillText(英雄专属)
2.  移除废弃的HeroSkillDesc文件与相关引用
3.  统一技能文案生成逻辑,修复数值漂移问题
4.  调整英雄配置中技能解锁等级与分档规则
5.  优化技能弹窗UI与参数传递逻辑
2026-08-28 08:36:39 +08:00

403 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file SinfoComp.ts
* @description 技能信息弹窗组件(UI 视图层)
*
* 职责:
* 1. 展示技能小图标(SSkillBoxComp)点击后的详细信息弹窗。
* 2. 展示技能名称、当前等级、图标、主功能说明。
* 3. 展示 1 级 / 2 级 / 3 级 激活状态(已激活显示"已激活",未激活显示"x级激活")。
* 4. 点击弹窗外部区域关闭。
*
* 使用(oops.gui 模块):
* oops.gui.open(UIID.SInfo, {
* uuid: 8701, // 卡牌/技能 UUID
* skill_lv: 1, // 当前等级
* card_icon: "xxx", // 图标ID(可选,缺省自动解析)
* skill_name: "战后恢复", // 名称(可选)
* field: [...], // 驻场词条(装备卡)
* trigger_type: 3, // 触发类型
* hero_uuid: 5001, // 所属英雄(英雄图鉴模式:按英雄技能分档描述渲染)
* });
*
* 依赖:
* - oops.gui(UI 生命周期)—— open / removeByNode
* - SkillSet / FieldSkillSet(技能/光环静态配置)
* - SkillCardList(卡牌静态配置)
* - CardSkillText(buildCardDescRich 富文本描述生成,卡牌模式)
* - HeroSkillText(buildHeroSkillTierLines 技能分档描述,英雄技能模式,
* 覆盖普攻/大招、触发技能、驻场光环词条与复活)
*/
import { _decorator, Node, Label, RichText, NodeEventType, Sprite, Color, input, Input, EventTouch, UITransform, Vec3 } 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 { CardTriggerType, CardConfig } from "../common/config/CardSet";
import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
import { SkillCardList } from "../common/config/SCardSet";
import { FieldEntry } from "../common/config/heroSet";
import { buildCardDescRich } from "../common/config/CardSkillText";
import { buildHeroSkillTierLines, HeroSkillTierLine } from "../common/HeroSkillText";
import { smc } from "../common/SingletonModuleComp";
const { ccclass, property } = _decorator;
/** SInfo 弹窗初始化参数 */
export interface SInfoParams {
/** 卡牌/技能 UUID */
uuid: number;
/** 当前等级 */
skill_lv: number;
/** 图标ID(可选,优先级最高) */
card_icon?: string;
/** 技能名称(可选) */
skill_name?: string;
/** 驻场词条(装备卡专用) */
field?: FieldEntry[];
/** 触发类型 */
trigger_type?: CardTriggerType;
/** 所属英雄 UUID(英雄图鉴模式:按英雄技能分档描述渲染,优先级高于卡牌逻辑) */
hero_uuid?: number;
}
/** 等级提升信息(用于 2 级 / 3 级面板) */
interface LvUpgradeInfo {
/** 是否已激活 */
unlocked: boolean;
/** 等级标签文本(如 "Lv.2") */
lvText: string;
/** 激活状态文本("已激活" 或 "2级激活") */
statusText: string;
}
@ccclass('SinfoComp')
@ecs.register('SinfoComp', false)
export class SinfoComp extends CCComp {
// ======================== 编辑器绑定节点 ========================
/** 技能图标节点 */
@property({ type: Node, displayName: "技能图标" })
private icon_node: Node = null;
/** 技能名称标签 */
@property({ type: Label, displayName: "技能名称" })
private name_label: Label = null;
/** 当前等级标签 */
@property({ type: Label, displayName: "当前等级" })
private lv_label: Label = null;
/** 主功能说明(富文本) */
@property({ type: RichText, displayName: "主功能说明" })
private main_info: RichText = null;
/** 1级激活状态标签 */
@property({ type: Label, displayName: "1级激活状态" })
private main_status: Label = null;
/** 2级激活状态标签 */
@property({ type: Label, displayName: "2级激活状态" })
private lv2_status: Label = null;
/** 2级提升说明(富文本) */
@property({ type: RichText, displayName: "2级提升说明" })
private lv2_desc: RichText = null;
/** 3级激活状态标签 */
@property({ type: Label, displayName: "3级激活状态" })
private lv3_status: Label = null;
/** 3级提升说明(富文本) */
@property({ type: RichText, displayName: "3级提升说明" })
private lv3_desc: RichText = null;
/** 关闭按钮节点 */
@property({ type: Node, displayName: "关闭按钮" })
private close_btn: Node = null;
// ======================== 运行时状态 ========================
/** 弹窗参数 */
private params: SInfoParams | null = null;
// ======================== 生命周期 ========================
onLoad() {
// 点击弹窗自身:阻止冒泡,由全局触摸判定触点位置决定是否关闭
this.node.on(NodeEventType.TOUCH_END, this.onSelfTouch, this);
// 关闭按钮:编辑器未绑定时按节点名 "close" 兜底查找
if (!this.close_btn) {
this.close_btn = this.node.getChildByName("close");
}
this.close_btn?.on(NodeEventType.TOUCH_END, this.onCloseClick, this);
}
onDestroy() {
super.onDestroy();
if (this.node && this.node.isValid) {
this.node.off(NodeEventType.TOUCH_END, this.onSelfTouch, this);
}
if (this.close_btn && this.close_btn.isValid) {
this.close_btn.off(NodeEventType.TOUCH_END, this.onCloseClick, this);
}
// 注销全局触摸监听
input.off(Input.EventType.TOUCH_END, this.onGlobalTouch, this);
}
/**
* oops.gui 添加到舞台回调(框架约定入口)
* @param params 见 SInfoParams
*/
onAdded(params: SInfoParams) {
this.params = params;
this.render();
// 延迟一帧注册全局触摸,避免本次点击立即触发关闭
this.scheduleOnce(() => {
input.on(Input.EventType.TOUCH_END, this.onGlobalTouch, this);
}, 0);
}
/** 点击弹窗自身:阻止冒泡,不关闭 */
private onSelfTouch(event: EventTouch) {
event.propagationStopped = true;
}
/** 关闭按钮点击 */
private onCloseClick(event: EventTouch) {
// 阻止冒泡,避免同时触发 onSelfTouch / onGlobalTouch 双重关闭
event.propagationStopped = true;
oops.audio.playEffect("music/button");
this.close();
}
/** 全局触摸结束:触点落在弹窗包围盒外才关闭 */
private onGlobalTouch(event: EventTouch) {
if (!this.node || !this.node.isValid) return;
// 触点 UI 坐标 → 弹窗局部坐标,判定是否在弹窗内(convertToNodeSpaceAR 需要 Vec3)
const uit = this.node.getComponent(UITransform);
if (!uit) return;
const uiPos = event.getUILocation();
const localPos = uit.convertToNodeSpaceAR(new Vec3(uiPos.x, uiPos.y, 0));
const halfW = uit.contentSize.width * uit.anchorX;
const halfH = uit.contentSize.height * uit.anchorY;
const inBox =
localPos.x >= -halfW && localPos.x <= uit.contentSize.width - halfW &&
localPos.y >= -halfH && localPos.y <= uit.contentSize.height - halfH;
if (!inBox) {
this.close();
}
}
/** 关闭弹窗 */
private close() {
oops.gui.removeByNode(this.node);
}
// ======================== 渲染逻辑 ========================
/**
* 英雄技能模式渲染(文本二:单个技能各阶段说明)。
* - main_info:技能 slv1 完整描述
* - lv2_desc:slv2 相对 slv1 的提升;lv3_desc:slv3 及以后档(合并展示)
* - lv2/lv3_status:按英雄局外等级(smc.getHeroOuterLv)对比档位解锁等级判定激活
* @returns true 已渲染(拦截通用卡牌逻辑);false 技能无分档数据,回退通用卡牌逻辑
*/
private renderHeroSkill(p: SInfoParams): boolean {
const tiers = buildHeroSkillTierLines(p.hero_uuid!, p.uuid);
if (tiers.length === 0) return false;
// 图标 / 名称 / 当前技能等级与通用逻辑一致(skill_lv 传的就是当前生效 s_lv)
this.renderIcon(p);
this.setLabel(this.name_label, this.resolveName(p));
this.setLabel(this.lv_label, `Lv.${p.skill_lv}`);
// 技能 1 级:完整能力描述
if (this.main_info && this.main_info.isValid) {
this.main_info.string = tiers[0].text || "暂无描述";
}
// 英雄当前局外等级:激活判定基准
const heroLv = Math.max(1, smc.getHeroOuterLv(p.hero_uuid!));
// 技能 1 级:激活状态与 2/3 级面板同一表示方式(描述已在 main_info 展示,此处仅渲染状态)
this.renderLvPanelFromTier(tiers[0], heroLv, this.main_status, null);
// 技能 2 级 / 3 级(3 级以后档位合并进 3 级栏,多行展示)
const lv2 = tiers.find(t => t.sLv === 2);
const lv3Plus = tiers.filter(t => t.sLv >= 3);
this.renderLvPanelFromTier(lv2, heroLv, this.lv2_status, this.lv2_desc);
const lv3Merged = lv3Plus.length
? { sLv: lv3Plus[0].sLv, heroLv: lv3Plus[0].heroLv, text: lv3Plus.map(t => t.text).join("\n") }
: undefined;
this.renderLvPanelFromTier(lv3Merged, heroLv, this.lv3_status, this.lv3_desc);
return true;
}
/** 按档位渲染单等级面板:激活状态按英雄等级对比档位解锁等级 */
private renderLvPanelFromTier(tier: HeroSkillTierLine | undefined, heroLv: number, statusLabel: Label | null, descRich: RichText | null) {
if (statusLabel && statusLabel.isValid) {
const unlocked = !!tier && heroLv >= tier.heroLv;
statusLabel.string = unlocked ? "已激活" : (tier ? `${tier.heroLv}级解锁` : "");
// 已激活显示绿色,未激活显示灰色
statusLabel.color = unlocked
? new Color(0, 255, 10, 255)
: new Color(150, 150, 150, 255);
}
if (descRich && descRich.isValid) {
descRich.string = tier?.text ?? "";
}
}
/** 主渲染入口 */
private render() {
if (!this.params) return;
const p = this.params;
// ---- 英雄技能模式:按英雄语境渲染分档描述(文本二) ----
if (p.hero_uuid && this.renderHeroSkill(p)) return;
// ---- 图标 ----
this.renderIcon(p);
// ---- 名称 ----
const name = this.resolveName(p);
this.setLabel(this.name_label, name);
// ---- 当前等级 ----
this.setLabel(this.lv_label, `Lv.${p.skill_lv}`);
// ---- 主功能说明 ----
this.renderMainInfo(p);
// ---- 1 级 / 2 级 / 3 级 激活信息 ----
const lv1Info = this.buildLvUpgradeInfo(p, 1);
const lv2Info = this.buildLvUpgradeInfo(p, 2);
const lv3Info = this.buildLvUpgradeInfo(p, 3);
this.renderLvPanel(lv1Info, this.main_status, null);
this.renderLvPanel(lv2Info, this.lv2_status, this.lv2_desc);
this.renderLvPanel(lv3Info, this.lv3_status, this.lv3_desc);
}
/** 渲染技能图标 */
private renderIcon(p: SInfoParams) {
if (!this.icon_node || !this.icon_node.isValid) return;
const iconId = p.card_icon ?? this.resolveIconId(p);
let sprite = this.icon_node.getComponent(Sprite) || this.icon_node.addComponent(Sprite);
if (smc.uiconsAtlas && iconId) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
sprite.spriteFrame = frame || null;
} else {
sprite.spriteFrame = null;
}
}
/** 解析图标ID */
private resolveIconId(p: SInfoParams): string | undefined {
if (p.card_icon) return p.card_icon;
// 技能卡:从 SkillCardList 查 icon
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard?.icon) return skillCard.icon;
// 驻场词条:从 FieldSkillSet 取
if (p.trigger_type === CardTriggerType.Field && p.field?.length) {
const fieldUuid = p.field[0].uuid;
return FieldSkillSet[fieldUuid]?.icon;
}
// 技能底座:从 SkillSet 取
const skillUuid = skillCard?.skill ?? p.uuid;
if (SkillSet[skillUuid]?.icon) return SkillSet[skillUuid]!.icon;
// 驻场光环词条:uuid 为 FieldSkillSet 词条(英雄图鉴光环弹窗)
return FieldSkillSet[p.uuid]?.icon;
}
/** 解析技能名称 */
private resolveName(p: SInfoParams): string {
if (p.skill_name) return p.skill_name;
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard?.name) return skillCard.name;
if (p.trigger_type === CardTriggerType.Field && p.field?.length) {
const fieldUuid = p.field[0].uuid;
return FieldSkillSet[fieldUuid]?.name ?? "";
}
const skillUuid = skillCard?.skill ?? p.uuid;
if (SkillSet[skillUuid]?.name) return SkillSet[skillUuid]!.name;
// 驻场光环词条:名称取 FieldSkillSet(英雄图鉴光环弹窗)
return FieldSkillSet[p.uuid]?.name ?? "";
}
/** 渲染主功能说明 */
private renderMainInfo(p: SInfoParams) {
if (!this.main_info || !this.main_info.isValid) return;
// 构建 CardConfig 用于描述生成
const card = this.buildCardConfig(p);
if (card) {
this.main_info.string = buildCardDescRich(card);
} else {
// 兜底:直接展示配置中的 info 字段
const info = this.resolveInfoText(p);
this.main_info.string = info || "暂无描述";
}
}
/** 构建 CardConfig 供描述生成器使用 */
private buildCardConfig(p: SInfoParams): CardConfig | null {
// 优先从技能卡池查
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard) return skillCard;
// 手动构建
return {
uuid: p.uuid,
type: 2, // CardType.Skill
cost: 0,
weight: 0,
kind: 2, // CKind.Skill
icon: p.card_icon,
name: p.skill_name,
field: p.field,
trigger_type: p.trigger_type,
} as CardConfig;
}
/** 解析主功能说明纯文本(兜底) */
private resolveInfoText(p: SInfoParams): string {
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard?.info) return skillCard.info;
const skillUuid = skillCard?.skill ?? p.uuid;
return SkillSet[skillUuid]?.info ?? "";
}
/** 构建指定等级的提升信息 */
private buildLvUpgradeInfo(p: SInfoParams, targetLv: number): LvUpgradeInfo {
const unlocked = p.skill_lv >= targetLv;
const lvText = `Lv.${targetLv}`;
const statusText = unlocked ? "已激活" : `${targetLv}级激活`;
return { unlocked, lvText, statusText };
}
/** 渲染单个等级面板 */
private renderLvPanel(info: LvUpgradeInfo, statusLabel: Label | null, descRich: RichText | null) {
if (statusLabel && statusLabel.isValid) {
statusLabel.string = info.statusText;
// 已激活显示绿色,未激活显示灰色
statusLabel.color = info.unlocked
? new Color(0, 255, 10, 255)
: new Color(150, 150, 150, 255);
}
if (descRich && descRich.isValid) {
descRich.string = info.unlocked ? "" : "未解锁";
}
}
/** 设置 Label 文本 */
private setLabel(label: Label | null, text: string) {
if (label && label.isValid) {
label.string = text;
}
}
/** ECS 组件移除时销毁节点 */
reset() {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}