Files
pixelheros/assets/script/game/map/ItemListComp.ts
panFD 24eb72ef99 feat(map): 为三类商店卡牌组件添加动画交互效果
1.  新增按钮按下/取消缩放反馈、点击回弹动画
2.  新增卡牌入场淡入放大、购买放大淡出、刷新缩小淡出动画
3.  优化卡片切换逻辑,支持按状态播放对应动画
4.  同步重构了ItemListComp、EquipListComp、SCardComp三个组件
5.  删除了废弃的装备预制体元数据文件
2026-07-26 14:48:43 +08:00

430 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 ItemListComp.ts
* @description 商店列表项组件UI 视图层 + 购买逻辑)
*
* 职责:
* 1. 渲染单个商品/装备卡的显示信息(名称、等级、属性词条、图标)。
* 2. 处理购买点击 → 扣费 → 根据卡牌类型派发对应事件触发购买效果。
*
* 支持类型:
* - 装备卡trigger_type=Field→ 派发 UseEquipCard由 MissEquipComp 处理。
* - 商品/药品卡kind=Potion→ 派发 UseItemCard由 MissSkillsComp 处理(一次性 buff
*
* 设计说明:
* 本组件同时服务于装备商店和商品商店,通过 cardData 的 trigger_type / kind 自动判断类型。
*/
import { mLogger } from "../common/Logger";
import { _decorator, Node, Sprite, Label, 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, CardTriggerType, CKind } from "../common/config/CardSet";
import { EquipPoolList } from "../common/config/EquipSet";
import { FieldSkillSet, SkillSet } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
import { MissionEconomy } from "./MissionEconomy";
const { ccclass, property } = _decorator;
/**
* ItemListComp —— 商店列表项(装备/商品通用)
*
* 由 MissionCardComp.populateEquipments() / populateShopItems() 实例化。
* 以图标 + 名称 + 描述/属性词条 + 购买按钮的形式呈现。
*/
@ccclass('ItemListComp')
@ecs.register('ItemListComp', false)
export class ItemListComp extends CCComp {
private debugMode: boolean = false;
// ======================== 编辑器绑定节点 ========================
/** 商品图标节点 */
@property({ type: Node })
private icon_node: Node = null;
/** 背景节点 */
@property({ type: Node })
private bg_node: Node = null;
/** 商品名称 */
@property(Label)
private name_label: Label = null;
/** 描述/属性词条 1 */
@property(Label)
private fied1: Label = null;
/** 描述/属性词条 2 */
@property(Label)
private fied2: Label = null;
/** 描述/属性词条 3 */
@property(Label)
private fied3: Label = null;
/** 等级标签 */
@property(Label)
private level_label: Label = null;
/** 购买按钮节点 */
@property({ type: Node })
private buy_node: Node = 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 商品/装备卡配置
*/
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 isEquip = this.cardData.trigger_type === CardTriggerType.Field;
const isPotion = this.cardData.kind === CKind.Potion;
// 名称:装备从 EquipPoolList 查找,商品直接用 cardData.name
if (this.name_label) {
if (isEquip) {
const equipConfig = EquipPoolList.find(c => c.uuid === this.cardData!.uuid);
this.name_label.string = equipConfig?.name || this.cardData.name || "";
} else {
this.name_label.string = this.cardData.name || "";
}
}
// 等级标签(★ 表示卡牌等级)
if (this.level_label) {
const lv = this.cardData.card_lv ?? 1;
this.level_label.string = lv >= 2 ? "★".repeat(lv - 1) : "";
}
// 描述/属性词条(最多显示 3 条)
const fieldLabels = [this.fied1, this.fied2, this.fied3];
if (isEquip) {
// 装备:显示 field 词条
const fields = this.cardData.field || [];
for (let i = 0; i < fieldLabels.length; i++) {
if (!fieldLabels[i]) continue;
if (i < fields.length) {
const fieldConfig = FieldSkillSet[fields[i]];
fieldLabels[i].string = fieldConfig?.info || "";
fieldLabels[i].node.active = true;
} else {
fieldLabels[i].string = "";
fieldLabels[i].node.active = false;
}
}
} else if (isPotion) {
// 商品:显示技能描述 + info
const skillConfig = this.cardData.skill ? SkillSet[this.cardData.skill] : null;
const skillInfo = skillConfig?.info || "";
const cardInfo = this.cardData.info || "";
if (fieldLabels[0]) {
fieldLabels[0].string = cardInfo;
fieldLabels[0].node.active = !!cardInfo;
}
if (fieldLabels[1]) {
fieldLabels[1].string = skillInfo;
fieldLabels[1].node.active = !!skillInfo && skillInfo !== cardInfo;
}
if (fieldLabels[2]) {
fieldLabels[2].string = "";
fieldLabels[2].node.active = false;
}
} else {
// 其他类型:显示 info
for (let i = 0; i < fieldLabels.length; i++) {
if (!fieldLabels[i]) continue;
fieldLabels[i].string = i === 0 ? (this.cardData.info || "") : "";
fieldLabels[i].node.active = i === 0 && !!this.cardData.info;
}
}
// 图标:装备取第一个 field 词条图标,商品取技能图标
if (this.icon_node) {
let iconId = "";
if (isEquip) {
const fields = this.cardData.field || [];
if (fields.length > 0) {
iconId = FieldSkillSet[fields[0]]?.icon || "";
}
} else if (isPotion && this.cardData.skill) {
iconId = SkillSet[this.cardData.skill]?.icon || "";
}
if (iconId) {
this.updateIcon(this.icon_node, iconId);
}
}
// 购买按钮费用显示
if (this.buy_node) {
this.buy_node.active = true;
const costLabel = this.buy_node.getChildByName("num")?.getComponent(Label)
|| this.buy_node.getComponentInChildren(Label);
if (costLabel) {
costLabel.string = `${this.cardData.cost ?? 0}`;
}
}
// 播放入场动画(由小变大 + 淡入)
this.playEnterAnim();
}
// ======================== 动画 ========================
/** 播放入场动画卡牌由小变大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();
}
/** 更新图标精灵 */
private updateIcon(node: Node, iconId: string) {
if (!node || !iconId) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (!sprite) return;
if (smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
sprite.spriteFrame = frame || null;
}
}
// ======================== 购买逻辑 ========================
/**
* 购买点击处理:
* 1. 扣除金币(失败则提示并中止)。
* 2. 根据卡牌类型派发对应事件:
* - 装备卡trigger_type=Field→ UseEquipCard → MissEquipComp
* - 商品/药品卡kind=Potion→ UseItemCard → MissSkillsComp一次性 buff 技能)
* 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, "ItemListComp", "purchase failed: not enough coin", {
uuid: this.cardData.uuid,
cost
});
return;
}
// 根据类型派发对应事件
const isEquip = this.cardData.trigger_type === CardTriggerType.Field;
const isPotion = this.cardData.kind === CKind.Potion;
if (isEquip) {
// 装备:被动驻场,由 MissEquipComp 处理
oops.message.dispatchEvent(GameEvent.UseEquipCard, this.cardData);
mLogger.log(this.debugMode, "ItemListComp", "equipment purchased", {
uuid: this.cardData.uuid,
cost
});
} else if (isPotion) {
// 商品/药品:一次性 buff 技能,由 MissSkillsComp 处理
oops.message.dispatchEvent(GameEvent.UseItemCard, this.cardData);
mLogger.log(this.debugMode, "ItemListComp", "item purchased", {
uuid: this.cardData.uuid,
skill: this.cardData.skill,
cost
});
} else {
// 默认走技能卡流程
oops.message.dispatchEvent(GameEvent.UseSkillCard, this.cardData);
mLogger.log(this.debugMode, "ItemListComp", "card purchased", {
uuid: this.cardData.uuid,
cost
});
}
// 标记已购买
this.isPurchased = true;
if (this.buy_node) {
this.buy_node.active = false;
}
// 播放购买动画(卡牌放大 + 淡出)
this.playPurchaseAnim();
}
/** 标记为已购买状态(用于跨波次保持购买记录) */
setPurchased(): void {
this.isPurchased = true;
if (this.buy_node) {
this.buy_node.active = false;
}
}
/** ECS 组件移除时销毁节点 */
reset() {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}