Files
pixelheros/assets/script/game/map/CardComp.ts
walkpan ec0c9c97f8 fix: 调整卡牌使用后回调的执行顺序
确保在清理状态后执行 onCardUsed 回调,避免状态不一致问题
2026-03-29 21:55:32 +08:00

547 lines
19 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.
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources } 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, SpecialRefreshCardList, SpecialUpgradeCardList } from "../common/config/CardSet";
import { CardUseComp } from "./CardUseComp";
import { HeroInfo } from "../common/config/heroSet";
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 { UIID } from "../common/config/GameUIConfig";
const { ccclass, property } = _decorator;
/** 视图层对象 */
@ccclass('CardComp')
@ecs.register('CardComp', false)
export class CardComp extends CCComp {
private debugMode: boolean = true;
/** 锁定态图标节点(显示时表示本槽位锁定) */
@property(Node)
Lock: Node = null!
@property(Node)
unLock: Node = null!
@property(Node)
info_node=null!
@property(Node)
oinfo_node=null!
@property(Node)
name_node=null!
@property(Node)
icon_node=null!
@property(Node)
cost_node=null!
card_cost:number=0
card_type:CardType=CardType.Hero
card_uuid:number=0
/** 是否处于锁定状态(锁定且有卡时,抽卡分发会被跳过) */
private isLocked: boolean = false;
/** 图标图集缓存(后续接图标资源时直接复用) */
private uiconsAtlas: SpriteAtlas | null = null;
/** 当前槽位承载的卡牌数据null 表示空槽 */
private cardData: CardConfig | null = null;
private readonly dragUseThreshold: number = 70;
private touchStartY: 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 cardUseComp: CardUseComp | null = null;
private iconVisualToken: number = 0;
onLoad() {
/** 初始阶段只做UI状态准备不触发业务逻辑 */
this.bindEvents();
this.restPosition = this.node.position.clone();
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
this.cardUseComp = this.resolveCardUseComp();
this.opacityComp.opacity = 255;
this.updateLockUI();
this.applyEmptyUI();
}
onDestroy() {
this.unbindEvents();
}
init(){
this.onMissionStart();
}
/** 游戏开始初始化 */
onMissionStart() {
}
/** 游戏结束清理 */
onMissionEnd() {
}
start() {
/** 单卡节点常驻,由数据控制显示内容 */
this.node.active = true;
}
/** 兼容旧接口:外部通过该入口更新卡牌 */
updateCardInfo(card:Node, data: CardConfig){
this.applyDrawCard(data);
}
private updateIcon(node: Node, iconId: string) {
if (!node || !iconId) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (!sprite) return;
if (this.uiconsAtlas) {
const frame = this.uiconsAtlas.getSpriteFrame(iconId);
if (frame) {
sprite.spriteFrame = frame;
} else {
sprite.spriteFrame = null;
}
return;
}
resources.load("gui/uicons", SpriteAtlas, (err, atlas) => {
if (err || !atlas) {
mLogger.log(this.debugMode, "CardComp", "load uicons atlas failed", err);
return;
}
this.uiconsAtlas = atlas;
const frame = atlas.getSpriteFrame(iconId);
if (frame) {
sprite.spriteFrame = frame;
} else {
sprite.spriteFrame = null;
}
});
}
/** 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发) */
updateCardData(index: number, data: CardConfig) {
this.applyDrawCard(data);
}
/** 兼容按钮回调入口:触发单卡使用 */
selectCard(e: any, index: string) {
this.useCard();
}
/**
* 关闭界面
*/
close() {
}
/** 抽卡分发入口:返回 true 表示本次已成功接收新卡 */
applyDrawCard(data: CardConfig | null): boolean {
if (!data) return false;
/** 锁定且已有旧卡时,跳过本次刷新,保持老卡 */
if (this.isLocked && this.cardData) {
mLogger.log(this.debugMode, "CardComp", "slot locked, skip update", this.card_uuid);
return false;
}
this.cardData = data;
this.card_uuid = data.uuid;
this.card_type = data.type;
this.card_cost = data.cost;
this.node.active = true;
this.applyCardUI();
this.playRefreshAnim();
mLogger.log(this.debugMode, "CardComp", "card updated", {
uuid: this.card_uuid,
type: this.card_type,
cost: this.card_cost
});
return true;
}
useCard(): CardConfig | null {
if (!this.cardData || this.isUsing) return null;
const cardCost = Math.max(0, Math.floor(this.cardData.cost ?? 0));
const currentCoin = this.getMissionCoin();
if (currentCoin < cardCost) {
oops.gui.toast(`金币不足,召唤需要${cardCost}`);
this.playReboundAnim();
mLogger.log(this.debugMode, "CardComp", "use card coin not enough", {
uuid: this.cardData.uuid,
type: this.cardData.type,
cardCost,
currentCoin
});
return null;
}
if (this.cardData.type === CardType.Hero) {
const guard = {
cancel: false,
reason: "",
uuid: this.cardData.uuid,
hero_lv: this.cardData.hero_lv
};
oops.message.dispatchEvent(GameEvent.UseHeroCard, guard);
if (guard.cancel) {
this.playReboundAnim();
return null;
}
}
this.setMissionCoin(currentCoin - cardCost);
oops.message.dispatchEvent(GameEvent.CoinAdd, {
syncOnly: true,
delta: -cardCost
});
this.isUsing = true;
const used = this.cardData;
mLogger.log(this.debugMode, "CardComp", "use card", {
uuid: used.uuid,
type: used.type,
cost: cardCost,
leftCoin: this.getMissionCoin()
});
this.playUseDisappearAnim(() => {
this.clearAfterUse();
this.isUsing = false;
this.cardUseComp?.onCardUsed(used);
});
return used;
}
/** 查询槽位是否有卡 */
hasCard(): boolean {
return !!this.cardData;
}
/** 外部设置锁定态 */
setLocked(value: boolean) {
this.isLocked = value;
this.updateLockUI();
}
/** 外部读取当前锁定态 */
isSlotLocked(): boolean {
return this.isLocked;
}
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.card_type = CardType.Hero;
this.isLocked = false;
this.isDragging = false;
this.isUsing = false;
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(1, 1, 1));
this.updateLockUI();
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.card_type = CardType.Hero;
this.isLocked = false;
this.isDragging = false;
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(1, 1, 1));
this.updateLockUI();
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);
this.Lock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this);
this.unLock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this);
}
/** 解绑触控,防止节点销毁后残留回调 */
private unbindEvents() {
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);
this.Lock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
this.unLock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
}
private onCardTouchStart(event: EventTouch) {
if (!this.cardData || this.isUsing) return;
this.touchStartY = event.getUILocation().y;
this.isDragging = true;
}
private onCardTouchMove(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const currentY = event.getUILocation().y;
const deltaY = Math.max(0, currentY - this.touchStartY);
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
}
/** 上拉超过阈值才视为使用,否则回弹原位 */
private onCardTouchEnd(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const endY = event.getUILocation().y;
const deltaY = endY - this.touchStartY;
this.isDragging = false;
if (deltaY >= this.dragUseThreshold) {
this.useCard();
return;
}
this.openHeroInfoIBox();
this.playReboundAnim();
}
private onCardTouchCancel() {
if (!this.isDragging || this.isUsing) return;
this.isDragging = false;
this.playReboundAnim();
}
/** 点击锁控件:切换锁态;空槽不允许锁定 */
private onToggleLock(event?: EventTouch) {
if (!this.cardData) return;
this.isLocked = !this.isLocked;
this.updateLockUI();
mLogger.log(this.debugMode, "CardComp", "toggle lock", {
uuid: this.card_uuid,
locked: this.isLocked
});
const stopPropagation = (event as any)?.stopPropagation;
if (typeof stopPropagation === "function") {
stopPropagation.call(event);
}
}
/** 根据锁态刷新 Lock / unLock 显示Lock=可点击上锁unLock=可点击解锁) */
private updateLockUI() {
// if (this.Lock) this.Lock.active = !this.isLocked;
// if (this.unLock) this.unLock.active = this.isLocked;
}
/** 根据当前 cardData 渲染卡面文字与图标 */
private applyCardUI() {
if (!this.cardData) {
this.applyEmptyUI();
return;
}
this.iconVisualToken += 1;
if (this.opacityComp) this.opacityComp.opacity = 255;
this.node.setPosition(this.restPosition);
if(this.card_type===CardType.Hero){
this.setLabel(this.name_node, `${HeroInfo[this.card_uuid].name}Lv.${this.cardData.hero_lv}`);
this.info_node.active = true;
this.oinfo_node.active = false;
this.info_node.getChildByName("ap").getChildByName("val").getComponent(Label).string = `${HeroInfo[this.card_uuid].ap*this.cardData.hero_lv}`;
this.info_node.getChildByName("hp").getChildByName("val").getComponent(Label).string = `${HeroInfo[this.card_uuid].hp*this.cardData.hero_lv}`;
}else{
const specialCard = this.card_type === CardType.SpecialUpgrade
? SpecialUpgradeCardList[this.card_uuid]
: SpecialRefreshCardList[this.card_uuid];
this.setLabel(this.name_node, `${specialCard?.name || ""}Lv.${this.cardData.lv}`);
this.info_node.active = false;
this.oinfo_node.active = true;
this.oinfo_node.getChildByName("info").getComponent(Label).string = `${specialCard?.info || ""}`;
}
this.setLabel(this.cost_node, `${this.card_cost}`);
const iconNode = this.icon_node as Node;
if (this.card_type === CardType.Hero) {
this.updateHeroAnimation(iconNode, this.card_uuid, this.iconVisualToken);
return;
}
this.clearIconAnimation(iconNode);
const iconId = this.resolveCardIconId(this.card_type, this.card_uuid);
if (iconId) {
this.updateIcon(iconNode, iconId);
} else {
const sprite = iconNode?.getComponent(Sprite) || iconNode?.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
}
}
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: this.restPosition,
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() {
this.iconVisualToken += 1;
this.setLabel(this.name_node, "");
this.setLabel(this.cost_node, "");
if (this.info_node) this.info_node.active = false;
if (this.oinfo_node) this.oinfo_node.active = false;
this.clearIconAnimation(this.icon_node as Node);
const sprite = this.icon_node?.getComponent(Sprite) || this.icon_node?.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
}
/** 安全设置文本,兼容节点上或子节点上的 Label */
private setLabel(node: Node | null, value: string) {
if (!node) return;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
if (label) label.string = value;
}
private resolveCardUseComp(): CardUseComp | null {
let current: Node | null = this.node.parent;
while (current) {
const comp = current.getComponent(CardUseComp);
if (comp) return comp;
current = current.parent;
}
mLogger.log(this.debugMode, "CardComp", "CardUseComp not found for", this.node.name);
return null;
}
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}`;
}
return `${uuid}`;
}
private openHeroInfoIBox() {
if (!this.cardData) return;
if (this.cardData.type !== CardType.Hero) return;
const hero = HeroInfo[this.cardData.uuid];
if (!hero) return;
const heroLv = Math.max(1, Math.floor(this.cardData.hero_lv ?? hero.lv ?? 1));
oops.gui.remove(UIID.IBox);
oops.gui.open(UIID.IBox, {
heroUuid: this.cardData.uuid,
heroLv
});
}
private updateHeroAnimation(node: Node, uuid: number, token: number) {
const sprite = node?.getComponent(Sprite) || node?.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
const hero = HeroInfo[uuid];
if (!hero) return;
const anim = node.getComponent(Animation) || node.addComponent(Animation);
this.clearAnimationClips(anim);
const path = `game/heros/hero/${hero.path}/idle`;
resources.load(path, AnimationClip, (err, clip) => {
if (err || !clip) {
mLogger.log(this.debugMode, "CardComp", `load hero animation failed ${uuid}`, err);
return;
}
if (token !== this.iconVisualToken || !this.cardData || this.card_type !== CardType.Hero || this.card_uuid !== uuid) {
return;
}
this.clearAnimationClips(anim);
anim.addClip(clip);
anim.play("idle");
});
}
private clearIconAnimation(node: Node) {
const anim = node?.getComponent(Animation);
if (!anim) return;
anim.stop();
this.clearAnimationClips(anim);
}
private clearAnimationClips(anim: Animation) {
const clips = (anim as any).clips as AnimationClip[] | undefined;
if (!clips || clips.length === 0) return;
[...clips].forEach(clip => anim.removeClip(clip, true));
}
private getMissionCoin(): number {
const missionData = smc?.vmdata?.mission_data;
return Math.max(0, Math.floor(missionData?.coin ?? 0));
}
private setMissionCoin(value: number) {
const missionData = smc?.vmdata?.mission_data;
if (!missionData) return;
missionData.coin = Math.max(0, Math.floor(value));
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.node.destroy();
}
}