Files
pixelheros/assets/script/game/map/ItemListComp.ts
pan 9f2599e9ec refactor: 统一将 pool_lv 重命名为 card_lv 并清理废弃代码
本次提交完成了全项目范围内的术语统一重构:
1.  将所有`pool_lv`相关变量、字段、注释替换为`card_lv`,对齐卡牌等级的业务含义
2.  移除了配置中冗余的默认`pool_lv`赋值,简化数据结构
3.  删除了废弃的`HlistComp.ts`组件及其元文件
4.  修正了多处逻辑错误,确保卡牌等级正确驱动背景框切换
5.  更新了相关注释和文档,保持代码一致性
2026-07-27 15:54:53 +08:00

513 lines
19 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, 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 { getCardIconId, setSpriteFrame } from "../common/CardIconHelper";
import { GameEvent } from "../common/config/GameEvent";
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;
/** 背景节点(含 green/blue/purple/red/yellow 颜色子节点,按 card_lv 激活) */
@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) : "";
}
// 背景:按 card_lv 激活对应颜色子节点1→green 2→blue 3→purple 4→red 5→yellow
this.updateBgNode();
// 描述/属性词条(最多显示 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;
}
}
// 图标共享工具解析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.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();
}
// ======================== 长按信息框 ========================
/** 长按时长(秒) */
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?.card_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();
}
}
}