Files
pixelheros/assets/script/game/map/SCardComp.ts
pan a06c2c0e31 feat: 重构卡牌描述渲染系统,实现动态富文本展示
1. 新增SkillDescSet配置商店技能与药水模板,补充6201-6206范围攻击与6502-6511计时强化技能配置
2. 替换原有单/多文本标签为富文本组件,统一使用buildCardDescRich生成描述
3. 简化物品商店与卡牌组件的描述渲染逻辑,移除冗余的getDescription方法
4. 为药水卡片移除硬编码描述,改为从技能模板动态生成,自动高亮数值与持续时间
5. 实现附加概率后缀自动生成,支持暴击/冰冻/击晕/击退概率动态展示
2026-07-31 16:02:47 +08:00

438 lines
15 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 SCardComp.ts
* @description 技能商店单个技能卡项组件UI 视图层 + 购买逻辑)
*
* 职责:
* 1. 渲染单个技能卡的显示信息(名称、等级、描述词条、图标)。
* 2. 处理购买点击 → 扣费 → 派发 UseSkillCard 事件触发技能生效。
*
* 技能使用机制:
* 购买后派发 GameEvent.UseSkillCard
* 由 MissSkillsComp 接收并创建 SBox 实体 / SkillBoxComp 执行技能逻辑。
*/
import { mLogger } from "../common/Logger";
import { _decorator, Node, Sprite, 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 { SkillCardList } from "../common/config/SCardSet";
import { SkillSet } from "../common/config/SkillSet";
import { buildCardDescRich } from "../common/config/HeroSkillDesc";
import { oops } from "db://oops-framework/core/Oops";
import { getCardIconId, setSpriteFrame } from "../common/CardIconHelper";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
import { MissionEconomy } from "./MissionEconomy";
const { ccclass, property } = _decorator;
/**
* SCardComp —— 技能商店单个技能卡项
*
* 由 MissionCardComp.dispatchCardsToSkillSlots() 实例化。
* 以图标 + 名称 + 描述词条 + 购买按钮的形式呈现。
*/
@ccclass('SCardComp')
@ecs.register('SCardComp', false)
export class SCardComp 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;
/** 描述富文本(多行,数值绿色高亮、技能名蓝色,由 buildCardDescRich 生成) */
@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 技能卡配置,传 null 时清空显示
*/
applyCardData(data: CardConfig | null): void {
// 刚购买(已播放大逝去动画)→ 不播普通逝去动画,直接应用新卡
if (this.isPurchased) {
this.cardData = data;
this.isPurchased = false;
if (data) {
this.node.active = true;
this.updateUI();
} else {
this.applyEmptyUI();
}
return;
}
// 旧卡存在且显示中 → 播普通逝去动画,结束后切新卡
if (this.cardData && this.node.active) {
this.playRefreshExitAnim(() => {
this.cardData = data;
this.isPurchased = false;
if (data) {
this.node.active = true;
this.updateUI();
} else {
this.applyEmptyUI();
}
});
return;
}
// 首次赋值或空槽 → 直接渲染
this.cardData = data;
this.isPurchased = false;
if (data) {
this.node.active = true;
this.updateUI();
} else {
this.applyEmptyUI();
}
}
/** 清空 UI 显示 */
private applyEmptyUI() {
// 清理动画状态
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
}
this.node.setScale(1, 1, 1);
if (this.name_label) this.name_label.string = "";
if (this.level_label) this.level_label.string = "";
if (this.info_rich) {
this.info_rich.string = "";
this.info_rich.node.active = false;
}
if (this.icon_node) {
const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
}
if (this.buy_node) {
this.buy_node.active = false;
}
this.node.active = false;
}
// ======================== UI 渲染 ========================
updateUI() {
if (!this.cardData) return;
const skillCard = SkillCardList.find(c => c.uuid === this.cardData!.uuid);
const skill = this.cardData.skill ? SkillSet[this.cardData.skill] : null;
// 名称
if (this.name_label) {
this.name_label.string = skillCard?.name || skill?.name || this.cardData.name || "";
}
// 等级标签(★ 表示卡牌等级)
if (this.level_label) {
const lv = this.cardData.card_lv ?? 1;
this.level_label.string = lv >= 2 ? "★".repeat(lv - 1) : "";
}
// 背景:按 card_lv 激活对应颜色子节点1→green 2→blue 3→purple 4→red 5→yellow
this.updateBgNode();
// 描述富文本(数值随 overrides 档位自动同步高亮)
if (this.info_rich) {
const desc = buildCardDescRich(this.cardData);
this.info_rich.string = 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. 派发 UseSkillCard 事件,触发技能生效流程。
* 3. 标记已购买,隐藏购买按钮。
*/
private onBuyClick() {
if (!this.cardData || this.isPurchased) return;
// 播放按钮点击回弹动画
this.playBtnClickAnim();
oops.audio.playEffect("music/button");
// 扣费
const cost = this.cardData.cost ?? 0;
const success = MissionEconomy.spendCoin(cost);
if (!success) {
oops.message.dispatchEvent(GameEvent.ShowSmallTip, "buy_coin");
mLogger.log(this.debugMode, "SCardComp", "purchase failed: not enough coin", {
uuid: this.cardData.uuid,
cost
});
return;
}
smc.vmdata.scores.refresh_hit_count++;
// 派发 UseSkillCard技能生效流程由 MissSkillsComp 接收处理
oops.message.dispatchEvent(GameEvent.UseSkillCard, this.cardData);
// 标记已购买
this.isPurchased = true;
if (this.buy_node) {
this.buy_node.active = false;
}
// 播放购买动画(卡牌放大 + 淡出)
this.playPurchaseAnim();
// 派发 CardUsed 事件,通知 MissionCardComp 刷新技能卡池
oops.message.dispatchEvent(GameEvent.CardUsed, this);
mLogger.log(this.debugMode, "SCardComp", "skill card purchased", {
uuid: this.cardData.uuid,
cost
});
}
/** 标记为已购买状态 */
setPurchased(): void {
this.isPurchased = true;
if (this.buy_node) {
this.buy_node.active = false;
}
}
/**
* 接收 MissionCardComp 分发的卡牌并刷新显示。
* @param data 卡牌配置null 时清空显示
* @returns 是否成功接收true = 接收并刷新 UI
*/
applyDrawCard(data: CardConfig | null): boolean {
this.applyCardData(data);
return !!data;
}
/**
* 系统清槽:任务开始/结束时强制重置。
* 清空数据与 UI 并隐藏节点。
*/
clearBySystem() {
this.cardData = null;
this.isPurchased = false;
this.applyEmptyUI();
this.node.active = false;
}
/** ECS 组件移除时销毁节点 */
reset() {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}