feat(map): 优化战斗流程与卡牌系统交互

1.  第一波准备阶段首个英雄召唤后自动开战,移除手动点击开始按钮
2.  重构卡牌交互逻辑,移除上划召唤改为点击召唤
3.  新增英雄卡池显隐切换功能
4.  调整卡牌预制体尺寸与布局
5.  移除特定波次技能卡弹窗逻辑
6.  简化引导流程,不再弹出Guide4
This commit is contained in:
panFD
2026-07-20 22:20:18 +08:00
parent 15ab2f7b0f
commit 76085c3ecd
5 changed files with 18934 additions and 13689 deletions

View File

@@ -3,13 +3,13 @@
* @description 单张卡牌槽位组件UI 视图层)
*
* 职责:
* 1. 管理单个卡牌槽位的 **显示** 和 **交互**触摸拖拽 / 点击使用 / 锁定切换)。
* 1. 管理单个卡牌槽位的 **显示** 和 **交互**点击选中 / 召唤按钮 / 锁定切换)。
* 2. 接收来自 MissionCardComp 分发的 CardConfig 数据,渲染对应卡面(英雄 / 技能 / 特殊卡)。
* 3. 在玩家触发"使用卡牌"操作时,执行 **费用扣除 → 动画表现 → 效果分发** 的完整流程。
*
* 关键设计:
* - 槽位可 **锁定**:锁定后刷新卡池不会覆盖旧卡,由 `isLocked` 控制。
* - 卡牌使用支持 **上划手势** 与 **点击** 两种触发方式
* - 卡牌使用通过 **点击卡牌 → 显示召唤按钮 → 点击召唤按钮** 触发
* - 英雄卡图标使用 AnimationClip 动态加载 idle 动画;技能 / 特殊卡使用 SpriteAtlas 静态图标。
* - 通过 `iconVisualToken` 防止异步加载资源回调与当前显示不一致(竞态保护)。
*
@@ -109,13 +109,7 @@ export class CardComp extends CCComp {
private isLocked: boolean = false;
/** 当前槽位承载的卡牌数据null 表示空槽 */
private cardData: CardConfig | null = null;
/** 上划使用阈值(像素):拖拽距离 >= 此值视为"使用卡牌" */
private readonly dragUseThreshold: number = 70;
/** 触摸起始 Y 坐标,用于计算拖拽距离 */
private touchStartY: number = 0;
/** 触摸起始 X 坐标,用于点击判断 */
private touchStartX: number = 0;
/** 当前是否正在拖拽 */
/** 当前是否正在触摸卡牌(用于阻止 setSlotPosition 等重置位置) */
private isDragging: boolean = false;
/** 当前是否正在执行"使用"流程(防止重复触发) */
private isUsing: boolean = false;
@@ -135,8 +129,6 @@ export class CardComp extends CCComp {
* 防止快速切卡时旧回调错误覆盖新图标。
*/
private iconVisualToken: number = 0;
/** 视为"点击"的最大位移阈值(像素),小于该值视为点击而非拖拽 */
private readonly tapMoveThreshold: number = 10;
// ======================== 生命周期 ========================
@@ -491,60 +483,35 @@ export class CardComp extends CCComp {
// ======================== 触摸交互 ========================
/** 触摸开始:记录起点坐标,进入拖拽状态 */
/**
* 触摸开始:标记正在触摸。
* 注:已移除上划手势召唤,卡牌不跟随手指移动,仅通过点击显示 call_btn 召唤。
*/
private onCardTouchStart(event: EventTouch) {
if (!this.cardData || this.isUsing) return;
this.touchStartY = event.getUILocation().y;
this.touchStartX = event.getUILocation().x;
this.isDragging = true;
}
/**
* 触摸移动:跟随手指向上偏移卡牌仅允许向上拖拽deltaY < 0 被 clamp 为 0
* 触摸移动:不做任何处理。
* 卡牌不跟随手指位移,避免点击选中时产生位移。
*/
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);
return;
}
/**
* 触摸结束:
* - 上拉距离 >= dragUseThreshold → 视为"使用卡牌"
* - 位移很小(点击)→ 触发 onCardTap显示 call_btn / 打开信息面板 / 通知其他卡牌联动
* 触摸结束:视为点击,触发 onCardTap显示召唤按钮 / 打开信息面板 / 通知其他卡牌联动)。
*/
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 (deltaY >= this.dragUseThreshold) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
}
return;
}
// 位移小于阈值视为"点击"
if (Math.abs(deltaY) < this.tapMoveThreshold && Math.abs(deltaX) < this.tapMoveThreshold) {
this.onCardTap();
return;
}
this.playReboundAnim();
this.onCardTap();
}
/** 触摸取消:回弹至原位 */
/** 触摸取消:仅重置触摸状态 */
private onCardTouchCancel() {
if (!this.isDragging || this.isUsing) return;
this.isDragging = false;
this.playReboundAnim();
}
/**
@@ -552,9 +519,10 @@ export class CardComp extends CCComp {
* 1. 显示本卡的召唤按钮 call_btn。
* 2. 派发 CardSelected 事件,其他卡牌收到后隐藏各自的 call_btn。
* 3. 英雄卡打开 HInfo 信息面板(点击查看,替代原长按)。
* 注不重置位置。applyCardUI 中 widget.updateAlignment() 会让节点脱离 restPosition
* 这里若强制 setPosition(restPosition) 会导致可见的跳位。
*/
private onCardTap() {
this.playReboundAnim();
this.showCallBtn();
oops.message.dispatchEvent(GameEvent.CardSelected, this);
// 英雄卡打开信息面板(点击替代长按)
@@ -694,7 +662,8 @@ export class CardComp extends CCComp {
// ---- 按卡牌类型渲染具体内容 ----
const uiTrans = this.node.getComponent(UITransform);
if (uiTrans) {
uiTrans.setContentSize(170, 230);
// 卡牌尺寸由编辑器场景节点决定,不再硬编码
// uiTrans.setContentSize(170, 230);
const widget = this.node.getComponent(Widget);
if (widget) widget.updateAlignment();
}
@@ -702,14 +671,14 @@ export class CardComp extends CCComp {
if (this.BG_node) {
const bgTrans = this.BG_node.getComponent(UITransform);
if (bgTrans) {
bgTrans.setContentSize(170, 230);
// bgTrans.setContentSize(170, 230);
const widget = this.BG_node.getComponent(Widget);
if (widget) widget.updateAlignment();
}
this.BG_node.children.forEach(child => {
const childTrans = child.getComponent(UITransform);
if (childTrans) {
childTrans.setContentSize(170, 230);
// childTrans.setContentSize(170, 230);
const widget = child.getComponent(Widget);
if (widget) widget.updateAlignment();
}
@@ -720,7 +689,7 @@ export class CardComp extends CCComp {
if (hbNode) {
const hbTrans = hbNode.getComponent(UITransform);
if (hbTrans) {
hbTrans.setContentSize(170, 230);
// hbTrans.setContentSize(170, 230);
const widget = hbNode.getComponent(Widget);
if (widget) widget.updateAlignment();
}
@@ -832,12 +801,15 @@ export class CardComp extends CCComp {
.start();
}
/** 回弹动画:从当前位置平滑回到静止位并恢复缩放 */
/**
* 回弹动画:仅恢复缩放到 1不改变位置。
* Why: applyCardUI 中 widget.updateAlignment() 会让节点脱离 restPosition
* 若强行 setPosition(restPosition) 会产生肉眼可见的跳位(如金币不足时反馈)。
*/
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();
@@ -845,14 +817,15 @@ export class CardComp extends CCComp {
/**
* 使用消失动画:
* - 节点向上移动 120px 并缩小至 0.8
* - 节点从当前位置向上移动 120pxX 轴保持当前值不变,不使用 restPosition.x
* - 同时 UIOpacity 渐隐至 0
* - 动画完成后调用 onComplete 回调
*
* @param onComplete 动画完成后的回调
*/
private playUseDisappearAnim(onComplete: () => void) {
const targetPos = new Vec3(this.restPosition.x, this.restPosition.y + 120, this.restPosition.z);
const current = this.node.position;
const targetPos = new Vec3(current.x, current.y + 120, current.z);
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
@@ -863,8 +836,7 @@ export class CardComp extends CCComp {
}
tween(this.node)
.to(0.18, {
position: targetPos,
scale: new Vec3(0.8, 0.8, 1)
position: targetPos
})
.call(onComplete)
.start();
@@ -877,21 +849,21 @@ export class CardComp extends CCComp {
private applyEmptyUI() {
const uiTrans = this.node.getComponent(UITransform);
if (uiTrans) {
uiTrans.setContentSize(170, 230);
// uiTrans.setContentSize(170, 230);
const widget = this.node.getComponent(Widget);
if (widget) widget.updateAlignment();
}
if (this.BG_node) {
const bgTrans = this.BG_node.getComponent(UITransform);
if (bgTrans) {
bgTrans.setContentSize(170, 230);
// bgTrans.setContentSize(170, 230);
const widget = this.BG_node.getComponent(Widget);
if (widget) widget.updateAlignment();
}
this.BG_node.children.forEach(child => {
const childTrans = child.getComponent(UITransform);
if (childTrans) {
childTrans.setContentSize(170, 230);
// childTrans.setContentSize(170, 230);
const widget = child.getComponent(Widget);
if (widget) widget.updateAlignment();
}
@@ -901,7 +873,7 @@ export class CardComp extends CCComp {
if (hbNode) {
const hbTrans = hbNode.getComponent(UITransform);
if (hbTrans) {
hbTrans.setContentSize(170, 230);
// hbTrans.setContentSize(170, 230);
const widget = hbNode.getComponent(Widget);
if (widget) widget.updateAlignment();
}