3 Commits

Author SHA1 Message Date
panFD
79746409ef refactor(MissionCard): 替换旧版按钮指引为卡牌引导动画,新增选次提示
1. 移除旧的新手按钮指引逻辑,替换为开局三选一卡牌抖动引导动画
2. 新增pickCountLabel节点绑定与选次计数逻辑,显示当前是第几次选择英雄
3. 重构新手引导相关常量与方法,统一卡牌引导的命名与实现
4. 调整任务初始化与结束时的引导清理逻辑
2026-08-19 23:28:11 +08:00
panFD
6c82c632b6 feat: 重构英雄编队逻辑,优化出生与排队规则
1. 移除英雄出生落点偏移,改为按职业优先级从右向左排队
2. 重构英雄卡点击逻辑,支持点击整张卡牌选取
3. 新增英雄血量背景预制体与UI按钮样式
4. 优化AOE技能目标选取逻辑,使用中排基准点
2026-08-19 22:52:17 +08:00
panFD
0ec27b09a4 fix(skill tooltip): adjust tooltip vertical offset from 30 to 60
fix the issue that skill tooltip might be blocked by health bar
2026-08-19 22:51:52 +08:00
16 changed files with 6764 additions and 6751 deletions

View File

@@ -29,16 +29,19 @@
},
{
"__id__": 26
},
{
"__id__": 34
}
],
"_active": true,
"_components": [
{
"__id__": 76
"__id__": 84
}
],
"_prefab": {
"__id__": 78
"__id__": 86
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -78,7 +81,7 @@
"__id__": 1
},
"_children": [],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 3
@@ -108,8 +111,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"x": 0.5,
"y": 0.5,
"z": 1
},
"_mobility": 0,
@@ -253,7 +256,7 @@
"__id__": 1
},
"_children": [],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 11
@@ -283,8 +286,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"x": 0.5,
"y": 0.5,
"z": 1
},
"_mobility": 0,
@@ -428,7 +431,7 @@
"__id__": 1
},
"_children": [],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 19
@@ -458,8 +461,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"x": 0.5,
"y": 0.5,
"z": 1
},
"_mobility": 0,
@@ -594,6 +597,181 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Button01_l_Yellow",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 27
},
{
"__id__": 29
},
{
"__id__": 31
}
],
"_prefab": {
"__id__": 33
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.6,
"y": 0.6,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 28
},
"_contentSize": {
"__type__": "cc.Size",
"width": 250,
"height": 133.33333333333334
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "f56shr5D9N2aJyQ8kZt9jR"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 30
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@0e901",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 1,
"_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": "7828VPg/9KFqdBBkwVgGO8"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 32
},
"_alignFlags": 45,
"_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": 80,
"_originalHeight": 142,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "046yIkdV9ETpWKshcgcsYj"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "66oR8aJCtH4IcgUpTJF6an",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Node",
@@ -604,29 +782,29 @@
},
"_children": [
{
"__id__": 27
"__id__": 35
},
{
"__id__": 33
"__id__": 41
},
{
"__id__": 51
"__id__": 59
}
],
"_active": true,
"_components": [
{
"__id__": 69
"__id__": 77
},
{
"__id__": 71
"__id__": 79
},
{
"__id__": 73
"__id__": 81
}
],
"_prefab": {
"__id__": 75
"__id__": 83
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -665,20 +843,20 @@
"mountedRoot": null
},
"_parent": {
"__id__": 26
"__id__": 34
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 28
"__id__": 36
},
{
"__id__": 30
"__id__": 38
}
],
"_prefab": {
"__id__": 32
"__id__": 40
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -715,11 +893,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 27
"__id__": 35
},
"_enabled": true,
"__prefab": {
"__id__": 29
"__id__": 37
},
"_contentSize": {
"__type__": "cc.Size",
@@ -743,11 +921,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 27
"__id__": 35
},
"_enabled": true,
"__prefab": {
"__id__": 31
"__id__": 39
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -826,27 +1004,27 @@
"mountedRoot": null
},
"_parent": {
"__id__": 26
"__id__": 34
},
"_children": [
{
"__id__": 34
},
{
"__id__": 40
}
],
"_active": true,
"_components": [
{
"__id__": 46
"__id__": 42
},
{
"__id__": 48
}
],
"_active": true,
"_components": [
{
"__id__": 54
},
{
"__id__": 56
}
],
"_prefab": {
"__id__": 50
"__id__": 58
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -883,20 +1061,20 @@
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 33
"__id__": 41
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 35
"__id__": 43
},
{
"__id__": 37
"__id__": 45
}
],
"_prefab": {
"__id__": 39
"__id__": 47
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -933,11 +1111,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 34
"__id__": 42
},
"_enabled": true,
"__prefab": {
"__id__": 36
"__id__": 44
},
"_contentSize": {
"__type__": "cc.Size",
@@ -961,11 +1139,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 34
"__id__": 42
},
"_enabled": true,
"__prefab": {
"__id__": 38
"__id__": 46
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -1019,20 +1197,20 @@
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 33
"__id__": 41
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 41
"__id__": 49
},
{
"__id__": 43
"__id__": 51
}
],
"_prefab": {
"__id__": 45
"__id__": 53
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -1069,11 +1247,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 40
"__id__": 48
},
"_enabled": true,
"__prefab": {
"__id__": 42
"__id__": 50
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1097,11 +1275,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 40
"__id__": 48
},
"_enabled": true,
"__prefab": {
"__id__": 44
"__id__": 52
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -1178,11 +1356,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 33
"__id__": 41
},
"_enabled": true,
"__prefab": {
"__id__": 47
"__id__": 55
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1206,11 +1384,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 33
"__id__": 41
},
"_enabled": true,
"__prefab": {
"__id__": 49
"__id__": 57
},
"_resizeMode": 1,
"_layoutType": 1,
@@ -1259,27 +1437,27 @@
"mountedRoot": null
},
"_parent": {
"__id__": 26
"__id__": 34
},
"_children": [
{
"__id__": 52
},
{
"__id__": 58
}
],
"_active": false,
"_components": [
{
"__id__": 64
"__id__": 60
},
{
"__id__": 66
}
],
"_active": false,
"_components": [
{
"__id__": 72
},
{
"__id__": 74
}
],
"_prefab": {
"__id__": 68
"__id__": 76
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -1316,20 +1494,20 @@
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 51
"__id__": 59
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 53
"__id__": 61
},
{
"__id__": 55
"__id__": 63
}
],
"_prefab": {
"__id__": 57
"__id__": 65
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -1366,11 +1544,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 52
"__id__": 60
},
"_enabled": true,
"__prefab": {
"__id__": 54
"__id__": 62
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1394,11 +1572,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 52
"__id__": 60
},
"_enabled": true,
"__prefab": {
"__id__": 56
"__id__": 64
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -1455,20 +1633,20 @@
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 51
"__id__": 59
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 59
"__id__": 67
},
{
"__id__": 61
"__id__": 69
}
],
"_prefab": {
"__id__": 63
"__id__": 71
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -1505,11 +1683,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 58
"__id__": 66
},
"_enabled": true,
"__prefab": {
"__id__": 60
"__id__": 68
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1533,11 +1711,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 58
"__id__": 66
},
"_enabled": true,
"__prefab": {
"__id__": 62
"__id__": 70
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -1614,11 +1792,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
"__id__": 59
},
"_enabled": true,
"__prefab": {
"__id__": 65
"__id__": 73
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1642,11 +1820,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
"__id__": 59
},
"_enabled": true,
"__prefab": {
"__id__": 67
"__id__": 75
},
"_resizeMode": 1,
"_layoutType": 1,
@@ -1693,11 +1871,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
"__id__": 34
},
"_enabled": true,
"__prefab": {
"__id__": 70
"__id__": 78
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1721,11 +1899,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
"__id__": 34
},
"_enabled": true,
"__prefab": {
"__id__": 72
"__id__": 80
},
"_alignFlags": 45,
"_target": null,
@@ -1757,11 +1935,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
"__id__": 34
},
"_enabled": true,
"__prefab": {
"__id__": 74
"__id__": 82
},
"_resizeMode": 1,
"_layoutType": 2,
@@ -1812,7 +1990,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 77
"__id__": 85
},
"_contentSize": {
"__type__": "cc.Size",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,851 @@
[
{
"__type__": "cc.Prefab",
"_name": "hpbg",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "hpbg",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 10
},
{
"__id__": 18
},
{
"__id__": 26
}
],
"_active": true,
"_components": [
{
"__id__": 34
},
{
"__id__": 36
}
],
"_prefab": {
"__id__": 38
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"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": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "Popup02~09_Topber_White_Bg",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
},
{
"__id__": 7
}
],
"_prefab": {
"__id__": 9
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"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": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 720,
"height": 1280
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "bdp/P0YJBDNIRtzvt1ZYlH"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 229,
"g": 237,
"b": 242,
"a": 255
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@b94ea",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 1,
"_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": "f6mrtQTPhBTr/oRv3n2y7H"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 8
},
"_alignFlags": 45,
"_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": 54,
"_originalHeight": 61,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "45Io/Pye5PoZwcjgb5AzVU"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "8czX9Q7aNPqKQ/I18JMThq",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Popup02~09_Topber_White_BgTop",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 11
},
{
"__id__": 13
},
{
"__id__": 15
}
],
"_prefab": {
"__id__": 17
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 561,
"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": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 10
},
"_enabled": true,
"__prefab": {
"__id__": 12
},
"_contentSize": {
"__type__": "cc.Size",
"width": 712,
"height": 150
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "be2QVw6n9LFYl0l8ppduaW"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 10
},
"_enabled": true,
"__prefab": {
"__id__": 14
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 153,
"g": 22,
"b": 37,
"a": 255
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@4a5a3",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 1,
"_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": "22EIS7o1lA4Kwmkz0o8wWF"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 10
},
"_enabled": true,
"__prefab": {
"__id__": 16
},
"_alignFlags": 41,
"_target": null,
"_left": 4,
"_right": 4,
"_top": 4,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 48,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "9elo28q4xGx5SRtvnP4h1I"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "325R/lW/ZKpKtAoLMy5t8a",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Popup02~09_Topber_White_BgTopLight",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 19
},
{
"__id__": 21
},
{
"__id__": 23
}
],
"_prefab": {
"__id__": 25
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 624,
"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": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 18
},
"_enabled": true,
"__prefab": {
"__id__": 20
},
"_contentSize": {
"__type__": "cc.Size",
"width": 712,
"height": 24
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "2fo59AaaRMSKRNKjh+ye/L"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 18
},
"_enabled": true,
"__prefab": {
"__id__": 22
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 45
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@3d086",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 1,
"_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": "abeLWbs05Hjp2RTnFx8XHJ"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 18
},
"_enabled": true,
"__prefab": {
"__id__": 24
},
"_alignFlags": 41,
"_target": null,
"_left": 4,
"_right": 4,
"_top": 4,
"_bottom": 173,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 50,
"_originalHeight": 24,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "7363RL0R9ErIoyiH6P9e4n"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "730isJJuJKuZ4TyYRIGiPl",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Popup03_Gradient",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 27
},
{
"__id__": 29
},
{
"__id__": 31
}
],
"_prefab": {
"__id__": 33
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 561,
"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": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 28
},
"_contentSize": {
"__type__": "cc.Size",
"width": 700,
"height": 150
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "e3U+s1uAdCgJorqCcyhk3a"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 30
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 245,
"g": 51,
"b": 76,
"a": 255
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@7f910",
"__expectedType__": "cc.SpriteFrame"
},
"_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": "26DiJMkAFBWb1mvvZDUtMs"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 32
},
"_alignFlags": 41,
"_target": null,
"_left": 10,
"_right": 10,
"_top": 4,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 473,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "24rC2H/9RBE6PRIrKWk/eF"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "3awTuW1YtIkJ0MAhOjyZM8",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 35
},
"_contentSize": {
"__type__": "cc.Size",
"width": 720,
"height": 1280
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "3e6xe2CYhAnaDRXFjAvMW1"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 37
},
"_alignFlags": 45,
"_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": 100,
"_originalHeight": 100,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "d1teNkwJlBsLfuBqxWNv09"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "b4ICzk8RFDq6KzVMReui0b",
"instance": null,
"targetOverrides": null
}
]

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "49480ebf-d16b-4242-8983-f4ba12861caa",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "hpbg"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 997 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,8 @@
* @description 统一移动组件与系统(英雄 + 怪物)
*
* 核心规则:
* 1. 准备阶段:双方原地待命,不推进不索敌。
* 1. 准备阶段:不推进不索敌;英雄按职业优先级(坦克→战士→刺客→射手→法师→辅助)
* 从最右迎敌位向左排队移动,防止同类型落点重叠;怪物原地待命。
* 2. 战斗阶段:双方向最近敌人推进,进入攻击范围(model.dis)后停止移动并攻击。
* 3. 场上无敌人:停止移动(战斗结束,由 MissionComp 进入结算)。
* 4. 击退/位移不会把单位推出屏幕:HERO 不越过 BoxSet.LETF_END,MON 不越过 BoxSet.RIGHT_END。
@@ -14,6 +15,7 @@ import { HeroViewComp } from "./HeroViewComp";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { BoxSet, FacSet } from "../common/config/GameSet";
import { HRole } from "../common/config/heroSet";
import { Node } from "cc";
@ecs.register('MoveComp')
@@ -50,6 +52,18 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number; laneScore: number }[] = [];
private renderEntryCount = 0;
// ======================== 英雄编队排队 ========================
/** 编队锚点:最右迎敌位(与 MissionHeroComp.HERO_ROW_FRONT 的 x 对齐,避免循环依赖在此独立定义) */
private static readonly QUEUE_ANCHOR_X = -200;
/** 编队槽位默认间隔(像素);人数超出边界宽度时自动压缩间距 */
private static readonly QUEUE_SPACING = 30;
/** 编队槽位重算节流间隔(秒):召唤/死亡后最多延迟一次重算 */
private readonly queueRebuildInterval = 0.1;
private lastQueueRebuildAt = 0;
/** 编队槽位缓存:按职业优先级排序后各英雄的目标 X */
private readonly queueSlots: { eid: number; targetX: number }[] = [];
private getHeroViewMatcher(): ecs.IMatcher {
if (!this.heroViewMatcher) {
this.heroViewMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp);
@@ -104,11 +118,15 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
}
}
/** 准备阶段:双方原地待命,不推进不索敌 */
/** 准备阶段:不推进不索敌;英雄向编队槽位排队移动,怪物原地待命 */
if (!smc.mission.in_fight) {
this.clearCombatTarget(model);
model.is_atking = false;
if (view.status !== "atk") view.status_change("idle");
if (model.fac === FacSet.HERO) {
this.processHeroQueue(e, model, move, view);
} else if (view.status !== "atk") {
view.status_change("idle");
}
return;
}
@@ -133,6 +151,90 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
model.enemy_in_cast_range = false;
}
// ======================== 英雄编队排队(准备阶段) ========================
/**
* 英雄编队排队:
* 按 坦克→战士→刺客→射手→法师→辅助 的职业优先级(即 HRole 枚举值升序),
* 从最右迎敌位(QUEUE_ANCHOR_X)向左依次排开,防止同类型英雄落点重叠。
*
* @param entity 当前英雄实体
* @param model 英雄属性(取 speed / role)
* @param move 移动组件(维护 direction)
* @param view 视图组件(驱动位移与动画状态)
*/
private processHeroQueue(entity: ecs.Entity, model: HeroAttrsComp, move: MoveComp, view: HeroViewComp): void {
const targetX = this.resolveHeroQueueTargetX(entity);
const selfX = view.node.position.x;
/** 无槽位(未登记)或已到位:贴齐槽位并原地待命,恢复朝右迎敌 */
if (targetX === null || Math.abs(targetX - selfX) <= 2) {
if (targetX !== null && Math.abs(selfX - targetX) > 0.01) {
view.node.setPosition(targetX, view.node.position.y, 0);
}
view.scale = 1;
if (view.status !== "atk") view.status_change("idle");
return;
}
/** 未到位:向槽位推进(与战斗推进同速),朝移动方向转身 */
const dir = targetX > selfX ? 1 : -1;
move.direction = dir;
view.scale = dir;
const delta = (model.speed / 3) * this.dt * dir;
const newX = this.clampByFacBoundary(selfX + delta, model.fac);
if (Math.abs(newX - selfX) >= 0.01) {
view.node.setPosition(newX, view.node.position.y, 0);
view.status_change("move");
}
}
/** 查询当前英雄的编队槽位目标 X(无槽位返回 null) */
private resolveHeroQueueTargetX(entity: ecs.Entity): number | null {
const now = Date.now() / 1000;
if (now - this.lastQueueRebuildAt >= this.queueRebuildInterval) {
this.lastQueueRebuildAt = now;
this.rebuildHeroQueueSlots();
}
for (const slot of this.queueSlots) {
if (slot.eid === entity.eid) return slot.targetX;
}
return null;
}
/**
* 重算编队槽位:
* 收集全部存活英雄,按 职业优先级 → 出生序 → eid 稳定排序后,
* 从 QUEUE_ANCHOR_X 向左按间隔分配;人数过多时压缩间距,保证不越过 LETF_END。
*/
private rebuildHeroQueueSlots(): void {
const heroes: { eid: number; role: number; spawnOrder: number }[] = [];
ecs.query(this.getHeroViewMatcher()).forEach(e => {
const m = e.get(HeroAttrsComp);
if (!m || m.fac !== FacSet.HERO || m.is_dead) return;
const v = e.get(HeroViewComp);
if (!v?.node) return;
const move = e.get(MoveComp);
heroes.push({ eid: e.eid, role: m.role ?? HRole.Support, spawnOrder: move?.spawnOrder ?? 0 });
});
heroes.sort((a, b) =>
(a.role - b.role) ||
(a.spawnOrder - b.spawnOrder) ||
(a.eid - b.eid)
);
const count = heroes.length;
const maxWidth = MoveSystem.QUEUE_ANCHOR_X - BoxSet.LETF_END;
const spacing = count > 1
? Math.min(MoveSystem.QUEUE_SPACING, maxWidth / (count - 1))
: 0;
this.queueSlots.length = 0;
for (let i = 0; i < count; i++) {
this.queueSlots.push({ eid: heroes[i].eid, targetX: MoveSystem.QUEUE_ANCHOR_X - i * spacing });
}
}
private syncCombatTarget(model: HeroAttrsComp, selfView: HeroViewComp, enemyView: HeroViewComp): void {
if (!enemyView || !enemyView.node || !enemyView.ent) {
this.clearCombatTarget(model);

View File

@@ -769,11 +769,9 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
}
return null;
} else {
// 怪物打英雄,英雄网格:
// 列1: 0,1,2 (X=-210)
// 列2: 3,4,5 (X=-300)
// 英雄只有2列,第2列中路是 index 4
return MissionHeroComp.HERO_POSITIONS[4].clone();
// 怪物打英雄:以英雄编队中排基准点为 AOE 目标
// (英雄已改为按职业优先级从最右向左排队,中排 -260 即队伍中部)
return MissionHeroComp.HERO_ROW_MID.clone();
}
}

View File

@@ -6,7 +6,7 @@
* 1. 渲染单个英雄卡的显示信息(名称、等级、描述、图标、费用),
* 展示方式与 SCardComp 一致(图集静态图标 + ★等级 + 描述词条)。
* 同时支持动态升级卡(SpecialUpgrade + target_hero_eid)按英雄卡样式渲染。
* 2. 处理购买点击 → UseHeroCard 上限校验(guard) → 扣费 → 派发 CallHero 召唤英雄。
* 2. 处理卡牌点击(点击卡牌任意区域即选取) → UseHeroCard 上限校验(guard) → 派发 CallHero 召唤英雄。
* 特殊卡(升级/刷新)则派发 UseSpecialCard。
* 3. 使用成功后派发 CardUsed 事件,通知 MissionCardComp 刷新英雄卡池。
*
@@ -33,7 +33,7 @@ const { ccclass, property } = _decorator;
* CHeroComp —— 英雄卡池单个英雄卡项
*
* 由 MissionCardComp.cacheCardComps() 实例化。
* 以图标 + 名称 + 描述词条 + 购买按钮的形式呈现。
* 以图标 + 名称 + 描述词条 + 费用角标的形式呈现,点击卡牌任意区域即选取。
*/
@ccclass('CHeroComp')
@ecs.register('CHeroComp', false)
@@ -72,7 +72,7 @@ export class CHeroComp extends CCComp {
@property(Label)
private level_label: Label = null;
/** 购买按钮节点 */
/** 费用显示节点(原购买按钮,现仅展示费用,不再承担点击) */
@property({ type: Node })
private buy_node: Node = null;
@@ -118,9 +118,10 @@ export class CHeroComp extends CCComp {
fied3: !!this.fied3,
buy_node: !!this.buy_node
});
this.buy_node?.on(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
this.buy_node?.on(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
// 点击整张卡牌选取英雄:监听挂载在卡牌根节点,命中区域由根节点 UITransformer 决定
this.node.on(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.on(NodeEventType.TOUCH_END, this.onCardClick, this);
this.node.on(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
// 缓存透明度组件(用于淡入淡出动画)
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
@@ -134,10 +135,10 @@ export class CHeroComp extends CCComp {
onDestroy() {
super.onDestroy();
if (this.buy_node && this.buy_node.isValid) {
this.buy_node.off(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
if (this.node && this.node.isValid) {
this.node.off(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.off(NodeEventType.TOUCH_END, this.onCardClick, this);
this.node.off(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
}
}
@@ -303,7 +304,7 @@ export class CHeroComp extends CCComp {
this.updateHeroAnimation(this.icon_node, renderUuid, this.iconVisualToken);
}
// 购买按钮费用显示
// 费用显示(原购买按钮节点,现仅作费用角标)
if (this.buy_node) {
this.buy_node.active = true;
const costLabel = this.cost_label
@@ -372,7 +373,7 @@ export class CHeroComp extends CCComp {
/**
* 播放购买动画:卡牌放大(scale 1→1.1)+ 淡出,结束后隐藏节点。
* 动画结束由 onBuyClick 的后续逻辑(CardUsed 刷新卡池)接管。
* 动画结束由 onCardClick 的后续逻辑(CardUsed 刷新卡池)接管。
*/
private playPurchaseAnim() {
Tween.stopAllByTarget(this.node);
@@ -413,44 +414,44 @@ export class CHeroComp extends CCComp {
}
}
// ======================== 按钮动画 ========================
// ======================== 点击反馈动画 ========================
/** 按钮正常缩放 */
private readonly btnNormalScale: number = 1;
/** 按钮按下缩放 */
private readonly btnPressScale: number = 0.92;
/** 按钮点击回弹峰值缩放 */
private readonly btnClickScale: number = 1.08;
/** 卡牌正常缩放 */
private readonly cardNormalScale: number = 1;
/** 卡牌按下缩放 */
private readonly cardPressScale: number = 0.92;
/** 卡牌点击回弹峰值缩放 */
private readonly cardClickScale: number = 1.08;
/** 购买按钮按下:缩小反馈 */
private onBuyTouchStart() {
this.playBtnScaleTo(this.btnPressScale, 0.06);
/** 卡牌按下:缩小反馈 */
private onCardTouchStart() {
this.playCardScaleTo(this.cardPressScale, 0.06);
}
/** 购买按钮取消:恢复缩放 */
private onBuyTouchCancel() {
this.playBtnScaleTo(this.btnNormalScale, 0.08);
/** 卡牌取消(滑出卡牌区域):恢复缩放 */
private onCardTouchCancel() {
this.playCardScaleTo(this.cardNormalScale, 0.08);
}
/** 按钮缩放动画(通用) */
private playBtnScaleTo(scale: number, duration: number) {
if (!this.buy_node || !this.buy_node.isValid) return;
Tween.stopAllByTarget(this.buy_node);
tween(this.buy_node)
/** 卡牌缩放动画(通用) */
private playCardScaleTo(scale: number, duration: number) {
if (!this.node || !this.node.isValid) return;
Tween.stopAllByTarget(this.node);
tween(this.node)
.to(duration, { scale: new Vec3(scale, scale, 1) })
.start();
}
/** 按钮点击回弹动画:先弹到峰值再回到正常 */
private playBtnClickAnim(onComplete?: () => void) {
if (!this.buy_node || !this.buy_node.isValid) {
/** 卡牌点击回弹动画:先弹到峰值再回到正常 */
private playCardClickAnim(onComplete?: () => void) {
if (!this.node || !this.node.isValid) {
onComplete?.();
return;
}
Tween.stopAllByTarget(this.buy_node);
tween(this.buy_node)
.to(0.05, { scale: new Vec3(this.btnClickScale, this.btnClickScale, 1) })
.to(0.08, { scale: new Vec3(this.btnNormalScale, this.btnNormalScale, 1) })
Tween.stopAllByTarget(this.node);
tween(this.node)
.to(0.05, { scale: new Vec3(this.cardClickScale, this.cardClickScale, 1) })
.to(0.08, { scale: new Vec3(this.cardNormalScale, this.cardNormalScale, 1) })
.call(() => onComplete?.())
.start();
}
@@ -561,20 +562,20 @@ export class CHeroComp extends CCComp {
// ======================== 购买逻辑 ========================
/**
* 购买点击处理:
* 1. 播放按钮点击回弹动画。
* 卡牌点击处理(点击卡牌任意区域即选取):
* 1. 播放卡牌点击回弹动画。
* 2. 英雄卡先通过 UseHeroCard 事件做数量上限校验(guard 模式,可被取消)。
* 3. 按卡牌类型分发效果:英雄卡 → CallHero 召唤英雄;特殊卡 → UseSpecialCard。
* 4. 派发 CardUsed 通知 MissionCardComp 刷新英雄卡池。
* 5. 标记已购买,隐藏购买按钮。
* 5. 标记已购买,隐藏费用角标。
*
* 注:购买不花费金币(免费获取),原扣费逻辑已移除。
*/
private onBuyClick() {
private onCardClick() {
if (!this.cardData || this.isPurchased) return;
// 播放按钮点击回弹动画(视觉反馈,不阻塞后续逻辑)
this.playBtnClickAnim();
// 播放卡牌点击回弹动画(视觉反馈,不阻塞后续逻辑)
this.playCardClickAnim();
oops.audio.playEffect("music/button");

View File

@@ -176,6 +176,10 @@ export class MissionCardComp extends CCComp {
@property(Node)
refresh_stone_num_node: Node = null!
/** 选择英雄次数提示 Label(显示"第X次") */
@property(Label)
pickCountLabel: Label = null!
// ======================== 运行时状态 ========================
@@ -198,6 +202,8 @@ export class MissionCardComp extends CCComp {
private static readonly OPENING_FREE_PICK_COUNT = 3;
/** 开局免费三选一剩余次数(>0 时招募不扣费,选完 1 张自动弹出下一组) */
private openingFreePicks: number = 0;
/** 当前是第几次选择英雄(从 1 开始,每次成功选用英雄卡后 +1) */
private currentPickIndex: number = 1;
// ======================== 面板划出状态 ========================
@@ -230,25 +236,21 @@ export class MissionCardComp extends CCComp {
/** 覆盖面板显示/隐藏动画时长 */
private readonly cardPanelAnimDuration: number = 0.25;
// ======================== 新手按钮指引 ========================
// ======================== 新手三选一卡牌引导 ========================
/** 新手按钮指引脉动放大倍率 */
private static readonly BUTTON_GUIDE_PULSE_SCALE: number = 1.15;
/** 新手按钮指引每步时长(秒) */
private static readonly BUTTON_GUIDE_STEP_DURATION: number = 4.2;
/** 新手按钮指引气泡停留时长(秒) */
private static readonly BUTTON_GUIDE_TIP_STAY: number = 3.4;
/** 指引步骤配置:完成标记键(smc.data.tip_done)+ 按钮节点取值器 + 气泡文案 */
private readonly guideSteps: Array<{ key: "hero" | "equip" | "skill" | "shop", getBtn: () => Node | null, text: string }> = [
{ key: "hero", getBtn: () => this.showHeros, text: "这里召唤英雄" },
{ key: "equip", getBtn: () => this.showEquips, text: "这里购买装备" },
{ key: "skill", getBtn: () => this.showSkills, text: "这里购买技能卷轴" },
{ key: "shop", getBtn: () => this.showShop, text: "这里购买药品" },
];
/** 当前指引下标(-1 = 未在指引中) */
private guideStepIndex: number = -1;
/** 气泡原文缓存(指引结束后还原) */
private guideTipTextCache: Map<Node, string> = new Map();
/** 引导动画抖动次数 */
private static readonly CARDS_GUIDE_SHAKE_TIMES: number = 3;
/** 引导动画抖动横向偏移(像素) */
private static readonly CARDS_GUIDE_SHAKE_OFFSET: number = 10;
/** 引导动画放大倍率(相对卡牌基准缩放) */
private static readonly CARDS_GUIDE_SCALE_PEAK: number = 1.15;
/** 引导动画单卡总时长(抖 3 下 + 放大回弹 ≈ 0.9s),作为轮流播放间隔 */
private static readonly CARDS_GUIDE_PER_CARD_DURATION: number = 0.9;
/** 引导动画启动延迟:需大于 CHeroComp 发牌入场时长(0.25s,scale 0→1),
* 否则基准缩放会在发牌未完成时被捕获为 0,导致放大目标也为 0(卡牌缩没) */
private static readonly CARDS_GUIDE_START_DELAY: number = 0.4;
/** 引导动画基准缓存(触发时刻捕获的位置与缩放,停止时还原) */
private cardsGuideBaseStates: { node: Node; pos: Vec3; scale: Vec3 }[] = [];
// ======================== 生命周期 ========================
@@ -276,8 +278,8 @@ export class MissionCardComp extends CCComp {
/** 组件销毁时解绑所有事件并清理英雄信息面板 */
onDestroy() {
super.onDestroy();
// 清理新手按钮指引的调度与动画,防止回调泄漏
this.stopButtonGuide();
// 清理新手卡牌引导动画,防止回调泄漏
this.stopCardsGuideAnim();
this.unscheduleAllCallbacks();
if (this.cards_refresh && this.cards_refresh.isValid) {
this.cards_refresh.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
@@ -363,10 +365,17 @@ export class MissionCardComp extends CCComp {
// 暂不启用 herobox 同步显示英雄信息(后续可能恢复),保留代码备查
// this.refreshHeroBoxSlots();
// 重置开局免费三选一次数,随后立即弹出第 1 组三选一
// 重置开局免费三选一次数与选择计数,随后立即弹出第 1 组三选一
this.openingFreePicks = MissionCardComp.OPENING_FREE_PICK_COUNT;
this.currentPickIndex = 1;
this.updatePickCountLabel();
this.showCardsPanel();
// 新人第一次进入关卡:3 张卡牌轮流播放抖动 + 放大引导动画(仅提醒 1 次)
if (!smc.data.tip_done.hero) {
this.playOpeningCardsGuideAnim();
}
mLogger.log(this.debugMode, "MissionCardComp", "mission start");
}
@@ -381,8 +390,8 @@ export class MissionCardComp extends CCComp {
comp.applyEmpty();
}
}
// 任务结束清理:停止新手按钮指引的调度与动画
this.stopButtonGuide();
// 任务结束清理:停止新手卡牌引导动画
this.stopCardsGuideAnim();
this.unscheduleAllCallbacks();
if (this.node && this.node.isValid) {
this.node.active = false;
@@ -498,13 +507,6 @@ export class MissionCardComp extends CCComp {
/** 战斗开始:保留卡牌面板,允许玩家在战斗阶段继续抽卡和召唤英雄 */
private onFightStart() {
this.enterBattlePhase();
// 首次进入战斗阶段:启动按钮逐个指引(仅提醒 1 次,全部完成则自动跳过)
const tips = smc.data.tip_done;
if (!tips.hero || !tips.equip || !tips.skill || !tips.shop) {
this.playButtonGuide();
}
}
private onShowSmallTip(event: string, args: any) {
@@ -893,6 +895,9 @@ export class MissionCardComp extends CCComp {
// 抽 1 张后关闭英雄卡池面板
this.hideCardsPanel();
}
// 本张已选完:选择计数 +1,供下一组三选一显示"第X次"
this.currentPickIndex++;
this.updatePickCountLabel();
// 英雄数量变化后同步 showHeros 可见性(满员/次数耗尽即隐藏)
this.updateShowHerosVisibility();
mLogger.log(this.debugMode, "MissionCardComp", "refresh hero pool after buy", {
@@ -951,150 +956,69 @@ export class MissionCardComp extends CCComp {
this.playButtonResetAnim(this.cards_refresh);
}
// ======================== 新手按钮指引 ========================
// ======================== 新手三选一卡牌引导(动画) ========================
/**
* 启动新手按钮指引:依次指引 英雄→装备→技能→药品 四个面板按钮。
* 每步播放 bg/icon 循环缩放脉动 + smalltip 气泡文案,
* 已完成的步骤(smc.data.tip_done 对应键为 true)自动跳过,仅提醒 1 次。
* 新人第一次进入关卡的开局三选一引导:
* 3 张卡牌从左到右轮流播放"抖动 3 下 + 放大 1 下"动画,仅提醒 1 次。
* 替代旧版按钮脉动 + 气泡指引(showHeros 按钮机制已移除,无气泡提示)。
*/
private playButtonGuide() {
// 确保无旧指引残留,再从头开始
this.stopButtonGuide();
this.guideStepIndex = -1;
this.nextButtonGuideStep();
}
/** 推进到下一个未完成指引的步骤;全部完成则结束 */
private nextButtonGuideStep() {
this.guideStepIndex++;
// 跳过已完成的步骤
while (this.guideStepIndex < this.guideSteps.length
&& smc.data.tip_done[this.guideSteps[this.guideStepIndex].key]) {
this.guideStepIndex++;
}
if (this.guideStepIndex >= this.guideSteps.length) {
this.finishButtonGuide();
return;
}
this.playButtonGuideStep();
}
/** 播放当前步指引,并用 scheduleOnce 驱动进入下一步 */
private playButtonGuideStep() {
const step = this.guideSteps[this.guideStepIndex];
if (!step) return;
const btn = step.getBtn();
if (btn && btn.isValid) {
this.playGuidePulse(btn, true);
this.showGuideTip(btn, step.text);
}
private playOpeningCardsGuideAnim() {
// 播放即标记完成,仅新人第一次进入关卡时提醒
smc.data.tip_done.hero = true;
this.cardsGuideBaseStates = [];
// 延迟启动:等发牌入场动画(scale 0→1,0.25s)播完,基准缩放才是稳定值
this.scheduleOnce(() => {
this.stopGuidePulse(btn);
// 本步提醒完成:记录完成标记(即使中止也只标记已播完的步骤)
smc.data.tip_done[step.key] = true;
this.nextButtonGuideStep();
}, MissionCardComp.BUTTON_GUIDE_STEP_DURATION);
}
/** 指引全部完成:还原气泡文案 */
private finishButtonGuide() {
this.restoreGuideTipTexts();
this.guideStepIndex = -1;
mLogger.log(this.debugMode, "MissionCardComp", "button guide finished");
}
/**
* 中止指引(任务结束 / 组件销毁时调用):
* 停止所有缩放脉动动画、还原气泡文案并隐藏气泡。
*/
private stopButtonGuide() {
if (this.guideStepIndex < 0 && this.guideTipTextCache.size === 0) return;
for (const step of this.guideSteps) {
this.stopGuidePulse(step.getBtn());
}
this.restoreGuideTipTexts();
this.guideStepIndex = -1;
}
/** 还原指引覆盖过的气泡文案并隐藏气泡 */
private restoreGuideTipTexts() {
for (const [tipNode, text] of this.guideTipTextCache) {
if (tipNode && tipNode.isValid) {
Tween.stopAllByTarget(tipNode);
const label = tipNode.getComponentInChildren(Label);
if (label) label.string = text;
tipNode.active = false;
for (let i = 0; i < this.cardComps.length; i++) {
const node = this.cardComps[i]?.node;
if (!node || !node.isValid) continue;
this.scheduleOnce(() => this.playOneCardGuideAnim(node), i * MissionCardComp.CARDS_GUIDE_PER_CARD_DURATION);
}
}
this.guideTipTextCache.clear();
}, MissionCardComp.CARDS_GUIDE_START_DELAY);
}
/**
* 对按钮 bg/icon 子节点播放循环缩放脉动动画。
* 使用 scale 往复 tween,不修改 position,避免与 Widget 布局冲突。
* @param btn 面板按钮节点(内部含 bg/icon 子节点)
* @param repeat 是否无限循环(true = 循环脉动)
*/
private playGuidePulse(btn: Node, repeat: boolean) {
const pulseScale = MissionCardComp.BUTTON_GUIDE_PULSE_SCALE;
for (const name of ["bg", "icon"]) {
const child = btn.getChildByName(name);
if (!child || !child.isValid) continue;
Tween.stopAllByTarget(child);
child.setScale(1, 1, 1);
// 单轮脉动约 0.6s,持续闪动与气泡停留节奏一致
const pulseTween = tween(child)
.to(0.3, { scale: new Vec3(pulseScale, pulseScale, 1) }, { easing: 'sineInOut' })
.to(0.3, { scale: new Vec3(1, 1, 1) }, { easing: 'sineInOut' });
if (repeat) {
tween(child).repeatForever(pulseTween).start();
} else {
pulseTween.start();
}
/** 单张卡牌引导动画:左右抖动 3 下后放大 1 下回弹 */
private playOneCardGuideAnim(node: Node) {
if (!node || !node.isValid) return;
// 触发时刻才捕获基准(发牌动画已结束),避免提前缓存到动画中的中间态
let base = this.cardsGuideBaseStates.find(s => s.node === node);
if (!base) {
base = { node, pos: node.position.clone(), scale: node.scale.clone() };
this.cardsGuideBaseStates.push(base);
}
}
const baseX = base.pos.x;
const baseY = base.pos.y;
const baseScale = base.scale;
const offset = MissionCardComp.CARDS_GUIDE_SHAKE_OFFSET;
const peakX = baseScale.x * MissionCardComp.CARDS_GUIDE_SCALE_PEAK;
const peakY = baseScale.y * MissionCardComp.CARDS_GUIDE_SCALE_PEAK;
/** 停止按钮 bg/icon 缩放脉动并复位缩放 */
private stopGuidePulse(btn: Node | null) {
if (!btn || !btn.isValid) return;
for (const name of ["bg", "icon"]) {
const child = btn.getChildByName(name);
if (!child || !child.isValid) continue;
Tween.stopAllByTarget(child);
child.setScale(1, 1, 1);
}
}
/**
* 在按钮节点上弹出 smalltip 气泡并覆盖文案。
* 首次覆盖前缓存原文,供指引结束后还原。
* @param btn 面板按钮节点(下挂 smalltip 子节点)
* @param text 指引文案
*/
private showGuideTip(btn: Node, text: string) {
const tipNode = btn.getChildByName("smalltip");
if (!tipNode) return;
if (!this.guideTipTextCache.has(tipNode)) {
const label = tipNode.getComponentInChildren(Label);
this.guideTipTextCache.set(tipNode, label ? label.string : "");
}
const label = tipNode.getComponentInChildren(Label);
if (label) label.string = text;
tipNode.active = true;
Tween.stopAllByTarget(tipNode);
tipNode.setScale(0, 0, 1);
tween(tipNode)
.to(0.15, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadOut' })
.to(0.05, { scale: new Vec3(1, 1, 1) })
.delay(MissionCardComp.BUTTON_GUIDE_TIP_STAY)
.to(0.15, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
.call(() => {
if (tipNode && tipNode.isValid) tipNode.active = false;
})
Tween.stopAllByTarget(node);
// 抖动 1 下 = 左偏 → 右偏 → 回中(结束后必回到基准位)
const shakeOnce = tween(node)
.to(0.05, { position: new Vec3(baseX - offset, baseY, 0) })
.to(0.1, { position: new Vec3(baseX + offset, baseY, 0) })
.to(0.05, { position: new Vec3(baseX, baseY, 0) });
tween(node)
.repeat(MissionCardComp.CARDS_GUIDE_SHAKE_TIMES, shakeOnce)
// 放大 1 下回弹(以触发时刻的基准缩放为锚,回到原值而非 0)
.to(0.15, { scale: new Vec3(peakX, peakY, baseScale.z) }, { easing: 'quadOut' })
.to(0.15, { scale: new Vec3(baseScale.x, baseScale.y, baseScale.z) }, { easing: 'quadIn' })
.start();
}
/** 停止卡牌引导动画并还原位置与缩放(任务结束 / 组件销毁时调用) */
private stopCardsGuideAnim() {
for (const state of this.cardsGuideBaseStates) {
if (state.node && state.node.isValid) {
Tween.stopAllByTarget(state.node);
state.node.setPosition(state.pos);
state.node.setScale(state.scale);
}
}
this.cardsGuideBaseStates = [];
}
// ======================== 面板底部划出系统 ========================
/** 面板节点数组(顺序:英雄→装备→技能→药品) */
@@ -2124,6 +2048,13 @@ export class MissionCardComp extends CCComp {
this.updateDrawCostUI();
}
/** 刷新选择英雄次数提示 Label("第X次"),未绑定或节点失效时静默跳过 */
private updatePickCountLabel() {
if (this.pickCountLabel && this.pickCountLabel.isValid) {
this.pickCountLabel.string = `第${this.currentPickIndex}次`;
}
}
private playCoinChangeAnim(isIncrease: boolean) {
if (!this.coins_node || !this.coins_node.isValid) return;
const icon = this.coins_node.getChildByName("icon");

View File

@@ -17,10 +17,11 @@
* revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。
*
* 出生点规则:
* 按攻击类型(HType)分三排:近战 → 前排(x=-200,最靠右迎敌),
* 按攻击类型(HType)落排:近战 → 前排(x=-200,最靠右迎敌),
* 中距 → 中排(x=-260),远程 → 后排(x=-320,最靠左)。
* 同一类型重复召唤时,从该类型基准点向两侧 ±SLOT_OFFSET 依次交替展开,避免落点重叠。
* posIndex 取 heroGrid 全局空闲下标(0~9)用于网格登记/索敌,落点偏移与 posIndex、职业均解耦。
* 同类型落点不再做 ± 偏移展开,落点重叠由 MoveSystem 的编队排队解决
* (准备阶段按职业优先级 坦克→战士→刺客→射手→法师→辅助 从最右向左排开)。
* posIndex 取 heroGrid 全局空闲下标(0~9)用于网格登记/索敌,与落点解耦。
*
* 依赖:
* - Hero(hero/Hero.ts)—— 英雄 ECS 实体类
@@ -39,7 +40,7 @@ import { HeroInfo, HType } from "../common/config/heroSet";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { FacSet, BoxSet } from "../common/config/GameSet";
import { HeroViewComp } from "../hero/HeroViewComp";
import { MoveComp } from "../hero/MoveComp";
const { ccclass } = _decorator;
/**
@@ -63,19 +64,9 @@ export class MissionHeroComp extends CCComp {
public static readonly HERO_ROW_MID = v3(-260, BoxSet.GAME_LINE, 0)
public static readonly HERO_ROW_BACK = v3(-320, BoxSet.GAME_LINE, 0)
/**
* 英雄登场占位点(兼容旧引用,等价于后/中/前三排基准点):
* - index 0=后排、1=中排、2=前排。
*/
public static readonly HERO_POSITIONS: Vec3[] = [
MissionHeroComp.HERO_ROW_BACK,
MissionHeroComp.HERO_ROW_MID,
MissionHeroComp.HERO_ROW_FRONT,
];
/**
* 攻击类型 → 出生排映射:
* 近战 → 前排,中距 → 中排,远程 → 后排。
* 近战 → 前排,中距 → 中排(优先中间位),远程 → 后排。
*/
private static readonly TYPE_ROW: Record<HType, Vec3> = {
[HType.Melee]: MissionHeroComp.HERO_ROW_FRONT,
@@ -83,9 +74,6 @@ export class MissionHeroComp extends CCComp {
[HType.Long]: MissionHeroComp.HERO_ROW_BACK,
};
/** 同类型重复召唤时的落点横向间隔(像素) */
private static readonly SLOT_OFFSET = 30
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
private static readonly HERO_DROP_HEIGHT = 260
@@ -103,6 +91,8 @@ export class MissionHeroComp extends CCComp {
is_processing_queue: boolean = false
/** 召唤请求队列:保证召唤按顺序串行执行 */
summon_queue: { uuid: number; hero_lv: number; card_lv: number }[] = []
/** 英雄出生序计数器(编队排队/渲染排序的稳定决胜) */
private heroSpawnOrder = 0
/** 预留英雄列表 */
heros: any = []
@@ -188,42 +178,26 @@ export class MissionHeroComp extends CCComp {
/**
* 为英雄分配出生槽位:
* 按攻击类型取三排基准点(近战前排、中距中排、远程后排),
* 同类型第 1 个落基准点,之后按 ±SLOT_OFFSET 交替展开(0, +30, -30, +60, -60 …)。
* 按攻击类型取三排基准点(近战前排、中距优先中排、远程后排),同类型不再做横向偏移展开;
* 落点重叠由 MoveSystem 的编队排队(按职业优先级从最右向左排开)负责分开。
*
* 槽位编码:posIndex 直接取 heroGrid 的全局空闲下标(0~9),与落点无映射。
* 落点偏移由「同攻击类型已占位英雄的 x 偏移」推展开序计算,与 posIndex、职业均解耦。
*
* @param type 英雄攻击类型(HType,决定所属排)
* @param heroes 当前存活英雄列表(spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰槽位判定)
* @param heroes 当前存活英雄列表(spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰网格判定)
* @returns { posIndex heroGrid 空闲下标(-1 表示满员未登记), landingPos 落地点 }
*/
private pickSpawnSlotForHero(type: HType, heroes: Hero[]): { posIndex: number; landingPos: Vec3 } {
const basePos = MissionHeroComp.TYPE_ROW[type] ?? MissionHeroComp.HERO_ROW_MID;
// 同攻击类型已占位的 x 偏移集合(依据落点 x 反推,与 posIndex 解耦)
const usedOffsets = new Set<number>();
// 已登记的网格下标集合(仅用于 posIndex 分配)
const occupiedGrid = new Set<number>();
for (const h of heroes) {
const m = h.get(HeroAttrsComp)!;
if (m.posIndex >= 0) occupiedGrid.add(m.posIndex);
if (m.type !== type) continue;
const view = h.get(HeroViewComp);
if (view?.node?.isValid) {
usedOffsets.add(Math.round(view.node.position.x - basePos.x));
}
}
// 展开序 n:0=居中,1=+30,2=-30,3=+60 …,找到第一个未被同类型占用的偏移
// 上限取 heroGrid 容量,同类型超过该数量时复用基准点(不再向外展开)
let offsetX = 0;
for (let n = 0; n < smc.mission.heroGrid.length; n++) {
const step = Math.ceil(n / 2);
const dir = n % 2 === 1 ? 1 : -1;
const candidate = n === 0 ? 0 : dir * step * MissionHeroComp.SLOT_OFFSET;
if (!usedOffsets.has(candidate)) { offsetX = candidate; break; }
}
const landingPos = v3(basePos.x + offsetX, basePos.y, 0);
const landingPos = v3(basePos.x, basePos.y, 0);
// posIndex 取 heroGrid 全局空闲下标,仅用于网格登记/索敌,与落点无映射
let posIndex = -1;
@@ -235,8 +209,9 @@ export class MissionHeroComp extends CCComp {
/**
* 生成一个英雄:
* - 按攻击类型自动分配三排槽位并展开落点。
* - 按攻击类型落排基准点(近战前排 / 中距中排 / 远程后排),不做横向偏移。
* - 计算出生点(空中)和落点(地面),播放掉落动画。
* - 落地后由 MoveSystem 按职业优先级从最右向左排队分开。
*
* @param uuid 英雄 UUID
* @param hero_lv 英雄等级
@@ -259,6 +234,12 @@ export class MissionHeroComp extends CCComp {
let spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos, scale, uuid, landingPos.y, hero_lv, card_lv, finalPosIndex);
// 登记出生序,供编队排队/渲染排序做稳定决胜
const move = hero.get(MoveComp);
if (move) {
move.spawnOrder = ++this.heroSpawnOrder;
}
// 召唤完成后,派发事件以更新英雄面板
const model = hero.get(HeroAttrsComp);
if (model) {

View File

@@ -90,7 +90,7 @@ export class TooltipCom extends CCComp {
const topSiblingIndex = 9999;
// 临时变量,存储当前Y坐标,方便统一处理。增加60的Y轴基础偏移,防止伤害飘字等遮挡血条
let currentY = this.node.position.y + 30;
let currentY = this.node.position.y + 60;
switch (this.stype) {
case TooltipTypes.life: // 普通伤害