重构(地图卡牌): 抽离技能卡牌逻辑为独立组件
- 新增SCardComp.ts,实现技能卡牌专属的UI渲染、点击交互与使用逻辑 - 更新MissionCardComp.ts,替换技能卡槽的组件类型为SCardComp并修正相关代码 - 重构CardComp.ts:移除所有Skill类型卡牌的处理代码,修复卡牌等级取值优先级问题,简化拖拽逻辑仅保留英雄卡上划使用功能
This commit is contained in:
387
assets/script/game/map/SCardComp.ts
Normal file
387
assets/script/game/map/SCardComp.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user