Files
pixelheros/assets/script/game/map/EquipListComp.ts
panFD b7f64a5aa7 refactor: 新增富文本描边工具并优化深色面板文案可读性
1. 新增RichOutline工具类,封装cc.RichText的outline标签包裹逻辑
2. 为所有道具/技能/英雄描述富文本添加黑色2px描边,提升深色背景下的可读性
3. 优化技能描述渲染逻辑:
   - 调整数值高亮色为更鲜亮的绿色
   - 使用罗马数字作为技能等级徽章
   - 移除升级预览弹窗的重复描述,改为当前/升级后双版本对比
   - 简化升级确认弹窗的格式排版
4. 调整道具/技能预制体的文字颜色为白色,适配深色面板
2026-08-09 14:16:16 +08:00

358 lines
13 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 EquipListComp.ts
* @description 装备商店单个装备项组件UI 视图层 + 购买逻辑)
*
* 职责:
* 1. 渲染单个装备卡的显示信息(名称、等级、属性词条、图标)。
* 2. 处理购买点击 → 扣费 → 派发 UseEquipCard 事件触发装备生效。
*
* 装备使用机制:
* 与技能卡使用流程一致——购买后派发 GameEvent.UseEquipCard
* 由 MissEquipComp 接收并创建 EquipBoxComp 实体,
* EquipBoxComp 按 Field 类型被动驻场,属性加成由 FieldSkillHelper 全局聚合。
*/
import { mLogger } from "../common/Logger";
import { _decorator, Node, Label, RichText, NodeEventType, Vec3, UIOpacity, tween, Tween } 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 { CardConfig } from "../common/config/CardSet";
import { EquipPoolList } from "../common/config/EquipSet";
import { buildEquipRich } from "../common/config/HeroSkillDesc";
import { outlineRich } from "../common/RichOutline";
import { oops } from "db://oops-framework/core/Oops";
import { getCardIconId, setSpriteFrame } from "../common/CardIconHelper";
import { GameEvent } from "../common/config/GameEvent";
const { ccclass, property } = _decorator;
/**
* EquipListComp —— 装备商店单个装备项
*
* 由 MissionCardComp.populateEquipments() 实例化。
* 以图标 + 名称 + 属性词条 + 购买按钮的形式呈现。
*/
@ccclass('EquipListComp')
@ecs.register('EquipListComp', false)
export class EquipListComp extends CCComp {
private debugMode: boolean = false;
// ======================== 编辑器绑定节点 ========================
/** 装备图标节点 */
@property({ type: Node })
private icon_node: Node = null;
/** 背景节点(含 green/blue/purple/red/yellow 颜色子节点,按 card_lv 激活) */
@property({ type: Node })
private bg_node: Node = null;
/** 装备名称 */
@property(Label)
private name_label: Label = null;
/** 属性词条富文本(多行,数值绿色高亮,由 buildEquipRich 按 FieldDescSet 实时渲染) */
@property(RichText)
private info_rich: RichText = null;
/** 等级标签 */
@property(Label)
private level_label: Label = null;
/** 购买按钮节点 */
@property({ type: Node })
private buy_node: Node = null;
/** 消费金币标签(未绑定时回退查找 buy_node 下的 num/Label 子节点) */
@property(Label)
private cost_label: Label = null;
// ======================== 运行时状态 ========================
/** 当前装备的卡牌配置 */
private cardData: CardConfig | null = null;
/** 购买/逝去动画播放中标记(仅用于跳过新卡的普通逝去动画,不影响再次购买) */
private isPurchased: boolean = false;
/** 透明度组件(用于淡入淡出动画) */
private opacityComp: UIOpacity | null = null;
/** 入场动画时长(秒) */
private readonly enterDuration: number = 0.25;
/** 购买/逝去动画时长(秒) */
private readonly exitDuration: number = 0.2;
// ======================== 生命周期 ========================
onLoad() {
this.buy_node?.on(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
this.buy_node?.on(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
}
onDestroy() {
super.onDestroy();
if (this.buy_node && this.buy_node.isValid) {
this.buy_node.off(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
}
}
// ======================== 数据初始化 ========================
/**
* 初始化装备卡数据并渲染 UI
*
* 若槽位已有旧卡且显示中,先播普通逝去动画(缩小淡出),结束后切新卡;
* 刚购买的槽位跳过逝去动画直接应用新卡(购买动画已播放大淡出)。
*
* @param data 装备卡配置(必须为 trigger_type=Field 的装备卡)
*/
applyCardData(data: CardConfig): void {
if (!data) return;
// 刚购买(已播放大逝去动画)→ 不播普通逝去动画,直接应用新卡
if (this.isPurchased) {
this.cardData = data;
this.isPurchased = false;
this.updateUI();
return;
}
// 旧卡存在且显示中 → 播普通逝去动画,结束后切新卡
if (this.cardData && this.node.active) {
this.playRefreshExitAnim(() => {
this.cardData = data;
this.isPurchased = false;
this.updateUI();
});
return;
}
// 首次赋值 → 直接渲染
this.cardData = data;
this.isPurchased = false;
this.updateUI();
}
// ======================== UI 渲染 ========================
updateUI() {
if (!this.cardData) return;
const cardConfig = EquipPoolList.find(c => c.uuid === this.cardData!.uuid);
// 名称
if (this.name_label) {
this.name_label.string = cardConfig?.name || "";
}
// 等级标签Lv.1 / Lv.2 / Lv.3
if (this.level_label) {
const lv = this.cardData.card_lv ?? 1;
this.level_label.string = `Lv.${lv}`;
}
// 背景:按 card_lv 激活对应颜色子节点1→green 2→blue 3→purple 4→red 5→yellow
this.updateBgNode();
// 属性词条富文本(多词条装备由 buildEquipRich 按 \n 多行渲染,数值实时高亮)
// outlineRich 为整段富文本包裹描边,提升深色面板可读性
if (this.info_rich) {
const desc = buildEquipRich(this.cardData.field || []);
this.info_rich.string = outlineRich(desc);
this.info_rich.node.active = !!desc;
}
// 图标共享工具解析cardData.icon → FieldSkillSet → SkillSet
if (this.icon_node) {
const iconId = getCardIconId(this.cardData);
if (iconId) {
setSpriteFrame(this.icon_node, iconId);
}
}
// 购买按钮费用显示
if (this.buy_node) {
this.buy_node.active = true;
const costLabel = this.cost_label
|| this.buy_node.getChildByName("num")?.getComponent(Label)
|| this.buy_node.getComponentInChildren(Label);
if (costLabel) {
costLabel.string = `${this.cardData.cost ?? 0}`;
}
}
// 播放入场动画(由小变大 + 淡入)
this.playEnterAnim();
}
/**
* 根据 card_lv 切换 bg_node 下的颜色子节点显示。
* 等级与颜色对应关系:
* card_lv 1 → green
* card_lv 2 → blue
* card_lv 3 → purple
* card_lv 4 → red
* card_lv 5 → yellow
*/
private updateBgNode() {
if (!this.bg_node || !this.cardData) return;
const lvToColor: Record<number, string> = {
1: "green",
2: "blue",
3: "purple",
4: "red",
5: "yellow",
};
const targetColor = lvToColor[this.cardData.card_lv ?? 1];
this.bg_node.children.forEach(child => {
child.active = (child.name === targetColor);
});
}
// ======================== 动画 ========================
/** 播放入场动画卡牌由小变大scale 0→1+ 淡入 */
private playEnterAnim() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
}
this.node.setScale(0, 0, 1);
if (this.opacityComp) {
this.opacityComp.opacity = 0;
}
tween(this.node)
.to(this.enterDuration, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
.start();
if (this.opacityComp) {
tween(this.opacityComp)
.to(this.enterDuration, { opacity: 255 })
.start();
}
}
/** 播放购买动画卡牌放大scale 1→1.1+ 淡出 */
private playPurchaseAnim() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
}
tween(this.node)
.to(this.exitDuration, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadIn' })
.start();
if (this.opacityComp) {
tween(this.opacityComp)
.to(this.exitDuration, { opacity: 0 })
.start();
}
}
/**
* 播放普通刷新"逝去"动画旧卡缩小scale 1→0+ 淡出,结束后回调切换新卡。
* @param onComplete 动画结束回调,用于应用新卡数据
*/
private playRefreshExitAnim(onComplete: () => void) {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
}
tween(this.node)
.to(this.exitDuration, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
.start();
if (this.opacityComp) {
tween(this.opacityComp)
.to(this.exitDuration, { opacity: 0 })
.call(onComplete)
.start();
} else {
this.scheduleOnce(onComplete, this.exitDuration);
}
}
// ======================== 按钮动画 ========================
/** 按钮正常缩放 */
private readonly btnNormalScale: number = 1;
/** 按钮按下缩放 */
private readonly btnPressScale: number = 0.92;
/** 按钮点击回弹峰值缩放 */
private readonly btnClickScale: number = 1.08;
/** 购买按钮按下:缩小反馈 */
private onBuyTouchStart() {
this.playBtnScaleTo(this.btnPressScale, 0.06);
}
/** 购买按钮取消:恢复缩放 */
private onBuyTouchCancel() {
this.playBtnScaleTo(this.btnNormalScale, 0.08);
}
/** 按钮缩放动画(通用) */
private playBtnScaleTo(scale: number, duration: number) {
if (!this.buy_node || !this.buy_node.isValid) return;
Tween.stopAllByTarget(this.buy_node);
tween(this.buy_node)
.to(duration, { scale: new Vec3(scale, scale, 1) })
.start();
}
/** 按钮点击回弹动画:先弹到峰值再回到正常 */
private playBtnClickAnim() {
if (!this.buy_node || !this.buy_node.isValid) return;
Tween.stopAllByTarget(this.buy_node);
tween(this.buy_node)
.to(0.05, { scale: new Vec3(this.btnClickScale, this.btnClickScale, 1) })
.to(0.08, { scale: new Vec3(this.btnNormalScale, this.btnNormalScale, 1) })
.start();
}
// ======================== 购买逻辑 ========================
/**
* 购买点击处理:
* 1. 扣除金币(失败则提示并中止)。
* 2. 派发 UseEquipCard 事件,触发装备生效流程。
* 3. 播放购买动画,购买后可继续购买(购买按钮保持可见)。
*
* 装备生效链路:
* UseEquipCard → MissEquipComp.addSkill() → SBox ECS 实体 → SkillBoxComp(Field 类型)
* → FieldSkillHelper.getFieldSkillTotalValue() 全局聚合属性加成
*/
private onBuyClick() {
if (!this.cardData) return;
// 播放按钮点击回弹动画
this.playBtnClickAnim();
oops.audio.playEffect("music/button");
// 购买不花费金币(免费获取),直接派发装备生效事件
oops.message.dispatchEvent(GameEvent.UseEquipCard, this.cardData);
// 播放购买动画(卡牌放大 + 淡出)
this.isPurchased = true;
this.playPurchaseAnim();
// 派发 CardUsed 事件,触发整池刷新
oops.message.dispatchEvent(GameEvent.CardUsed, this);
mLogger.log(this.debugMode, "EquipListComp", "equipment purchased", {
uuid: this.cardData.uuid
});
}
/** 标记购买动画播放中(供外部在刷新前保持动画状态,不再用于限购) */
setPurchased(): void {
this.isPurchased = true;
}
/** ECS 组件移除时销毁节点 */
reset() {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}