1. 为所有卡牌配置添加icon字段,支持自定义指定图集内的图标 2. 重构列表组件与卡片组件的图标获取逻辑,优先使用自定义图标 3. 更新预加载的图集资源从ui3改为ui6,适配新图标图集 4. 调整弹窗预制体尺寸与图集引用,适配新的图标资源系统
507 lines
19 KiB
TypeScript
507 lines
19 KiB
TypeScript
/**
|
||
* @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 { SkillCardList } from "../common/config/SCardSet";
|
||
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 { UIID } from "../common/config/GameUIConfig";
|
||
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.node.on(NodeEventType.TOUCH_START, this.onInfoTouchStart, this);
|
||
this.node.on(NodeEventType.TOUCH_END, this.onInfoTouchEnd, this);
|
||
this.node.on(NodeEventType.TOUCH_CANCEL, this.onInfoTouchEnd, 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);
|
||
}
|
||
if (this.node && this.node.isValid) {
|
||
this.node.off(NodeEventType.TOUCH_START, this.onInfoTouchStart, this);
|
||
this.node.off(NodeEventType.TOUCH_END, this.onInfoTouchEnd, this);
|
||
this.node.off(NodeEventType.TOUCH_CANCEL, this.onInfoTouchEnd, this);
|
||
}
|
||
// 清理长按计时器,防止销毁后回调访问失效节点
|
||
this.unschedule(this.showInfo);
|
||
}
|
||
|
||
// ======================== 数据初始化 ========================
|
||
|
||
/**
|
||
* 初始化商品卡数据并渲染 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;
|
||
}
|
||
}
|
||
|
||
// 图标:优先用卡牌自定义 icon,装备取第一个 field 词条图标,商品取技能图标
|
||
if (this.icon_node) {
|
||
let iconId = "";
|
||
if (this.cardData.icon) {
|
||
iconId = this.cardData.icon;
|
||
} else 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;
|
||
}
|
||
}
|
||
|
||
// ======================== 长按信息框 ========================
|
||
|
||
/** 长按时长(秒) */
|
||
private static readonly LONG_PRESS_TIME: number = 0.5;
|
||
/** 信息框是否已弹出(区分长按完成与短按取消) */
|
||
private info_shown: boolean = false;
|
||
|
||
/** 卡面按下:启动长按计时 */
|
||
private onInfoTouchStart() {
|
||
if (!this.cardData) return;
|
||
this.info_shown = false;
|
||
this.scheduleOnce(this.showInfo, ItemListComp.LONG_PRESS_TIME);
|
||
}
|
||
|
||
/** 卡面放开/取消:已弹出则关闭信息框,未弹出则取消长按计时 */
|
||
private onInfoTouchEnd() {
|
||
this.unschedule(this.showInfo);
|
||
if (this.info_shown) {
|
||
this.info_shown = false;
|
||
// oops.gui 移除信息框
|
||
oops.gui.remove(UIID.HInfo);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 长按到点:按卡牌类型弹出对应信息框。
|
||
* - 装备卡(trigger_type=Field)→ 装备预览(isEquipCard)
|
||
* - 商品/药品及其他 → 技能卡预览(uuid 反查 SkillCardList)
|
||
*/
|
||
private showInfo() {
|
||
if (!this.cardData) return;
|
||
this.info_shown = true;
|
||
|
||
const isEquip = this.cardData.trigger_type === CardTriggerType.Field;
|
||
if (isEquip) {
|
||
// oops.gui 打开 HInfo 装备卡预览(复用 EquipBoxComp 的打开参数)
|
||
oops.gui.remove(UIID.HInfo);
|
||
oops.gui.open(UIID.HInfo, {
|
||
heroUuid: this.cardData.uuid,
|
||
heroLv: this.cardData.card_lv ?? 1,
|
||
poolLv: 1,
|
||
isSkillCard: true,
|
||
isEquipCard: true
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 技能/商品卡:反查 SkillCardList 拿到卡池 uuid(查不到时降级为本卡 uuid)
|
||
const config = SkillCardList.find(c => c.uuid === this.cardData!.uuid || c.skill === this.cardData!.uuid);
|
||
const cardUuid = config ? config.uuid : this.cardData.uuid;
|
||
oops.gui.remove(UIID.HInfo);
|
||
oops.gui.open(UIID.HInfo, {
|
||
heroUuid: cardUuid,
|
||
heroLv: this.cardData.card_lv ?? 1,
|
||
poolLv: config?.pool_lv ?? 1,
|
||
isSkillCard: true
|
||
});
|
||
}
|
||
|
||
// ======================== 购买逻辑 ========================
|
||
|
||
/**
|
||
* 购买点击处理:
|
||
* 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();
|
||
|
||
// 派发 CardUsed 事件,触发整池刷新
|
||
oops.message.dispatchEvent(GameEvent.CardUsed, this);
|
||
}
|
||
|
||
/** 标记为已购买状态(用于跨波次保持购买记录) */
|
||
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();
|
||
}
|
||
}
|
||
}
|