- 将SkillConfig.buffs字段改为直接存储BuffConf对象数组 - 移除预定义的BuffsList和相关导入引用 - 简化SCastSystem中buff应用逻辑,直接使用配置对象 - 移除CardComp中Buff/Debuff类型的图标获取逻辑 - 简化HeroAttrsComp调试日志,移除buff名称显示
474 lines
16 KiB
TypeScript
474 lines
16 KiB
TypeScript
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, SpecialCardList } from "../common/config/CardSet";
|
||
import { CardUseComp } from "./CardUseComp";
|
||
import { HeroInfo } from "../common/config/heroSet";
|
||
import { SkillSet } from "../common/config/SkillSet";
|
||
|
||
|
||
|
||
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 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;
|
||
this.isUsing = true;
|
||
const used = this.cardData;
|
||
mLogger.log(this.debugMode, "CardComp", "use card", {
|
||
uuid: used.uuid,
|
||
type: used.type
|
||
});
|
||
this.playUseDisappearAnim(() => {
|
||
this.cardUseComp?.onCardUsed(used);
|
||
this.clearAfterUse();
|
||
this.isUsing = false;
|
||
});
|
||
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;
|
||
this.restPosition = new Vec3(x, current.y, current.z);
|
||
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.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{
|
||
this.setLabel(this.name_node, `${SpecialCardList[this.card_uuid].name}Lv.${this.cardData.lv}`);
|
||
this.info_node.active = false;
|
||
this.oinfo_node.active = true;
|
||
this.oinfo_node.getChildByName("info").getComponent(Label).string = `${SpecialCardList[this.card_uuid].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 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));
|
||
}
|
||
|
||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||
reset() {
|
||
this.node.destroy();
|
||
}
|
||
}
|