重构(地图卡牌): 抽离技能卡牌逻辑为独立组件

- 新增SCardComp.ts,实现技能卡牌专属的UI渲染、点击交互与使用逻辑
- 更新MissionCardComp.ts,替换技能卡槽的组件类型为SCardComp并修正相关代码
- 重构CardComp.ts:移除所有Skill类型卡牌的处理代码,修复卡牌等级取值优先级问题,简化拖拽逻辑仅保留英雄卡上划使用功能
This commit is contained in:
panFD
2026-06-04 19:08:54 +08:00
parent 2276ff1fbd
commit 237df1dc4e
6 changed files with 21246 additions and 1930 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -298,7 +298,7 @@ export class CardComp extends CCComp {
reason: "",
uuid: this.cardData.uuid,
hero_lv: this.cardData.hero_lv ?? 1,
card_lv: this.cardData.pool_lv ?? 1
card_lv: this.cardData.base_pool_lv ?? this.cardData.pool_lv ?? 1
};
oops.message.dispatchEvent(GameEvent.UseHeroCard, guard);
if (guard.cancel) {
@@ -355,9 +355,6 @@ export class CardComp extends CCComp {
case CardType.Hero:
oops.message.dispatchEvent(GameEvent.CallHero, payload);
break;
case CardType.Skill:
oops.message.dispatchEvent(GameEvent.UseSkillCard, payload);
break;
case CardType.SpecialUpgrade:
case CardType.SpecialRefresh:
oops.message.dispatchEvent(GameEvent.UseSpecialCard, payload);
@@ -507,10 +504,7 @@ export class CardComp extends CCComp {
}
}
// 技能卡不支持上划移动
if (this.cardData.type !== CardType.Skill) {
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
}
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
}
/**
@@ -533,24 +527,13 @@ export class CardComp extends CCComp {
oops.gui.remove(UIID.HInfo);
}
// 技能卡改为点击使用
if (this.cardData.type === CardType.Skill) {
if (Math.abs(deltaY) < 20 && Math.abs(deltaX) < 20) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
}
return;
}
} else {
// 英雄卡保持上划使用
if (deltaY >= this.dragUseThreshold) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
}
return;
// 英雄卡保持上划使用
if (deltaY >= this.dragUseThreshold) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
}
return;
}
this.playReboundAnim();
@@ -722,19 +705,6 @@ export class CardComp extends CCComp {
this.hp_node.active = true;
// 英雄卡:隐藏技能信息节点
if (this.info_node) this.info_node.active = false;
} else if (this.card_type === CardType.Skill) {
if (this.lvl_node) this.lvl_node.node.active = false;
// 技能卡:显示技能名 + 品质后缀 + 描述
const skill = SkillSet[this.card_uuid];
const skillCard = CardPoolList.find(c => c.uuid === this.card_uuid);
const card_lv = Math.max(1, Math.floor(this.cardData.card_lv ?? 1));
const spSuffix = card_lv >= 2 ? "★".repeat(card_lv - 1) : "";
this.setLabel(this.name_node, `${spSuffix}${skillCard?.name || skill?.name || ""}${spSuffix}`);
this.ap_node.active = false;
this.hp_node.active = false;
// 技能卡:显示技能信息节点
if (this.info_node) this.info_node.active = true;
} else {
if (this.lvl_node) this.lvl_node.node.active = false;
// 特殊卡(升级 / 刷新):显示卡名 + 品质后缀 + 描述
@@ -758,11 +728,6 @@ export class CardComp extends CCComp {
}
}
if (this.name_node) {
const currentPos = this.name_node.position;
this.name_node.setPosition(currentPos.x, -70, currentPos.z);
}
// ---- 图标 ----
const iconNode = this.icon_node as Node;
if (this.card_type === CardType.Hero) {
@@ -880,10 +845,6 @@ export class CardComp extends CCComp {
});
});
if (this.name_node) {
const currentPos = this.name_node.position;
this.name_node.setPosition(currentPos.x, -70, currentPos.z);
}
this.iconVisualToken += 1;
this.setLabel(this.name_node, "");
if (this.cost_node) {
@@ -936,9 +897,6 @@ export class CardComp extends CCComp {
* @returns 图标帧名称
*/
private resolveCardIconId(type: CardType, uuid: number): string {
if (type === CardType.Skill) {
return SkillSet[uuid]?.icon || `${uuid}`;
}
if (type === CardType.Hero) {
return HeroInfo[uuid]?.icon || `${uuid}`;
}

View File

@@ -39,6 +39,7 @@ import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/modu
import { GameEvent } from "../common/config/GameEvent";
import { CARD_POOL_INIT_LEVEL, CARD_POOL_MAX_LEVEL, CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE, CardConfig, CardType, CardsUpSet, drawCardsByRule, getCardsByLv, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
import { CardComp } from "./CardComp";
import { SCardComp } from "./SCardComp";
import { oops } from "db://oops-framework/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
@@ -138,7 +139,7 @@ export class MissionCardComp extends CCComp {
/** 四个槽位对应的 CardComp 控制器缓存(有序数组) */
private cardComps: CardComp[] = [];
/** 技能卡槽控制器缓存 */
private skillCardComps: CardComp[] = [];
private skillCardComps: SCardComp[] = [];
/** 当前卡池等级(仅影响抽卡来源,不直接改卡槽现有内容) */
private poolLv: number = CARD_POOL_INIT_LEVEL;
/** 是否已缓存卡牌面板基准缩放 */
@@ -671,8 +672,8 @@ export class MissionCardComp extends CCComp {
const skillNodes = [this.skill_card1, this.skill_card2, this.skill_card3];
this.skillCardComps = skillNodes
.map(node => node?.getComponent(CardComp))
.filter((comp): comp is CardComp => !!comp);
.map(node => node?.getComponent(SCardComp))
.filter((comp): comp is SCardComp => !!comp);
}
// ======================== 核心业务:抽卡 & 升级 ========================

View File

@@ -0,0 +1,387 @@
/**
* @file SCardComp.ts
* @description 技能卡牌槽位组件UI 视图层)
*
* 职责:
* 1. 管理技能卡牌槽位的显示和交互(点击使用)。
* 2. 渲染技能卡面。
* 3. 触发使用时扣除费用并分发 UseSkillCard 事件。
*/
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, UITransform, Widget } 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, CardType, CKind, CardPoolList } from "../common/config/CardSet";
import { CardBgComp } from "./CardBgComp";
import { SkillSet } from "../common/config/SkillSet";
import { GameEvent } from "../common/config/GameEvent";
import { oops } from "db://oops-framework/core/Oops";
import { smc } from "../common/SingletonModuleComp";
import { MissionEconomy } from "./MissionEconomy";
const { ccclass, property } = _decorator;
@ccclass('SCardComp')
@ecs.register('SCardComp', false)
export class SCardComp extends CCComp {
private debugMode: boolean = true;
@property(Node)
name_node: Node = null!
@property(Node)
icon_node: Node = null!
@property(Node)
cost_node: Node = null!
@property(Node)
Ckind_node: Node = null!
@property(Node)
BG_node: Node = null!
@property(Node)
info_node: Node = null!
card_cost: number = 0
card_type: CardType = CardType.Skill
card_uuid: number = 0
private cardData: CardConfig | null = null;
private touchStartY: number = 0;
private touchStartX: number = 0;
private isDragging: boolean = false;
private isUsing: boolean = false;
private restPosition: Vec3 = new Vec3();
private hasFixedBasePosition: boolean = false;
private fixedBaseY: number = 0;
private fixedBaseZ: number = 0;
private opacityComp: UIOpacity | null = null;
private iconVisualToken: number = 0;
onLoad() {
this.bindEvents();
this.restPosition = this.node.position.clone();
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
this.opacityComp.opacity = 255;
this.applyEmptyUI();
}
onDestroy() {
super.onDestroy();
this.unbindEvents();
}
init() { }
start() {
this.node.active = true;
}
applyDrawCard(data: CardConfig | null): boolean {
if (!data) return false;
this.cardData = data;
this.card_uuid = data.uuid;
this.card_type = data.type;
this.card_cost = Math.floor(data.cost ?? 0);
this.node.active = true;
this.applyCardUI();
this.playRefreshAnim();
mLogger.log(this.debugMode, "SCardComp", "skill card updated", {
uuid: this.card_uuid,
cost: this.card_cost
});
return true;
}
useCard(): CardConfig | null {
if (!this.cardData || this.isUsing) return null;
const cardCost = this.card_cost;
const success = MissionEconomy.spendCoin(cardCost);
if (!success) {
oops.gui.toast(`金币不足,需要${cardCost}`);
this.playReboundAnim();
return null;
}
smc.vmdata.scores.refresh_hit_count++;
this.isUsing = true;
const used = this.cardData;
this.playUseDisappearAnim(() => {
this.clearAfterUse();
this.isUsing = false;
oops.message.dispatchEvent(GameEvent.UseSkillCard, used);
});
return used;
}
hasCard(): boolean {
return !!this.cardData;
}
setSlotPosition(x: number) {
const current = this.node.position;
if (!this.hasFixedBasePosition) {
this.fixedBaseY = current.y;
this.fixedBaseZ = current.z;
this.hasFixedBasePosition = true;
}
this.restPosition = new Vec3(x, this.fixedBaseY, this.fixedBaseZ);
if (!this.isDragging && !this.isUsing) {
this.node.setPosition(this.restPosition);
}
}
clearBySystem() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
}
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.isDragging = false;
this.isUsing = false;
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(1, 1, 1));
this.applyEmptyUI();
this.node.active = false;
}
private clearAfterUse() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
}
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.isDragging = false;
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(1, 1, 1));
this.applyEmptyUI();
this.node.active = false;
}
private bindEvents() {
this.node.on(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.on(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this);
this.node.on(NodeEventType.TOUCH_END, this.onCardTouchEnd, this);
this.node.on(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
}
private unbindEvents() {
if (this.node && this.node.isValid) {
this.node.off(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.off(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this);
this.node.off(NodeEventType.TOUCH_END, this.onCardTouchEnd, this);
this.node.off(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
}
}
private onCardTouchStart(event: EventTouch) {
if (!this.cardData || this.isUsing) return;
this.touchStartY = event.getUILocation().y;
this.touchStartX = event.getUILocation().x;
this.isDragging = true;
}
private onCardTouchMove(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
// 技能卡不支持上拉移动
}
private onCardTouchEnd(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const endY = event.getUILocation().y;
const endX = event.getUILocation().x;
const deltaY = endY - this.touchStartY;
const deltaX = endX - this.touchStartX;
this.isDragging = false;
// 点击触发
if (Math.abs(deltaY) < 20 && Math.abs(deltaX) < 20) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
}
return;
}
this.playReboundAnim();
}
private onCardTouchCancel() {
if (!this.isDragging || this.isUsing) return;
this.isDragging = false;
this.playReboundAnim();
}
private applyCardUI() {
if (!this.cardData) {
this.applyEmptyUI();
return;
}
this.iconVisualToken += 1;
if (this.opacityComp) this.opacityComp.opacity = 255;
this.node.setPosition(this.restPosition.x, this.restPosition.y, this.restPosition.z);
const kindName = CKind[this.cardData.kind];
if (this.BG_node) {
const bgLv = this.cardData.base_pool_lv ?? this.cardData.pool_lv;
this.BG_node.children.forEach(child => {
child.active = (child.name === kindName);
const bg = child.getComponent(CardBgComp);
if (bg) child.active ? bg.apply(bgLv) : bg.clear();
});
}
if (this.Ckind_node) {
this.Ckind_node.children.forEach(child => {
child.active = (child.name === kindName);
});
}
this.node.children.forEach(child => {
const widget = child.getComponent(Widget);
if (widget) widget.updateAlignment();
child.children.forEach(subChild => {
const subWidget = subChild.getComponent(Widget);
if (subWidget) subWidget.updateAlignment();
});
});
const skill = SkillSet[this.card_uuid];
const skillCard = CardPoolList.find(c => c.uuid === this.card_uuid);
const card_lv = Math.max(1, Math.floor(this.cardData.card_lv ?? 1));
const spSuffix = card_lv >= 2 ? "★".repeat(card_lv - 1) : "";
this.setLabel(this.name_node, `${spSuffix}${skillCard?.name || skill?.name || ""}${spSuffix}`);
if (this.info_node) this.info_node.active = true;
if (this.cost_node) {
this.cost_node.active = true;
const numNode = this.cost_node.getChildByName("num");
if (numNode) {
this.setLabel(numNode, `${this.card_cost}`);
}
}
const iconNode = this.icon_node as Node;
if (iconNode) {
iconNode.setScale(new Vec3(1, 1, 1));
this.clearIconAnimation(iconNode);
const iconId = skill?.icon || `${this.card_uuid}`;
this.updateIcon(iconNode, iconId);
}
}
private playRefreshAnim() {
Tween.stopAllByTarget(this.node);
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(0.92, 0.92, 1));
tween(this.node)
.to(0.08, { scale: new Vec3(1.06, 1.06, 1) })
.to(0.1, { scale: new Vec3(1, 1, 1) })
.start();
}
private playReboundAnim() {
Tween.stopAllByTarget(this.node);
tween(this.node)
.to(0.12, {
position: new Vec3(this.restPosition.x, this.restPosition.y, this.restPosition.z),
scale: new Vec3(1, 1, 1)
})
.start();
}
private playUseDisappearAnim(onComplete: () => void) {
const targetPos = new Vec3(this.restPosition.x, this.restPosition.y + 120, this.restPosition.z);
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
tween(this.opacityComp)
.to(0.18, { opacity: 0 })
.start();
}
tween(this.node)
.to(0.18, {
position: targetPos,
scale: new Vec3(0.8, 0.8, 1)
})
.call(onComplete)
.start();
}
private applyEmptyUI() {
if (this.BG_node) {
this.BG_node.children.forEach(child => {
child.active = false;
const bg = child.getComponent(CardBgComp);
if (bg) bg.clear();
});
}
this.node.children.forEach(child => {
const widget = child.getComponent(Widget);
if (widget) widget.updateAlignment();
child.children.forEach(subChild => {
const subWidget = subChild.getComponent(Widget);
if (subWidget) subWidget.updateAlignment();
});
});
this.iconVisualToken += 1;
this.setLabel(this.name_node, "");
if (this.cost_node) {
const numNode = this.cost_node.getChildByName("num");
if (numNode) {
this.setLabel(numNode, "");
}
this.cost_node.active = false;
}
if (this.Ckind_node) {
this.Ckind_node.children.forEach(child => {
child.active = false;
});
}
if (this.info_node) this.info_node.active = false;
if (this.icon_node) {
(this.icon_node as Node).setScale(new Vec3(1, 1, 1));
this.clearIconAnimation(this.icon_node as Node);
const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
}
}
private setLabel(node: Node | null, value: string) {
if (!node) return;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
if (label) label.string = value;
}
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 clearIconAnimation(node: Node) {
const anim = node?.getComponent(Animation);
if (!anim) return;
anim.stop();
const clips = (anim as any).clips as AnimationClip[] | undefined;
if (!clips || clips.length === 0) return;
[...clips].forEach(clip => anim.removeClip(clip, true));
}
/** ECS 组件移除时的释放钩子:销毁节点 */
reset() {
this.node.destroy();
}
}

View File

@@ -0,0 +1 @@
{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"3fe4ed29-26ed-4aef-9feb-a7a451a31f99","files":[],"subMetas":{},"userData":{}}