Files
pixelheros/assets/script/game/map/SCardComp.ts
panFD 500ba03eb9 feat(game): 重构卡牌技能关联逻辑,调整游戏UI布局
为CardConfig新增skill字段,支持卡牌独立关联技能UUID
重构SCardComp与SkillBoxComp的技能获取逻辑,修复技能图标与配置读取问题,修正组件参数注释
调整GameSet中的游戏地平线坐标,优化多个场景与UI预制体的布局位置
移除部分预制体中的冗余配置项,清理无效代码
2026-06-04 23:12:57 +08:00

389 lines
13 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 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 s_uuid = this.cardData.skill ?? this.card_uuid;
const skill = SkillSet[s_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 || `${s_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();
}
}