4 Commits

Author SHA1 Message Date
panFD
551715ef44 refactor(map): 注释掉战斗盒子节点的显式显隐逻辑
暂时禁用了fight节点下fbox的自动显隐操作,避免不必要的UI状态干扰
2026-07-20 22:35:28 +08:00
panFD
1612e02790 refactor(map): 重构抽卡逻辑与卡牌渲染规则
1.  实现升级卡轮询机制,按英雄UUID升序循环切换必出升级卡
2.  重构buildDrawCards抽卡流程,分离必出升级卡与随机抽取逻辑
3.  移除抽卡池内的刷新功能卡,调整特殊卡渲染逻辑
4.  统一英雄卡与升级卡的交互与渲染表现
2026-07-20 22:35:21 +08:00
panFD
cff7b4703f refactor(uiPrefab): 调整任务和英雄顶部UI的布局与组件配置
统一修改mission.prefab和top.prefab的节点激活状态、位置、尺寸,替换旧的精灵/文本组件为正确配置,清理冗余节点和组件,优化UI显示效果
2026-07-20 22:26:28 +08:00
panFD
76085c3ecd feat(map): 优化战斗流程与卡牌系统交互
1.  第一波准备阶段首个英雄召唤后自动开战,移除手动点击开始按钮
2.  重构卡牌交互逻辑,移除上划召唤改为点击召唤
3.  新增英雄卡池显隐切换功能
4.  调整卡牌预制体尺寸与布局
5.  移除特定波次技能卡弹窗逻辑
6.  简化引导流程,不再弹出Guide4
2026-07-20 22:20:18 +08:00
7 changed files with 22339 additions and 16706 deletions

View File

@@ -37,11 +37,11 @@
"_active": true,
"_components": [
{
"__id__": 104
"__id__": 98
}
],
"_prefab": {
"__id__": 106
"__id__": 100
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -1805,27 +1805,24 @@
},
{
"__id__": 87
},
{
"__id__": 93
}
],
"_active": false,
"_active": true,
"_components": [
{
"__id__": 99
"__id__": 93
},
{
"__id__": 101
"__id__": 95
}
],
"_prefab": {
"__id__": 103
"__id__": 97
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 14.78,
"x": -26.182,
"y": -0.337,
"z": 0
},
"_lrot": {
@@ -1853,7 +1850,7 @@
},
{
"__type__": "cc.Node",
"_name": "lv1",
"_name": "Label_Flag_01_Single_Blue",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
@@ -1874,7 +1871,7 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -27,
"x": 0,
"y": 0,
"z": 0
},
@@ -1887,8 +1884,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"x": 0.2,
"y": 0.14,
"z": 1
},
"_mobility": 0,
@@ -1915,8 +1912,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 15,
"height": 15
"width": 52,
"height": 75
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -1927,7 +1924,7 @@
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "e0fzAAgU5Ana+yMSJ8kW3S"
"fileId": "a63DzZmvxEvKGHs56RWbEy"
},
{
"__type__": "cc.Sprite",
@@ -1952,12 +1949,12 @@
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@5599f",
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@5c7a0",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_sizeMode": 1,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
@@ -1972,7 +1969,7 @@
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "01JRJpjTFNULsk9C1ok6Sn"
"fileId": "569j93a6xMIrRxlzI33n0i"
},
{
"__type__": "cc.PrefabInfo",
@@ -1982,14 +1979,14 @@
"asset": {
"__id__": 0
},
"fileId": "71Em0LB4dL27DqYQ9OkFiQ",
"fileId": "c1FCc3phFGUq3G390z6qe1",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "lv2",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
@@ -2051,8 +2048,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 15,
"height": 15
"width": 4.38,
"height": 40
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -2063,10 +2060,10 @@
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "21FzPtdO5I5pUGPECHUDeL"
"fileId": "cd5P7lbVFAg70ze2pGxgkP"
},
{
"__type__": "cc.Sprite",
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
@@ -2087,164 +2084,54 @@
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@5599f",
"__expectedType__": "cc.SpriteFrame"
"_string": "1",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 15,
"_fontSize": 15,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": {
"__uuid__": "eca7fc51-87d0-4bb0-89e1-aa9c39411bc6",
"__expectedType__": "cc.BitmapFont"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "ddFSRJWHlDIYzffFeulvvt"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "dfOByluMhG0ZeObswL8kPn",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "lv3",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 80
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 94
},
{
"__id__": 96
}
],
"_prefab": {
"__id__": 98
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 27,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 93
},
"_enabled": true,
"__prefab": {
"__id__": 95
},
"_contentSize": {
"__type__": "cc.Size",
"width": 15,
"height": 15
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "a14AlTLO5E5bIpLQVQ3hl/"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 93
},
"_enabled": true,
"__prefab": {
"__id__": 97
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"_isSystemFontUsed": false,
"_spacingX": 0,
"_isItalic": false,
"_isBold": false,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": true,
"_outlineColor": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@5599f",
"__expectedType__": "cc.SpriteFrame"
"_outlineWidth": 1,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
"x": 2,
"y": 2
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "0fB8B34hpFTKX8wY0yQ1Dd"
"fileId": "acRiOZGzFAqo50YQ9wJHmy"
},
{
"__type__": "cc.PrefabInfo",
@@ -2254,7 +2141,7 @@
"asset": {
"__id__": 0
},
"fileId": "70CJhUD51O/Yn7lWbJeuyp",
"fileId": "bdfy91W21F0omKxp2GjJM4",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
@@ -2269,12 +2156,12 @@
},
"_enabled": true,
"__prefab": {
"__id__": 100
"__id__": 94
},
"_contentSize": {
"__type__": "cc.Size",
"width": 69,
"height": 15
"width": 10,
"height": 10
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -2295,9 +2182,9 @@
"node": {
"__id__": 80
},
"_enabled": true,
"_enabled": false,
"__prefab": {
"__id__": 102
"__id__": 96
},
"_resizeMode": 1,
"_layoutType": 1,
@@ -2348,7 +2235,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 105
"__id__": 99
},
"_contentSize": {
"__type__": "cc.Size",

File diff suppressed because it is too large Load Diff

View File

@@ -58,14 +58,14 @@
"_active": true,
"_components": [
{
"__id__": 323
"__id__": 327
},
{
"__id__": 325
"__id__": 329
}
],
"_prefab": {
"__id__": 327
"__id__": 331
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -515,8 +515,8 @@
],
"value": {
"__type__": "cc.Size",
"width": 200,
"height": 270
"width": 170,
"height": 230
}
},
{
@@ -766,8 +766,8 @@
],
"value": {
"__type__": "cc.Size",
"width": 200,
"height": 270
"width": 170,
"height": 230
}
},
{
@@ -786,8 +786,8 @@
],
"value": {
"__type__": "cc.Size",
"width": 400,
"height": 540
"width": 340,
"height": 460
}
},
{
@@ -806,8 +806,8 @@
],
"value": {
"__type__": "cc.Size",
"width": 392,
"height": 528
"width": 332,
"height": 448
}
},
{
@@ -826,7 +826,7 @@
],
"value": {
"__type__": "cc.Size",
"width": 170,
"width": 140,
"height": 140
}
},
@@ -846,7 +846,7 @@
],
"value": {
"__type__": "cc.Size",
"width": 340,
"width": 280,
"height": 280
}
},
@@ -867,7 +867,7 @@
"value": {
"__type__": "cc.Vec3",
"x": 0,
"y": 50,
"y": 30,
"z": 0
}
},
@@ -1926,8 +1926,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 200,
"height": 270
"width": 170,
"height": 230
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -2071,7 +2071,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 50,
"y": 30,
"z": 0
},
"_lrot": {
@@ -2247,7 +2247,7 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 170,
"width": 140,
"height": 140
},
"_anchorPoint": {
@@ -2413,8 +2413,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 79,
"y": 114,
"x": 64,
"y": 94,
"z": 0
},
"_lrot": {
@@ -3050,8 +3050,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -56.79599999999999,
"y": -114.26499999999999,
"x": -41.79599999999999,
"y": -94.265,
"z": 0
},
"_lrot": {
@@ -3708,8 +3708,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 55.925,
"y": -114.26499999999999,
"x": 40.925,
"y": -94.265,
"z": 0
},
"_lrot": {
@@ -4516,7 +4516,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -41.72399999999999,
"y": -61.72399999999999,
"z": 0
},
"_lrot": {
@@ -4723,8 +4723,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -79,
"y": 114,
"x": -64,
"y": 94,
"z": 0
},
"_lrot": {
@@ -5683,22 +5683,25 @@
"__id__": 308
},
{
"__id__": 314
"__id__": 316
}
],
"_active": false,
"_active": true,
"_components": [
{
"__id__": 320
"__id__": 322
},
{
"__id__": 324
}
],
"_prefab": {
"__id__": 322
"__id__": 326
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -23.213,
"y": -142.978,
"z": 0
},
"_lrot": {
@@ -5740,10 +5743,13 @@
},
{
"__id__": 311
},
{
"__id__": 313
}
],
"_prefab": {
"__id__": 313
"__id__": 315
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -5760,8 +5766,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.8,
"y": 0.8,
"x": 0.5,
"y": 0.5,
"z": 1
},
"_mobility": 0,
@@ -5788,8 +5794,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 185,
"height": 81
"width": 340,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -5828,9 +5834,9 @@
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@b2501",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_type": 1,
"_fillType": 0,
"_sizeMode": 1,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
@@ -5847,6 +5853,42 @@
"__type__": "cc.CompPrefabInfo",
"fileId": "08MkVcRXlJh64uV97FC5zt"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 308
},
"_enabled": true,
"__prefab": {
"__id__": 314
},
"_alignFlags": 40,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 340,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "e0NAQnwtdBJbK3eTqlXN1t"
},
{
"__type__": "cc.PrefabInfo",
"root": {
@@ -5872,14 +5914,14 @@
"_active": true,
"_components": [
{
"__id__": 315
"__id__": 317
},
{
"__id__": 317
"__id__": 319
}
],
"_prefab": {
"__id__": 319
"__id__": 321
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -5916,11 +5958,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 314
"__id__": 316
},
"_enabled": true,
"__prefab": {
"__id__": 316
"__id__": 318
},
"_contentSize": {
"__type__": "cc.Size",
@@ -5944,11 +5986,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 314
"__id__": 316
},
"_enabled": true,
"__prefab": {
"__id__": 318
"__id__": 320
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -6029,11 +6071,11 @@
},
"_enabled": true,
"__prefab": {
"__id__": 321
"__id__": 323
},
"_contentSize": {
"__type__": "cc.Size",
"width": 148,
"width": 170,
"height": 64.8
},
"_anchorPoint": {
@@ -6047,6 +6089,42 @@
"__type__": "cc.CompPrefabInfo",
"fileId": "94GKNoSM1MwIqd0zy183DD"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 307
},
"_enabled": true,
"__prefab": {
"__id__": 325
},
"_alignFlags": 40,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 170,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "89gxeFO+ZD9q7ucM1+gj9B"
},
{
"__type__": "cc.PrefabInfo",
"root": {
@@ -6070,12 +6148,12 @@
},
"_enabled": true,
"__prefab": {
"__id__": 324
"__id__": 328
},
"_contentSize": {
"__type__": "cc.Size",
"width": 200,
"height": 270
"width": 170,
"height": 230
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -6098,7 +6176,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 326
"__id__": 330
},
"Lock": null,
"unLock": null,

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,13 @@
* @description 单张卡牌槽位组件UI 视图层)
*
* 职责:
* 1. 管理单个卡牌槽位的 **显示** 和 **交互**触摸拖拽 / 点击使用 / 锁定切换)。
* 1. 管理单个卡牌槽位的 **显示** 和 **交互**点击选中 / 召唤按钮 / 锁定切换)。
* 2. 接收来自 MissionCardComp 分发的 CardConfig 数据,渲染对应卡面(英雄 / 技能 / 特殊卡)。
* 3. 在玩家触发"使用卡牌"操作时,执行 **费用扣除 → 动画表现 → 效果分发** 的完整流程。
*
* 关键设计:
* - 槽位可 **锁定**:锁定后刷新卡池不会覆盖旧卡,由 `isLocked` 控制。
* - 卡牌使用支持 **上划手势** 与 **点击** 两种触发方式
* - 卡牌使用通过 **点击卡牌 → 显示召唤按钮 → 点击召唤按钮** 触发
* - 英雄卡图标使用 AnimationClip 动态加载 idle 动画;技能 / 特殊卡使用 SpriteAtlas 静态图标。
* - 通过 `iconVisualToken` 防止异步加载资源回调与当前显示不一致(竞态保护)。
*
@@ -23,7 +23,7 @@ import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Input, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, input, resources, Light, 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, SpecialRefreshCardList, SpecialUpgradeCardList, CKind, CardPoolList } from "../common/config/CardSet";
import { CardConfig, CardType, SpecialRefreshCardList, CKind, CardPoolList } from "../common/config/CardSet";
import { CardBgComp } from "./CardBgComp";
import { HeroInfo } from "../common/config/heroSet";
import { SkillSet } from "../common/config/SkillSet";
@@ -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,14 +519,16 @@ 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);
// 英雄卡打开信息面板(点击替代长按)
if (this.card_type === CardType.Hero) {
const heroUuid = this.card_uuid;
// 英雄卡 / 升级卡打开信息面板(点击替代长按)
const isUpgradeCard = this.card_type === CardType.SpecialUpgrade && !!this.cardData?.target_hero_uuid;
if (this.card_type === CardType.Hero || isUpgradeCard) {
const heroUuid = isUpgradeCard ? (this.cardData!.target_hero_uuid as number) : this.card_uuid;
const heroLv = Math.max(1, this.cardData?.hero_lv ?? 1);
const poolLv = Math.max(1, this.cardData?.base_pool_lv ?? this.cardData?.pool_lv ?? 1);
oops.gui.remove(UIID.HInfo);
@@ -670,8 +639,15 @@ export class CardComp extends CCComp {
this.node.setPosition(this.restPosition.x, this.restPosition.y, this.restPosition.z);
// 渲染参数动态升级卡SpecialUpgrade + target_hero_uuid按英雄卡样式渲染
// 使用 target_hero_uuid 对应的英雄信息(名称/AP/HP/动画),等级显示为目标等级。
const isUpgradeCard = this.card_type === CardType.SpecialUpgrade && !!this.cardData.target_hero_uuid;
const renderType: CardType = isUpgradeCard ? CardType.Hero : this.card_type;
const renderUuid: number = isUpgradeCard ? (this.cardData.target_hero_uuid as number) : this.card_uuid;
const renderKind: CKind = isUpgradeCard ? CKind.Hero : this.cardData.kind;
// ---- 卡牌种类标识(近战 / 远程 / 辅助等) ----
const kindName = CKind[this.cardData.kind];
const kindName = CKind[renderKind];
if (this.BG_node) {
const bgLv = this.cardData.base_pool_lv ?? this.cardData.pool_lv;
@@ -694,7 +670,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 +679,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 +697,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();
}
@@ -745,8 +722,8 @@ export class CardComp extends CCComp {
if (widget) widget.updateAlignment();
}
if (this.card_type === CardType.Hero) {
const hero = HeroInfo[this.card_uuid];
if (renderType === CardType.Hero) {
const hero = HeroInfo[renderUuid];
const heroLv = Math.max(1, this.cardData.hero_lv ?? hero?.lv ?? 1);
this.setLabel(this.name_node, `${hero?.name || ""}`);
if (this.lvl_node) {
@@ -764,27 +741,14 @@ export class CardComp extends CCComp {
if (this.info_node) this.info_node.active = false;
} else {
if (this.lvl_node) this.lvl_node.node.active = false;
// 动态升级卡:显示对应英雄名 + "升级"后缀 + 目标等级
if (this.card_type === CardType.SpecialUpgrade && this.cardData.target_hero_uuid) {
const targetHero = HeroInfo[this.cardData.target_hero_uuid];
const targetLv = Math.max(1, Math.floor(this.cardData.hero_lv ?? 1));
this.setLabel(this.name_node, `${targetHero?.name || ""} 升级`);
if (this.info_node) {
this.info_node.active = true;
this.setLabel(this.info_node, `升至 Lv.${targetLv}`);
}
} else {
// 特殊卡(升级 / 刷新):显示卡名 + 品质后缀 + 描述
const specialCard = this.card_type === CardType.SpecialUpgrade
? SpecialUpgradeCardList[this.card_uuid]
: SpecialRefreshCardList[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}${specialCard?.name || ""}${spSuffix}`);
if (this.info_node) {
this.info_node.active = true;
this.setLabel(this.info_node, specialCard?.info || "");
}
// 特殊卡(刷新):显示卡名 + 品质后缀 + 描述
const specialCard = SpecialRefreshCardList[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}${specialCard?.name || ""}${spSuffix}`);
if (this.info_node) {
this.info_node.active = true;
this.setLabel(this.info_node, specialCard?.info || "");
}
this.ap_node.active = false;
this.hp_node.active = false;
@@ -801,10 +765,10 @@ export class CardComp extends CCComp {
// ---- 图标 ----
const iconNode = this.icon_node as Node;
if (this.card_type === CardType.Hero) {
if (renderType === CardType.Hero) {
iconNode.setScale(new Vec3(-1.5, 1.5, 1));
// 英雄卡使用 AnimationClip加载 idle 动画
this.updateHeroAnimation(iconNode, this.card_uuid, this.iconVisualToken);
// 英雄卡 / 升级卡使用 AnimationClip加载 idle 动画
this.updateHeroAnimation(iconNode, renderUuid, this.iconVisualToken);
return;
}
iconNode.setScale(new Vec3(1, 1, 1));
@@ -832,12 +796,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 +812,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 +831,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 +844,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 +868,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();
}

View File

@@ -47,7 +47,7 @@ import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { HeroInfo, HType } from "../common/config/heroSet";
import { HeroViewComp } from "../hero/HeroViewComp";
import { FacSet, FightSet, SKILL_CARD_WAVES } from "../common/config/GameSet";
import { FacSet, FightSet } from "../common/config/GameSet";
import { MissionEconomy } from "./MissionEconomy";
import { UIID } from "../common/config/GameUIConfig";
@@ -98,6 +98,10 @@ export class MissionCardComp extends CCComp {
/** 抽卡(刷新)按钮节点 */
@property(Node)
cards_chou: Node = null!
/** 英雄卡牌池cards_node显示隐藏 */
@property(Node)
showHeros: Node = null!
/** 卡池升级按钮节点(已废弃,保留节点引用防止 editor 报错) */
@property(Node)
cards_up: Node = null!
@@ -147,7 +151,12 @@ export class MissionCardComp extends CCComp {
/** 卡牌面板收起态缩放scale=0 隐藏) */
private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
/** 卡牌原始定位点 */
private cardsPos = [-260, -75, 108, 260]
private cardsPos = [-220, 0, 220]
/**
* 必出升级卡的轮询索引:每次刷新按场上可升级英雄 UUID 升序轮询选 1 张。
* 每次 buildDrawCards 取模更新,保证多次刷新时升级卡依次切换。
*/
private upgradeRotationIndex: number = 0;
// ======================== 生命周期 ========================
@@ -176,6 +185,9 @@ export class MissionCardComp extends CCComp {
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
}
if (this.showHeros && this.showHeros.isValid) {
this.showHeros.off(NodeEventType.TOUCH_END, this.onShowHerosClick, this);
}
this.unbindEvents();
}
@@ -218,11 +230,6 @@ export class MissionCardComp extends CCComp {
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
const wave = this.getCurrentWave();
if (SKILL_CARD_WAVES.includes(wave)) {
this.showSkillCardPopup();
}
mLogger.log(this.debugMode, "MissionCardComp", "mission start");
}
@@ -281,6 +288,9 @@ export class MissionCardComp extends CCComp {
this.cards_chou?.on(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou?.on(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
/** 英雄卡池显示/隐藏切换按钮 */
this.showHeros?.on(NodeEventType.TOUCH_END, this.onShowHerosClick, this);
/** 技能卡刷新按钮 */
this.skill_refresh?.on(NodeEventType.TOUCH_START, this.onSkillDrawTouchStart, this);
this.skill_refresh?.on(NodeEventType.TOUCH_END, this.onSkillDrawTouchEnd, this);
@@ -369,11 +379,6 @@ export class MissionCardComp extends CCComp {
this.layoutCardSlots();
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
const wave = this.getCurrentWave();
if (SKILL_CARD_WAVES.includes(wave)) {
this.showSkillCardPopup();
}
}
/**
@@ -462,6 +467,9 @@ export class MissionCardComp extends CCComp {
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
}
if (this.showHeros && this.showHeros.isValid) {
this.showHeros.off(NodeEventType.TOUCH_END, this.onShowHerosClick, this);
}
if (this.skill_refresh && this.skill_refresh.isValid) {
this.skill_refresh.off(NodeEventType.TOUCH_START, this.onSkillDrawTouchStart, this);
this.skill_refresh.off(NodeEventType.TOUCH_END, this.onSkillDrawTouchEnd, this);
@@ -490,15 +498,12 @@ export class MissionCardComp extends CCComp {
const after = this.getAliveHeroCount();
this.updateHeroNumUI(true, after > before);
// 第一次召唤英雄后关闭guide3打开guide4
// 第一次召唤英雄后关闭guide3
// 注:首个英雄召唤后战斗自动开始(见 MissionComp不再需要 Guide4 引导点击开始按钮
if (!smc.finish_guides.includes(3)) {
smc.finish_guides.push(3);
oops.gui.remove(UIID.Guide3);
}
if (!smc.finish_guides.includes(4)) {
oops.gui.open(UIID.Guide4);
}
}
/** 英雄死亡事件回调:刷新面板列表并更新英雄数量 UI */
@@ -582,6 +587,22 @@ export class MissionCardComp extends CCComp {
this.playButtonResetAnim(this.cards_chou);
}
/**
* 英雄卡池显隐切换按钮回调:
* - 切换 cards_node 的 active 状态。
* - 显示时激活 showHeros 下的 "active" 子节点,隐藏时关闭该子节点。
*/
private onShowHerosClick() {
if (!this.cards_node || !this.cards_node.isValid) return;
oops.audio.playEffect("music/button");
const visible = !this.cards_node.active;
this.cards_node.active = visible;
const activeChild = this.showHeros?.getChildByName("active");
if (activeChild) {
activeChild.active = visible;
}
}
// ======================== 技能抽卡按钮回调 ========================
private onSkillDrawTouchStart() {
this.playButtonPressAnim(this.skill_refresh);
@@ -712,17 +733,12 @@ export class MissionCardComp extends CCComp {
/**
* 构建本次抽卡结果,保证最终可分发 3 条数据。
*
* 抽卡池构成
* - 动态升级卡:扫描场上存活英雄,每个 UUID 至多生成一张升级卡
* (已达到 HERO_MAX_LV 的英雄不出卡)。同 UUID 只出一张,避免重复
* - 基础英雄卡:所有英雄的 lv1 卡
* - 刷新功能卡SpecialRefresh 卡。
* 抽卡规则
* 1. **必出规则**:当场上有可升级英雄,每次刷新必出 1 张升级卡,并放在返回数组首位(对应卡池最左侧槽位)。
* 多次刷新时,按场上英雄 target_hero_uuid 升序循环切换升级卡upgradeRotationIndex
* 2. 其余 2 张从混合池(剩余升级卡 + 基础英雄卡)按权重抽取
*
* 三类卡按 weight 权重混合抽 3 张,且升级卡之间 unique 去重
*
* 特殊规则:当场存活英雄数已达 HERO_MAX_NUM 时,
* 不再抽取基础英雄卡和刷新卡,只抽取场上已有英雄的升级卡
* (若全部已满级则降级回混合池,避免卡池为空)。
* 特殊规则:当场存活英雄数已达 HERO_MAX_NUM 时,全部出升级卡(若全部已满级则降级回混合池)
*/
private buildDrawCards(): CardConfig[] {
const upgradeCards = this.buildHeroUpgradeCards();
@@ -742,21 +758,42 @@ export class MissionCardComp extends CCComp {
}
const heroCards = CardPoolList.filter(c => c.type === CardType.Hero);
const refreshCards: CardConfig[] = Object.values(SpecialRefreshCardList);
const mixedPool: CardConfig[] = [...upgradeCards, ...heroCards, ...refreshCards];
// 英雄池只刷英雄卡(含动态升级卡),不再混入刷新功能卡
const mixedPool: CardConfig[] = [...upgradeCards, ...heroCards];
if (mixedPool.length === 0) return [];
const picked = this.pickMixedCards(mixedPool, 3, upgradeCards);
if (picked.length >= 3) return picked.slice(0, 3);
const result: CardConfig[] = [];
const usedUpgradeTargets = new Set<number>();
/** 兜底:不足 3 张时循环补齐 */
const filled = [...picked];
while (filled.length < 3) {
if (mixedPool.length === 0) break;
filled.push(mixedPool[filled.length % mixedPool.length]);
// 必出 1 张升级卡,按 UUID 升序轮询,放在返回数组首位(对应最左侧卡槽)
if (upgradeCards.length > 0) {
const idx = this.upgradeRotationIndex % upgradeCards.length;
this.upgradeRotationIndex = (this.upgradeRotationIndex + 1) % upgradeCards.length;
const mandatory = upgradeCards[idx];
result.push(mandatory);
usedUpgradeTargets.add(mandatory.target_hero_uuid ?? 0);
}
return filled;
// 剩余张数从混合池抽取,排除已选升级卡 target_hero_uuid
const remainingCount = 3 - result.length;
if (remainingCount > 0) {
const remainingPool = mixedPool.filter(c => {
if (c.type === CardType.SpecialUpgrade && c.target_hero_uuid) {
return !usedUpgradeTargets.has(c.target_hero_uuid);
}
return true;
});
const rest = this.pickMixedCards(remainingPool, remainingCount, upgradeCards);
result.push(...rest);
}
// 兜底:不足 3 张时循环补齐
while (result.length < 3) {
if (mixedPool.length === 0) break;
result.push(mixedPool[result.length % mixedPool.length]);
}
return result.slice(0, 3);
}
/**
@@ -834,6 +871,8 @@ export class MissionCardComp extends CCComp {
hero_lv: lv + 1,
});
});
// 按 target_hero_uuid 升序排序,保证返回顺序稳定(便于 buildDrawCards 按顺序轮询)
result.sort((a, b) => (a.target_hero_uuid ?? 0) - (b.target_hero_uuid ?? 0));
return result;
}

View File

@@ -186,6 +186,8 @@ export class MissionComp extends CCComp {
this.on(GameEvent.NewWave, this.onNewWave, this)
this.on(GameEvent.DO_AD_BACK, this.do_ad, this)
this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
// 第一波准备阶段,首个英雄召唤后自动开始战斗(无需玩家点击开始按钮)
oops.message.on(GameEvent.MasterCalled, this.onHeroSummoned, this);
this.removeMemoryPanel()
}
@@ -197,16 +199,17 @@ export class MissionComp extends CCComp {
this.mission_start();
}, 0);
smc.map.MapView.scene.mapLayer.stopAnimations();
smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = true;
// smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = true;
}
onDestroy() {
smc.map.MapView.scene.mapLayer.playAnimations()
smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = false;
// smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = false;
super.onDestroy();
if (this.start_btn && this.start_btn.isValid) {
this.start_btn.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this);
}
oops.message.off(GameEvent.MasterCalled, this.onHeroSummoned, this);
}
/**
@@ -699,6 +702,18 @@ export class MissionComp extends CCComp {
this.to_fight();
}
/**
* 英雄召唤事件回调:
* 第一波准备阶段Prepare只有第 1 波才会进入,
* 此时首个英雄一旦召唤即自动开战,无需玩家点击开始按钮。
* 后续波次走 PrepareStart 倒计时自动流转,不会进入 Prepare。
*/
private onHeroSummoned() {
if (this.currentPhase !== MissionPhase.Prepare) return;
if (!smc.mission.play || smc.mission.pause) return;
this.to_fight();
}
/**
* 打开结算弹窗: