Files
pixelheros/assets/script/game/map/SCardComp.ts
pan 30163e8dac refactor: 拆分技能卡配置到独立文件,优化抽卡逻辑
1. 新建SCardSet.ts单独管理技能卡池配置,将原CardSet.ts中的技能卡数据迁移
2. 重构多处业务代码,从SkillCardList而非CardPoolList获取技能卡配置
3. 简化技能卡抽卡逻辑,新增drawSkillCards工具函数统一处理抽卡
4. 修复TalentsComp中计算contentHeight的小语法问题
5. 新增技能卡商店显隐控制功能
2026-07-22 17:42:41 +08:00

414 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 } from "../common/config/CardSet";
import { SkillCardList } from "../common/config/SCardSet";
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);
// 派发 CardUsed 让 MissionCardComp 刷新整个技能卡池
oops.message.dispatchEvent(GameEvent.CardUsed, this);
});
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 = SkillCardList.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();
}
}