473 lines
19 KiB
TypeScript
473 lines
19 KiB
TypeScript
/**
|
||
* @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 技能分档描述,英雄技能模式,
|
||
* 覆盖普攻/大招、触发技能、驻场光环词条;复活已纳入 dead 触发组)
|
||
*/
|
||
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, SkillTriggerType } 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;
|
||
/** 技能所属槽位(英雄图鉴模式):同一底座可同时为普攻与触发技能(如 5002 的 6004),
|
||
* 指定后仅在该槽位取档,激活等级/档位按槽位独立判定 */
|
||
hero_slot?: SkillTriggerType;
|
||
}
|
||
|
||
/** 等级提升信息(用于 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级面板节点(无 2 级档位的技能隐藏) */
|
||
@property({ type: Node, displayName: "2级面板" })
|
||
private lv2_node: Node = null;
|
||
|
||
/** 2级激活状态标签 */
|
||
@property({ type: Label, displayName: "2级激活状态" })
|
||
private lv2_status: Label = null;
|
||
|
||
/** 2级提升说明(富文本) */
|
||
@property({ type: RichText, displayName: "2级提升说明" })
|
||
private lv2_desc: RichText = null;
|
||
|
||
/** 3级面板节点(无 3 级及以上档位的技能隐藏) */
|
||
@property({ type: Node, displayName: "3级面板" })
|
||
private lv3_node: Node = 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;
|
||
|
||
/** 全屏遮罩节点(铺满屏幕,点击弹窗主体以外区域关闭) */
|
||
@property({ type: Node, displayName: "遮罩节点" })
|
||
private mask_node: Node = null;
|
||
|
||
/** 技能未激活图标背景(技能未激活时显示,已激活隐藏) */
|
||
@property({ type: Node, displayName: "未激活图标背景" })
|
||
private inactive_bg: 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);
|
||
// 遮罩点击关闭:编辑器未绑定时按节点名 "mask" 兜底查找
|
||
if (!this.mask_node) {
|
||
this.mask_node = this.node.getChildByName("mask");
|
||
}
|
||
this.mask_node?.on(NodeEventType.TOUCH_END, this.onMaskClick, this);
|
||
}
|
||
|
||
onDisable() {
|
||
// 节点离场(含被 LayerManager 移入缓存池)即注销全局触摸,
|
||
// 防止残留监听导致 detached 节点再次响应关闭
|
||
this.unscheduleAllCallbacks();
|
||
input.off(Input.EventType.TOUCH_END, this.onGlobalTouch, 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);
|
||
}
|
||
if (this.mask_node && this.mask_node.isValid) {
|
||
this.mask_node.off(NodeEventType.TOUCH_END, this.onMaskClick, 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(() => {
|
||
if (this.node && this.node.isValid && this.node.activeInHierarchy) {
|
||
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 onMaskClick(event: EventTouch) {
|
||
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() {
|
||
// 防御重复关闭:节点已被移除(如关闭按钮与全局触摸同帧先后触发,
|
||
// removeByNode 默认不销毁节点,此时 isValid 仍为 true 但 parent 为 null)
|
||
if (!this.node || !this.node.isValid || !this.node.parent) return;
|
||
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, p.hero_slot);
|
||
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 级档位未解锁时显示
|
||
this.updateInactiveBg(heroLv >= tiers[0].heroLv);
|
||
|
||
// 技能 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);
|
||
// 无对应档位的技能隐藏 2 级 / 3 级面板节点
|
||
this.setPanelActive(this.lv2_node, !!lv2);
|
||
this.setPanelActive(this.lv3_node, lv3Plus.length > 0);
|
||
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 setPanelActive(node: Node | null, active: boolean) {
|
||
if (node && node.isValid) {
|
||
node.active = active;
|
||
}
|
||
}
|
||
|
||
/** 按档位渲染单等级面板:激活状态按英雄等级对比档位解锁等级 */
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新未激活图标背景显隐
|
||
* @param unlocked 技能是否已激活
|
||
*/
|
||
private updateInactiveBg(unlocked: boolean) {
|
||
if (this.inactive_bg && this.inactive_bg.isValid) {
|
||
this.inactive_bg.active = !unlocked;
|
||
}
|
||
}
|
||
|
||
/** ECS 组件移除时销毁节点 */
|
||
reset() {
|
||
if (this.node && this.node.isValid) {
|
||
this.node.destroy();
|
||
}
|
||
}
|
||
}
|