Files
pixelheros/assets/script/game/map/SCardComp.ts
panFD 107e7fde96 fix(map): 修复卡牌描述逻辑的优先级顺序
调整卡牌描述的获取逻辑,先处理驻场技能卡的描述获取,再 fallback 到其他配置来源,修正原有的优先级混乱问题
2026-06-20 12:42:45 +08:00

411 lines
14 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 { FieldSkillSet, 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.message.dispatchEvent(GameEvent.ShowSmallTip, "buy_coin");
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;
// 驻场技能卡描述取 FieldSkillSet其他卡牌取卡牌/技能配置
let desc = "";
if (this.cardData.field && this.cardData.field.length > 0) {
desc = FieldSkillSet[this.cardData.field[0]]?.info || "";
}
if (!desc) {
desc = skillCard?.info || skill?.info || this.cardData?.info || "";
}
// 与 HlistComp 保持一致:优先查找名为 "info" 的子节点上的 Label
const infoLabel = this.info_node.getChildByName("info")?.getComponent(Label)
|| this.info_node.getComponent(Label)
|| this.info_node.getComponentInChildren(Label);
if (infoLabel) infoLabel.string = desc;
}
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);
// 驻场技能卡(skill=undefined 但有 field)使用 FieldSkillSet 中的图标
let iconId: string;
if (!this.cardData.skill && this.cardData.field && this.cardData.field.length > 0) {
const fieldUuid = this.cardData.field[0];
iconId = FieldSkillSet[fieldUuid]?.icon || `${fieldUuid}`;
} else {
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();
}
}