修复出怪逻辑bug

master
humingxing 1 week ago
parent 40239b4697
commit 35ffa6281b
  1. 432
      assets/bundles/comm/prefab/PopCoinToast.prefab
  2. 4
      assets/bundles/first/config/Const.json
  3. 414
      assets/bundles/first/config/Monster.json
  4. 7
      assets/mx/module/battle/unit/WaveSpawner.ts
  5. 79
      assets/script/battle/bridge/CocosBattleMovementBridge.ts
  6. 11
      assets/script/battle/combat/MonsterSkillHost.ts
  7. 6
      assets/script/battle/config/TableBattleConfigProvider.ts
  8. 12
      assets/script/battle/heros/HeroActor.ts
  9. 137
      assets/script/battle/presentation/SkillVisualChannel.ts
  10. 1
      assets/script/battle/presentation/ThrownCircleAtkPresenter.ts
  11. 129
      assets/script/battle/view/CocosBattleMonsterPresenter.ts
  12. 5
      assets/script/battle/view/IBattleMonsterView.ts
  13. 20
      assets/script/battle/view/TextGrid.ts
  14. 97
      assets/script/manager/MonsterDataManager.ts
  15. 19
      assets/script/ui/BattleBoss/UIBattleBoss.ts
  16. 5
      assets/script/ui/BattleGame/BattleGameMenShenTrapController.ts
  17. 89
      assets/script/ui/BattleGame/Monster.ts
  18. 7
      assets/script/ui/BattleGame/UIBattleGame.ts
  19. 21
      docs/方案-出怪逻辑实现.md
  20. 4
      docs/方案-战斗表现数据驱动.md

@ -28,17 +28,17 @@
"_active": true,
"_components": [
{
"__id__": 87
"__id__": 75
},
{
"__id__": 89
"__id__": 77
},
{
"__id__": 91
"__id__": 79
}
],
"_prefab": {
"__id__": 93
"__id__": 81
},
"_lpos": {
"__type__": "cc.Vec3",
@ -398,26 +398,20 @@
"__id__": 32
},
{
"__id__": 38
},
{
"__id__": 44
},
{
"__id__": 66
"__id__": 54
}
],
"_active": true,
"_components": [
{
"__id__": 82
"__id__": 70
},
{
"__id__": 84
"__id__": 72
}
],
"_prefab": {
"__id__": 86
"__id__": 74
},
"_lpos": {
"__type__": "cc.Vec3",
@ -549,7 +543,7 @@
"a": 255
},
"_spriteFrame": {
"__uuid__": "d7c2cb35-ecb8-47c3-906e-3b9da168423b@f9941",
"__uuid__": "5592f520-f3c0-4003-8646-e07a8d2ec0c9@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
@ -586,7 +580,7 @@
},
{
"__type__": "cc.Node",
"_name": "弹窗标题底",
"_name": "desc",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
@ -605,278 +599,6 @@
"_prefab": {
"__id__": 31
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 2.512,
"y": 169.296,
"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": 33554432,
"_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": 249,
"height": 71
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "7bgR1pERlOloS4CPV4/Qj7"
},
{
"__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__": "46c4f42e-8df5-4c75-93aa-612fab9f0000@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "c6P/RJb1dIqbFsg/R37WwN"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "batlAV3YBEtZSQ6lWWSOIH",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "ui_title1",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 19
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 33
},
{
"__id__": 35
}
],
"_prefab": {
"__id__": 37
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 171.564,
"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": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 32
},
"_enabled": true,
"__prefab": {
"__id__": 34
},
"_contentSize": {
"__type__": "cc.Size",
"width": 147,
"height": 30
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "46HcfMnvNE0LCUrLwHVu3r"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 32
},
"_enabled": true,
"__prefab": {
"__id__": 36
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "a5cba4ba-23ef-4bb7-89c6-6dea6c1736ec@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "43c0S0kUBNz5S6QcnSmFGZ"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "919j1y5f9JTZWMIVUJKZho",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "desc",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 19
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 39
},
{
"__id__": 41
}
],
"_prefab": {
"__id__": 43
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
@ -912,11 +634,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 38
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 40
"__id__": 28
},
"_contentSize": {
"__type__": "cc.Size",
@ -940,11 +662,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 38
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 42
"__id__": 30
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@ -1025,29 +747,29 @@
},
"_children": [
{
"__id__": 45
"__id__": 33
},
{
"__id__": 51
"__id__": 39
}
],
"_active": true,
"_components": [
{
"__id__": 57
"__id__": 45
},
{
"__id__": 59
"__id__": 47
},
{
"__id__": 61
"__id__": 49
},
{
"__id__": 63
"__id__": 51
}
],
"_prefab": {
"__id__": 65
"__id__": 53
},
"_lpos": {
"__type__": "cc.Vec3",
@ -1084,20 +806,20 @@
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 44
"__id__": 32
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 46
"__id__": 34
},
{
"__id__": 48
"__id__": 36
}
],
"_prefab": {
"__id__": 50
"__id__": 38
},
"_lpos": {
"__type__": "cc.Vec3",
@ -1134,15 +856,15 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 45
"__id__": 33
},
"_enabled": true,
"__prefab": {
"__id__": 47
"__id__": 35
},
"_contentSize": {
"__type__": "cc.Size",
"width": 114,
"width": 62,
"height": 31
},
"_anchorPoint": {
@ -1162,11 +884,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 45
"__id__": 33
},
"_enabled": true,
"__prefab": {
"__id__": 49
"__id__": 37
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@ -1179,7 +901,7 @@
"a": 255
},
"_spriteFrame": {
"__uuid__": "25223971-df9c-4e83-a986-56c92c4b90bf@f9941",
"__uuid__": "ae9a2d49-4f7c-49b3-bf32-5e8bc6017a1e@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
@ -1220,20 +942,20 @@
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 44
"__id__": 32
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 52
"__id__": 40
},
{
"__id__": 54
"__id__": 42
}
],
"_prefab": {
"__id__": 56
"__id__": 44
},
"_lpos": {
"__type__": "cc.Vec3",
@ -1270,11 +992,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
"__id__": 39
},
"_enabled": true,
"__prefab": {
"__id__": 53
"__id__": 41
},
"_contentSize": {
"__type__": "cc.Size",
@ -1298,11 +1020,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
"__id__": 39
},
"_enabled": true,
"__prefab": {
"__id__": 55
"__id__": 43
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@ -1356,11 +1078,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 44
"__id__": 32
},
"_enabled": true,
"__prefab": {
"__id__": 58
"__id__": 46
},
"_contentSize": {
"__type__": "cc.Size",
@ -1384,11 +1106,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 44
"__id__": 32
},
"_enabled": true,
"__prefab": {
"__id__": 60
"__id__": 48
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@ -1406,7 +1128,7 @@
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
@ -1429,11 +1151,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 44
"__id__": 32
},
"_enabled": true,
"__prefab": {
"__id__": 62
"__id__": 50
},
"clickEvents": [],
"_interactable": true,
@ -1485,11 +1207,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 44
"__id__": 32
},
"_enabled": true,
"__prefab": {
"__id__": 64
"__id__": 52
},
"_id": ""
},
@ -1520,26 +1242,26 @@
},
"_children": [
{
"__id__": 67
"__id__": 55
}
],
"_active": true,
"_components": [
{
"__id__": 73
"__id__": 61
},
{
"__id__": 75
"__id__": 63
},
{
"__id__": 77
"__id__": 65
},
{
"__id__": 79
"__id__": 67
}
],
"_prefab": {
"__id__": 81
"__id__": 69
},
"_lpos": {
"__type__": "cc.Vec3",
@ -1576,20 +1298,20 @@
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 66
"__id__": 54
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 68
"__id__": 56
},
{
"__id__": 70
"__id__": 58
}
],
"_prefab": {
"__id__": 72
"__id__": 60
},
"_lpos": {
"__type__": "cc.Vec3",
@ -1626,11 +1348,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 67
"__id__": 55
},
"_enabled": true,
"__prefab": {
"__id__": 69
"__id__": 57
},
"_contentSize": {
"__type__": "cc.Size",
@ -1654,11 +1376,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 67
"__id__": 55
},
"_enabled": true,
"__prefab": {
"__id__": 71
"__id__": 59
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@ -1676,7 +1398,7 @@
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
@ -1712,11 +1434,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 66
"__id__": 54
},
"_enabled": true,
"__prefab": {
"__id__": 74
"__id__": 62
},
"_contentSize": {
"__type__": "cc.Size",
@ -1740,11 +1462,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 66
"__id__": 54
},
"_enabled": true,
"__prefab": {
"__id__": 76
"__id__": 64
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@ -1762,7 +1484,7 @@
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
@ -1785,11 +1507,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 66
"__id__": 54
},
"_enabled": true,
"__prefab": {
"__id__": 78
"__id__": 66
},
"clickEvents": [],
"_interactable": true,
@ -1841,11 +1563,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 66
"__id__": 54
},
"_enabled": true,
"__prefab": {
"__id__": 80
"__id__": 68
},
"_id": ""
},
@ -1876,7 +1598,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 83
"__id__": 71
},
"_contentSize": {
"__type__": "cc.Size",
@ -1904,7 +1626,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 85
"__id__": 73
},
"_alignFlags": 45,
"_target": null,
@ -1953,7 +1675,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 88
"__id__": 76
},
"_contentSize": {
"__type__": "cc.Size",
@ -1981,7 +1703,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 90
"__id__": 78
},
"_alignFlags": 45,
"_target": null,
@ -2017,7 +1739,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 92
"__id__": 80
},
"_id": ""
},

@ -20,13 +20,13 @@
{
"id": 4,
"desc": "战斗初始刷新消耗",
"para": 10,
"para": 50,
"para1": ""
},
{
"id": 5,
"desc": "战斗刷新消耗递增值",
"para": 2,
"para": 0,
"para1": ""
},
{

@ -1,380 +1,4 @@
[
{
"id": 101,
"name": "猴",
"desc": "",
"icon": "猴",
"res": [
"猴",
"猴",
"猩",
"猩",
"猿"
],
"type": 1,
"speed": 40,
"atkdistance": 83,
"size": 1,
"hp": 1,
"exp": 1,
"peach": 1,
"weapondrop": 50,
"skill": 0
},
{
"id": 105,
"name": "奔波儿灞",
"desc": "",
"icon": "奔波儿灞",
"res": [
"奔波儿灞"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 6.5,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30105
},
{
"id": 106,
"name": "牛魔王",
"desc": "",
"icon": "牛魔王",
"res": [
"牛魔王"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 12,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30106
},
{
"id": 107,
"name": "六耳",
"desc": "",
"icon": "六耳",
"res": [
"六耳"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 15,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30107
},
{
"id": 201,
"name": "虾",
"desc": "",
"icon": "虾",
"res": [
"虾",
"虾",
"蟹",
"蟹",
"龟"
],
"type": 1,
"speed": 40,
"atkdistance": 83,
"size": 1,
"hp": 1,
"exp": 1,
"peach": 1,
"weapondrop": 50,
"skill": 0
},
{
"id": 205,
"name": "蟹将",
"desc": "",
"icon": "蟹将",
"res": [
"蟹将"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 6.5,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30205
},
{
"id": 206,
"name": "龟丞相",
"desc": "",
"icon": "龟丞相",
"res": [
"龟丞相"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 12,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30206
},
{
"id": 207,
"name": "敖广",
"desc": "",
"icon": "敖广",
"res": [
"敖广"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 15,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30207
},
{
"id": 301,
"name": "魂",
"desc": "",
"icon": "魂",
"res": [
"魂",
"魂",
"魅",
"魅",
"魈"
],
"type": 1,
"speed": 40,
"atkdistance": 83,
"size": 1,
"hp": 1,
"exp": 1,
"peach": 1,
"weapondrop": 50,
"skill": 0
},
{
"id": 305,
"name": "黑无常",
"desc": "",
"icon": "黑无常",
"res": [
"黑无常"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 6.5,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30305
},
{
"id": 306,
"name": "白无常",
"desc": "",
"icon": "白无常",
"res": [
"白无常"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 12,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30306
},
{
"id": 307,
"name": "谛听",
"desc": "",
"icon": "谛听",
"res": [
"谛听"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 15,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30307
},
{
"id": 401,
"name": "兵",
"desc": "",
"icon": "兵",
"res": [
"兵",
"兵",
"将",
"将",
"帅"
],
"type": 1,
"speed": 40,
"atkdistance": 83,
"size": 1,
"hp": 1,
"exp": 1,
"peach": 1,
"weapondrop": 50,
"skill": 0
},
{
"id": 405,
"name": "千里眼",
"desc": "",
"icon": "千里眼",
"res": [
"千里眼"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 6.5,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30405
},
{
"id": 406,
"name": "顺风耳",
"desc": "",
"icon": "顺风耳",
"res": [
"顺风耳"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 12,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30406
},
{
"id": 407,
"name": "太白",
"desc": "",
"icon": "太白",
"res": [
"太白"
],
"type": 2,
"speed": 25,
"atkdistance": 208,
"size": 1,
"hp": 15,
"exp": 1,
"peach": 1,
"weapondrop": 500,
"skill": 30407
},
{
"id": 10001,
"name": "哮天犬",
"desc": "",
"icon": "哮天犬",
"res": [
"哮天犬"
],
"type": 3,
"speed": 40,
"atkdistance": 83,
"size": 1,
"hp": 4,
"exp": 1,
"peach": 1,
"weapondrop": 0,
"skill": 40001
},
{
"id": 10002,
"name": "猴",
"desc": "",
"icon": "猴",
"res": [
"猴"
],
"type": 3,
"speed": 40,
"atkdistance": 83,
"size": 1,
"hp": 3,
"exp": 1,
"peach": 1,
"weapondrop": 0,
"skill": 0
},
{
"id": 10003,
"name": "魂",
"desc": "",
"icon": "魂",
"res": [
"魂"
],
"type": 3,
"speed": 60,
"atkdistance": 83,
"size": 1,
"hp": 3,
"exp": 1,
"peach": 1,
"weapondrop": 0,
"skill": 0
},
{
"id": 10004,
"name": "木小怪",
"desc": "",
"icon": "魂",
"res": [
"魂"
],
"type": 3,
"speed": 60,
"atkdistance": 83,
"size": 1,
"hp": 3,
"exp": 1,
"peach": 1,
"weapondrop": 0,
"skill": 0
},
{
"id": 1001,
"name": "金小怪1",
@ -433,9 +57,9 @@
"id": 1101,
"name": "金boss",
"desc": "",
"icon": "BOSS金",
"icon": "Boss金",
"res": [
"BOSS金"
"Boss金"
],
"type": 2,
"speed": 25,
@ -505,9 +129,9 @@
"id": 2101,
"name": "木boss",
"desc": "",
"icon": "BOSS木",
"icon": "Boss木",
"res": [
"BOSS木"
"Boss木"
],
"type": 2,
"speed": 25,
@ -577,9 +201,9 @@
"id": 3101,
"name": "水boss",
"desc": "",
"icon": "BOSS水",
"icon": "Boss水",
"res": [
"BOSS水"
"Boss水"
],
"type": 2,
"speed": 25,
@ -649,9 +273,9 @@
"id": 4101,
"name": "火boss",
"desc": "",
"icon": "BOSS火",
"icon": "Boss火",
"res": [
"BOSS火"
"Boss火"
],
"type": 2,
"speed": 25,
@ -721,9 +345,9 @@
"id": 5101,
"name": "土boss",
"desc": "",
"icon": "BOSS土",
"icon": "Boss土",
"res": [
"BOSS土"
"Boss土"
],
"type": 2,
"speed": 25,
@ -734,5 +358,23 @@
"peach": 10,
"weapondrop": 50,
"skill": 51101
},
{
"id": 10004,
"name": "木小怪",
"desc": "",
"icon": "小怪木",
"res": [
"小怪木"
],
"type": 3,
"speed": 60,
"atkdistance": 83,
"size": 1,
"hp": 3,
"exp": 1,
"peach": 1,
"weapondrop": 0,
"skill": 0
}
]

@ -608,14 +608,13 @@ export class WaveSpawner implements ISystem {
move.spawnProtected = true;
}
const visualPort = startCell ? 0 : spawnPoint;
// 先寻路再派发:Presenter 跳出读 path[0];禁止空 path 时表现先落到错误坐标
if (move && via) {
this._movement.assignPathVia(entity, via);
}
// 立即派发:Presenter 在解锁 presentLocked 前须已有路径(技能跳读 path[0])
this._events.emit(BattleEventFactory.unitSpawned(entity, unitDefId, visualPort, maxHp), true);
if (move && !startCell && !via) {
} else if (move && !startCell) {
this._movement.assignPath(entity);
}
this._events.emit(BattleEventFactory.unitSpawned(entity, unitDefId, visualPort, maxHp), true);
return entity;
}
}

@ -81,14 +81,10 @@ export function syncMovementNode(
const tf = session.world.getComponent(entityId, TransformComp);
if (!move || !tf) return;
resolveEntityWorldPosInto(
session,
map,
tf.row,
tf.col,
move,
_syncWorldPos,
);
if (!resolveEntityWorldPosInto(session, map, tf.row, tf.col, move, _syncWorldPos)) {
console.error('[CocosBattleMovementBridge] syncMovementNode 坐标解析失败,保持原位', entityId, tf.row, tf.col);
return;
}
node.setWorldPosition(_syncWorldPos);
}
@ -134,7 +130,18 @@ export function syncMonsterNode(
return;
}
resolveEntityWorldPosInto(session, map, tf.row, tf.col, move, _syncWorldPos);
if (!resolveEntityWorldPosInto(session, map, tf.row, tf.col, move, _syncWorldPos)) {
console.error(
'[CocosBattleMovementBridge] syncMonsterNode 坐标解析失败,保持原位',
entityId,
tf.row,
tf.col,
move.worldX,
move.worldY,
);
syncMonsterCombatState(session, entityId, node);
return;
}
applyMonsterFootOffset(_syncWorldPos, _syncFootPos, map.mapGridSize);
if (!setMonsterNodeWorldPos(node, _syncFootPos)) {
syncMonsterCombatState(session, entityId, node);
@ -231,6 +238,10 @@ export function resolveSpawnJumpVisualPosInto(
return true;
}
/**
* 解析实体表现世界坐标。
* @returns 成功写入 out 时为 true;格节点缺失时为 false(不得写 0,0)
*/
function resolveEntityWorldPosInto(
session: BattleSession,
map: MapManager,
@ -238,25 +249,22 @@ function resolveEntityWorldPosInto(
col: number,
movement: MovementComp,
out: Vec3,
): void {
): boolean {
// 已停步开战(path 已清空)或走完路径:用逻辑 world 映射到 Cocos,禁止 (x,-y) 或强行吸回格心
if (movement.reachedGoal || movement.pathIndex >= movement.path.length) {
logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
return;
return logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
}
const curGrid = map.getMapGrid(row, col);
if (!curGrid?.node?.isValid) {
logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
return;
return logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
}
const curCenter = session.grid.cellToWorld({ row, col });
const next = movement.path[movement.pathIndex];
const nextGrid = map.getMapGrid(next.row, next.col);
if (!nextGrid?.node?.isValid) {
logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
return;
return logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
}
const nextCenter = session.grid.cellToWorld(next);
@ -271,14 +279,18 @@ function resolveEntityWorldPosInto(
: Math.sqrt(px * px + py * py);
// 击退等会把逻辑坐标打出路径线段;再按路程投影会把后退/侧移折成前进
if (along < -1 || along > total + 1 || perp > 4) {
logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
return;
return logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
}
const t = total > 1e-4 ? Math.min(1, Math.max(0, along / total)) : 1;
Vec3.lerp(out, curGrid.node.worldPosition, nextGrid.node.worldPosition, t);
return true;
}
/**
* 逻辑世界坐标 → Cocos 世界坐标(写入 out)。
* @returns 地图格可用时 true;缺失时 false 且不改 out(禁止写成屏幕原点)
*/
function logicalWorldToCocosInto(
session: BattleSession,
map: MapManager,
@ -287,7 +299,7 @@ function logicalWorldToCocosInto(
logicalX: number,
logicalY: number,
out: Vec3,
): void {
): boolean {
const logicalCenter = session.grid.cellToWorld({ row, col });
const ox = logicalX - logicalCenter.x;
const oy = logicalY - logicalCenter.y;
@ -296,19 +308,28 @@ function logicalWorldToCocosInto(
if (curGrid?.node?.isValid) {
const cw = curGrid.node.worldPosition;
out.set(cw.x + ox, cw.y - oy, 0);
return;
return true;
}
// 格节点缺失时仍按同尺度映射,不用错误的 Y 取反绝对坐标
console.log('[CocosBattleMovementBridge] map grid missing for visual sync', row, col);
out.set(logicalX, -logicalY, 0);
console.error(
'[CocosBattleMovementBridge] map grid missing for visual sync',
row,
col,
'map=',
map.mapRows,
'x',
map.mapCols,
'logic=',
logicalX,
logicalY,
);
return false;
}
/**
* 逻辑世界坐标(GridMap,Y 向下)→ Cocos 世界坐标(Y 向上)
* 以当前格心为锚做局部偏移,避免空 path 时用 (worldX, -worldY) 飞出屏幕
* 逻辑世界坐标(GridMap,Y 向下)→ Cocos 世界坐标(Y 向上)。
* 以当前格心为锚做局部偏移;格节点缺失返回 null(禁止落到屏幕原点)。
*/
/** 逻辑世界坐标 → Cocos 世界坐标 */
export function logicalWorldToCocos(
session: BattleSession,
map: MapManager,
@ -316,9 +337,11 @@ export function logicalWorldToCocos(
col: number,
logicalX: number,
logicalY: number,
): Vec3 {
): Vec3 | null {
const out = v3();
logicalWorldToCocosInto(session, map, row, col, logicalX, logicalY, out);
if (!logicalWorldToCocosInto(session, map, row, col, logicalX, logicalY, out)) {
return null;
}
return out;
}

@ -441,7 +441,8 @@ export class MonsterSkillHost {
ctx.finish();
return;
}
if (sk.animation === '死亡') {
const deathAnim = MonsterDataManager.resolveDeathAnim(view.Data ?? null);
if (sk.animation === '死亡' || (deathAnim && sk.animation === deathAnim)) {
ctx.finish();
return;
}
@ -488,8 +489,12 @@ export class MonsterSkillHost {
const view = node?.isValid
? node.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null
: null;
if (view?.playWalk && view.anim?.isValid && view.anim.animation !== '死亡') {
view.playWalk();
if (view?.playWalk && view.anim?.isValid) {
const deathAnim = MonsterDataManager.resolveDeathAnim(view.Data ?? null);
const cur = String(view.anim.animation ?? '');
if (cur !== '死亡' && !(deathAnim && cur === deathAnim)) {
view.playWalk();
}
}
this._consumeDevour(item.entity);
this._spawnDestroyIfNeeded(ctx);

@ -45,7 +45,6 @@ export class TableBattleConfigProvider implements IConfigProvider {
for (const buff of buildBasicDebuffDefs()) {
this._store.buffs.set(buff.id, buff);
}
this.ensureFallbackEnemy();
}
/** 每次开战刷新 Weapon1 单位映射 */
@ -279,9 +278,4 @@ export class TableBattleConfigProvider implements IConfigProvider {
return finitePositiveHp(row.hp, 1);
}
private ensureFallbackEnemy(): void {
if (!this._store.units.has('monster_101')) {
this.ensureMonsterUnit('monster_101');
}
}
}

@ -9,7 +9,7 @@ const { ccclass } = _decorator;
/**
* 通用英雄表现壳:字形 + 可选武器通道,三槽均走 SkillPresenterRouter。
* 组合英雄激活后加载当前装备武器,锚在组合首字靠左。
* 组合英雄 / 五行数字激活后常驻武器(格子左侧);五行数字不播字形「攻击」。
*/
@ccclass('HeroActor')
export class HeroActor extends Skill {
@ -71,7 +71,7 @@ export class HeroActor extends Skill {
this._heroSkin?.playIdleAnim();
}
/** 绑定字形;五行 / 组合英雄启用武器通道;五行仅攻击显示武器 */
/** 绑定字形;五行启用武器通道(常驻格子左侧,不播数字「攻击」) */
protected _bindChannel(): void {
this._heroSkin = this.node.getComponent(HeroSkin);
const data = this._heroSkin?.Data
@ -99,12 +99,8 @@ export class HeroActor extends Skill {
if (this.isActive && !this.isPresentationBusy()) this.onIdle();
}
/** 首字格 / 武器预制体晚于激活:下一帧再挂待机武器(五行数字不补挂) */
/** 首字格 / 武器预制体晚于激活:下一帧再挂待机武器 */
private _retryIdleWeapon(): void {
if (this.visualChannel.attackOnlyWeapon) {
this._weaponRetry = 0;
return;
}
if (!this.isActive || !this.visualChannel.useWeapon || this.isPresentationBusy()) {
this._weaponRetry = 0;
return;
@ -114,7 +110,7 @@ export class HeroActor extends Skill {
return;
}
if (this._weaponRetry >= 60) {
console.warn('[HeroActor] 组合武器未显示', this._heroId, this._entityId);
console.warn('[HeroActor] 武器未显示', this._heroId, this._entityId);
return;
}
this._weaponRetry++;

@ -245,17 +245,17 @@ const GLYPH_IDLE_ANIM = '待机';
/**
* 视觉通道:字形 + 可选武器挂载。
* 组合英雄激活后常驻显示当前装备武器;五行数字仅攻击显示武器。
* 组合英雄 / 五行数字激活后常驻显示武器(格子左侧);五行数字不播字形「攻击」。
*/
export class SkillVisualChannel {
private _skin: HeroSkin | null = null;
/** 五行数字节点 Spine(无 HeroSkin 时播攻击/待机) */
/** 五行数字节点 Spine(无 HeroSkin 时播待机;不播攻击) */
private _glyphAnim: sp.Skeleton | null = null;
private readonly _weapon = new HeroWeaponMount();
private _parent: Node | null = null;
private _heroId = 0;
private _useWeapon = false;
/** 五行数字:仅攻击显示武器,收招后 1 秒无出手再藏 */
/** 五行数字:武器常驻;弓/出手帧走五行路径;数字不播「攻击」 */
private _attackOnlyWeapon = false;
/** 五行武器本地 X;null 用 WEAPON_LEFT_OFFSET_PX */
private _weaponLocalX: number | null = null;
@ -303,7 +303,7 @@ export class SkillVisualChannel {
return this._useWeapon;
}
/** 五行数字:仅攻击显示武器 */
/** 五行数字通道(武器常驻、不播数字攻击) */
get attackOnlyWeapon(): boolean {
return this._attackOnlyWeapon;
}
@ -314,7 +314,7 @@ export class SkillVisualChannel {
}
/**
* 五行数字:只在攻击时显示武器。
* 标记五行数字通道。
* @param on 是否开启
*/
setAttackOnlyWeapon(on: boolean): void {
@ -322,7 +322,7 @@ export class SkillVisualChannel {
}
/**
* 绑定五行数字 Spine(播「攻击」/「待机」)。
* 绑定五行数字 Spine(仅待机;不播攻击)。
* @param anim 数字节点 Skeleton
*/
setGlyphAnim(anim: sp.Skeleton | null): void {
@ -428,18 +428,14 @@ export class SkillVisualChannel {
}
/**
* 激活待机:组合英雄显示武器;五行数字藏武器并播数字「待机」。
* 激活待机:显示武器;五行数字播「待机」(不播攻击)。
* 武器释放中 / 连击窗内跳过,避免掐断出手。
*/
playIdle(): void {
// 定格窗内禁止切待机动画:否则 Spine 末帧会被待机姿覆盖,武器角也像被拧回
if (this._weaponCasting || this._restHoldActive) return;
if (this._attackOnlyWeapon) {
this._playGlyphIdle();
this._weapon.setVisible(false);
return;
}
this._skin?.playIdleAnim();
if (this._attackOnlyWeapon) this._playGlyphIdle();
else this._skin?.playIdleAnim();
if (this._useWeapon) this.showIdleWeapon();
else this._weapon.setVisible(false);
}
@ -449,7 +445,6 @@ export class SkillVisualChannel {
* @returns 是否已显示待机武器
*/
tryShowIdleWeapon(): boolean {
if (this._attackOnlyWeapon) return false;
if (!this._useWeapon || this._weaponCasting || this._restHoldActive || this._weaponFlies.length > 0) return false;
return this.showIdleWeapon();
}
@ -516,14 +511,15 @@ export class SkillVisualChannel {
this._cancelWeaponAim();
this._cancelWeaponOrbit();
this._weapon.hideBowAtk(true);
if (this._useWeapon && !this._attackOnlyWeapon) this._weapon.setVisible(true);
if (this._useWeapon) this._weapon.setVisible(true);
else this._weapon.setVisible(false);
if (this._caster != null) getBowAtkHost().cancelCaster(this._caster);
}
/**
* 释放表现:字形播「攻击」;有武器时武器播 Skill.animation(缺省「攻击」)。
* 跟随武器时等字形与武器都播完再收招。玉帝飞剑:不播原武器攻击,直接隐藏。
* 释放表现:组合字形播「攻击」;五行数字不播字形攻击。
* 有武器时武器播 Skill.animation(缺省「攻击」)。跟随武器时等字形与武器都播完再收招。
* 玉帝飞剑:不播原武器攻击,直接隐藏。
* @returns 本段释放动画倍速(供命中延迟换算;主动技能为 1)
*/
playCastAnim(params: CastAnimParams): number {
@ -543,6 +539,7 @@ export class SkillVisualChannel {
console.warn('[SkillVisualChannel] Skill.animation 为空', params.caster);
}
const follow = params.followWeapon && this._useWeapon && this.ensureWeapon();
const playGlyph = !this._attackOnlyWeapon;
if (follow) {
this._cancelRestHold();
this._cancelWeaponAim();
@ -577,7 +574,7 @@ export class SkillVisualChannel {
}
if (orbitStaff) this._startWukongStaffOrbit(params.caster, weaponAnim, animScale);
if (params.holdCasting) {
this._playGlyphAttack(animScale, null);
if (playGlyph) this._playGlyphAttack(animScale, null);
if (params.throwWeapon) {
this._onFollowCastAllComplete();
return animScale;
@ -597,7 +594,7 @@ export class SkillVisualChannel {
}
return animScale;
}
if (this._hasGlyphCastAnim()) {
if (playGlyph && this._hasGlyphCastAnim()) {
this._castAnimLeft++;
this._playGlyphAttack(animScale, this._onCastAnimPartComplete);
}
@ -614,8 +611,12 @@ export class SkillVisualChannel {
}
return animScale;
}
this._playGlyphAttack(animScale, params.holdCasting ? null : params.onAnimComplete);
if (!this._hasGlyphCastAnim() && !params.holdCasting) params.onAnimComplete();
if (playGlyph) {
this._playGlyphAttack(animScale, params.holdCasting ? null : params.onAnimComplete);
if (!this._hasGlyphCastAnim() && !params.holdCasting) params.onAnimComplete();
} else if (!params.holdCasting) {
params.onAnimComplete();
}
return animScale;
}
@ -626,10 +627,6 @@ export class SkillVisualChannel {
this._castAnimLeft = 0;
this._cancelWeaponOrbit();
this._cancelRestHold();
if (this._attackOnlyWeapon) {
this._enterAttackOnlyRestHold();
return;
}
if (this._shouldTweenRestAim()) {
// 弓/枪:先连击窗保持当前角,超时再回待机
this._enterWeaponRestHold(1);
@ -670,64 +667,6 @@ export class SkillVisualChannel {
});
}
/**
* 五行数字:攻击结束后保持武器可见。
* 弓/枪:定格攻击末帧并保持当前朝向,1 秒无下一击再插值回待机角后藏武器。
* 刀/木等:可立刻复位;超时藏武器并播数字「待机」。
*/
private _enterAttackOnlyRestHold(): void {
this._cancelRestHold();
this._weapon.setArrowVisible(false);
this.syncWeaponPivot(this._caster, true);
this._weapon.setVisible(true);
// 弓/枪:连击窗内保持攻击姿与瞄准角(对齐组合弓/枪 _enterWeaponRestHold)
if (this._shouldTweenRestAim()) {
const skin = this._weapon.skinAnim;
if (skin?.isValid) {
BattleSpinePlayback.clearPendingComplete(skin);
skin.timeScale = 0;
}
this._restHoldActive = true;
this._restHoldAspd = 1;
const seq = ++this._restHoldSeq;
schedulePresentation(WEAPON_REST_HOLD_SEC, () => {
if (seq !== this._restHoldSeq) return;
if (this._weaponCasting) return;
this._restHoldActive = false;
if (skin?.isValid) skin.timeScale = 1;
this._restAttackOnlyWeaponAim();
});
return;
}
if (!this._isStaffWeapon()) this._weapon.resetAim();
this._weapon.playIdle();
this._restHoldActive = true;
const seq = ++this._restHoldSeq;
schedulePresentation(WEAPON_REST_HOLD_SEC, () => {
if (seq !== this._restHoldSeq) return;
if (this._weaponCasting) return;
this._restHoldActive = false;
this._weapon.setVisible(false);
this._playGlyphIdle();
});
}
/**
* 五行弓/枪:连击窗结束,从当前瞄准角插值回待机角后再藏武器。
*/
private _restAttackOnlyWeaponAim(): void {
const duration = WEAPON_AIM_SEC / Math.max(0.01, this._restHoldAspd > 0 ? this._restHoldAspd : 1);
this._tweenWeaponAim(WEAPON_REST_ANGLE, duration, this._onAttackOnlyAimRestCompleteBound);
}
/** 五行弓/枪拧回待机角后:藏武器并播数字待机 */
private readonly _onAttackOnlyAimRestCompleteBound = (): void => {
if (this._weaponCasting) return;
this._weapon.resetAim();
this._weapon.setVisible(false);
this._playGlyphIdle();
};
/** 是否有字形释放 Spine */
private _hasGlyphCastAnim(): boolean {
return !!(this._skin?.anim?.isValid || this._resolveGlyphAnim()?.isValid);
@ -786,7 +725,7 @@ export class SkillVisualChannel {
}
/**
* 播字形「攻击」:优先 HeroSkin,否则五行数字 Spine。
* 播字形「攻击」:HeroSkin(组合英雄);五行数字由 playCastAnim 跳过。
* @param timeScale 倍速
* @param onComplete 播完;null 不等待
*/
@ -867,20 +806,16 @@ export class SkillVisualChannel {
if (!params) return;
if (!params.holdCasting) {
this._weaponCasting = false;
if (this._attackOnlyWeapon) {
this._enterAttackOnlyRestHold();
const aspd = Math.max(0.01, SkillPresentationEnv.getFinalAttackSpeed(params.caster));
const restScale = params.animTimeScale != null && params.animTimeScale > 0 ? params.animTimeScale : aspd;
if (this._isBowWeapon() && this._weapon.bowAtkShowing) {
this._weapon.freezeBowDraw();
// 弓与枪相同:连击窗保持当前角,超时再回待机
this._enterWeaponRestHold(restScale);
} else if (this._shouldTweenRestAim()) {
this._enterWeaponRestHold(restScale);
} else {
const aspd = Math.max(0.01, SkillPresentationEnv.getFinalAttackSpeed(params.caster));
const restScale = params.animTimeScale != null && params.animTimeScale > 0 ? params.animTimeScale : aspd;
if (this._isBowWeapon() && this._weapon.bowAtkShowing) {
this._weapon.freezeBowDraw();
// 弓与枪相同:连击窗保持当前角,超时再回待机
this._enterWeaponRestHold(restScale);
} else if (this._shouldTweenRestAim()) {
this._enterWeaponRestHold(restScale);
} else {
this._weapon.playIdle();
}
this._weapon.playIdle();
}
}
params.onAnimComplete();
@ -897,9 +832,13 @@ export class SkillVisualChannel {
const parent = this._parent;
if (!root?.isValid || !parent?.isValid) return false;
const id = caster ?? this._caster;
// 五行数字:挂在字块本地,按 TextGrid.wPosX
// 五行数字:挂在字块本地,按 TextGrid.wPosX(须靠左为负)
if (this._weapon.weaponId <= 0) {
const x = this._weaponLocalX != null ? this._weaponLocalX : -WEAPON_LEFT_OFFSET_PX;
let x = this._weaponLocalX != null ? this._weaponLocalX : -WEAPON_LEFT_OFFSET_PX;
if (!(x < 0)) {
console.error('[SkillVisualChannel] 五行武器本地 X 须靠左', this._heroId, x);
x = -WEAPON_LEFT_OFFSET_PX;
}
root.setPosition(x, 0, 0);
root.angle = 0;
if (this._weapon.bowAtkShowing) this._weapon.syncBowAtkPivot();

@ -328,6 +328,7 @@ export class ThrownCircleHost {
const tf = session.world.getComponent(lockId, TransformComp);
if (!tf) return null;
const cocos = logicalWorldToCocos(session, map, tf.row, tf.col, center.x, center.y);
if (!cocos) return null;
cocos.z = z;
return cocos;
}

@ -106,6 +106,16 @@ const SHADOW_PREFAB_GLOW = '发光阴影';
* 未配置的怪会 warn 并用 1(以龟丞相为基准)。
*/
const MONSTER_SHADOW_SCALE: { [prefabName: string]: number } = {
小怪金: 0.55,
小怪木: 0.55,
小怪水: 0.55,
小怪火: 0.55,
小怪土: 0.55,
Boss金: 1,
Boss木: 1,
Boss水: 1,
Boss火: 1,
Boss土: 1,
兵: 0.5,
将: 0.5,
帅: 0.55,
@ -885,7 +895,7 @@ export class CocosBattleMonsterPresenter {
view?.syncMxHp?.(hp, maxHp);
if (view) view.state = MonsterState.STOP;
if (view?.Data?.type !== MonsterType.TreasureBox) {
view?.playSpineAnim?.('待机', true);
view?.playWalk?.();
}
const growSec = view?.playReviveEffect?.(this._monsterRoot ?? undefined) ?? 0;
@ -968,8 +978,35 @@ export class CocosBattleMonsterPresenter {
this._syncMonsterShadow(entityId);
return;
}
membershipDelete(this._pendingRecycle, entityId);
const node = this._entityNodes.get(entityId);
const view = node?.isValid
? (node.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null)
: null;
const data = view?.Data;
// 五行小怪:先播契约死亡轨再回收
if (node?.isValid && data && MonsterDataManager.isElementSmallMonster(data) && view?.playDeath) {
if (membershipHas(this._spawningEntities, entityId)) {
membershipDelete(this._spawningEntities, entityId);
}
this._unlockPresentMovement(entityId);
this._clearSpawnProtected(entityId);
this._clearSpawnInvulnTimer(entityId);
const played = view.playDeath(() => {
this._finishRemoveVisual(entityId, node);
});
if (played) return;
console.error('[CocosBattleMonsterPresenter] 五行小怪死亡轨未起播', data.id, data.icon);
}
this._finishRemoveVisual(entityId, node ?? null);
}
/**
* 回收怪物节点与映射。
* @param entityId 实体
* @param node 表现节点
*/
private _finishRemoveVisual(entityId: EntityId, node: Node | null): void {
membershipDelete(this._pendingRecycle, entityId);
this._entityNodes.delete(entityId);
membershipDelete(this._spawningEntities, entityId);
this._unlockPresentMovement(entityId);
@ -1013,7 +1050,12 @@ export class CocosBattleMonsterPresenter {
const tf = session.world.getComponent(entityId, TransformComp);
const move = session.world.getComponent(entityId, MovementComp);
if (!tf || !move) return node.worldPosition.clone();
return logicalWorldToCocos(session, this._map, tf.row, tf.col, move.worldX, move.worldY);
const cocos = logicalWorldToCocos(session, this._map, tf.row, tf.col, move.worldX, move.worldY);
if (!cocos) {
console.error('[CocosBattleMonsterPresenter] 机制停位坐标解析失败', entityId, tf.row, tf.col);
return node.worldPosition.clone();
}
return cocos;
}
/** 飞禽:芭蕉扇/复活收回落地格,而不是出怪口或当前格 */
@ -1251,6 +1293,13 @@ export class CocosBattleMonsterPresenter {
if (landRow < 0) {
const spawnGrid = map.getMapGrid(spawnRow, spawnCol);
if (!spawnGrid?.node?.isValid) {
console.error(
'[CocosBattleMonsterPresenter] 跳出无可用起落点',
entityId,
spawnRow,
spawnCol,
next,
);
this._recoverSpawnPortalDelayFailed(entityId, node);
onLand?.();
return;
@ -1259,6 +1308,51 @@ export class CocosBattleMonsterPresenter {
end.set(start);
}
if (!Number.isFinite(start.x) || !Number.isFinite(start.y) || !Number.isFinite(end.x) || !Number.isFinite(end.y)) {
console.error(
'[CocosBattleMonsterPresenter] 跳出坐标非法',
entityId,
start.x,
start.y,
end.x,
end.y,
spawnRow,
spawnCol,
);
this._recoverSpawnPortalDelayFailed(entityId, node);
onLand?.();
return;
}
// 禁止落到画布原点:格节点缺失时曾写出 (0,0),金币会从左下角飞出
if (Math.abs(start.x) < 1 && Math.abs(start.y) < 1) {
console.error(
'[CocosBattleMonsterPresenter] 跳出起点接近原点,中止',
entityId,
start.x,
start.y,
spawnRow,
spawnCol,
);
this._recoverSpawnPortalDelayFailed(entityId, node);
onLand?.();
return;
}
if (Math.abs(end.x) < 1 && Math.abs(end.y) < 1) {
console.error(
'[CocosBattleMonsterPresenter] 跳出落点接近原点,改为起点落地',
entityId,
end.x,
end.y,
spawnRow,
spawnCol,
landRow,
landCol,
);
end.set(start);
landRow = -1;
landCol = -1;
}
const spanX = Math.abs(end.x - start.x);
const extraLift = spanX > cellSize * 1.5 ? Math.min(spanX * 0.25, cellSize * 1.5) : 0;
const peakY = Math.max(start.y, end.y) + cellSize * 0.7 + extraLift;
@ -1548,6 +1642,11 @@ export class CocosBattleMonsterPresenter {
entityId?: EntityId,
spawnPort?: number,
): void {
// 五行新怪 / 新 Boss:仅 default 皮,不 setSkin、不区分阵营
if (MonsterDataManager.skipsBattleSpineSkin(monsterData)) {
monster.applyMarchSpineCache?.();
return;
}
const anim = monster.anim;
if (!anim?.isValid) return;
const override = this._resolveSpineSkinOverride(entityId);
@ -1559,7 +1658,7 @@ export class CocosBattleMonsterPresenter {
const enemyCamp = this._isEnemyCampMonster(entityId, spawnPort);
const preferred = MonsterDataManager.resolveBattleSpineSkin(monsterData, enemyCamp);
if (!preferred) {
console.log('[CocosBattleMonsterPresenter] 阵营皮肤名为空', monsterData.id, 'port=', spawnPort);
console.error('[CocosBattleMonsterPresenter] 阵营皮肤名为空', monsterData.id, 'port=', spawnPort);
return;
}
this._setSkeletonCampSkin(anim, preferred, monsterData.id, spawnPort, enemyCamp);
@ -1642,7 +1741,7 @@ export class CocosBattleMonsterPresenter {
}
}
(anim as sp.Skeleton & { defaultSkin: string }).defaultSkin = originalDefault;
console.log(
console.error(
'[CocosBattleMonsterPresenter] 阵营皮肤全部失败',
preferred,
'id=',
@ -1661,6 +1760,7 @@ export class CocosBattleMonsterPresenter {
entityId?: EntityId,
spawnPort?: number,
): void {
if (MonsterDataManager.skipsBattleSpineSkin(monsterData)) return;
const override = this._resolveSpineSkinOverride(entityId);
if (override) {
this._setSkeletonCampSkin(anim, override, monsterData.id, spawnPort, this._isEnemyCampMonster(entityId, spawnPort));
@ -1668,7 +1768,10 @@ export class CocosBattleMonsterPresenter {
}
const enemyCamp = this._isEnemyCampMonster(entityId, spawnPort);
const preferred = MonsterDataManager.resolveBattleSpineSkin(monsterData, enemyCamp);
if (!preferred) return;
if (!preferred) {
console.error('[CocosBattleMonsterPresenter] 阵营皮肤名为空', monsterData.id, 'port=', spawnPort);
return;
}
this._setSkeletonCampSkin(anim, preferred, monsterData.id, spawnPort, enemyCamp);
}
@ -2222,12 +2325,12 @@ function alignMxEntityToMapGrid(
): void {
const cell = session.topology.getSpawnCell(spawnPort);
if (!cell) {
console.log('[CocosBattleMonsterPresenter] alignMxEntity: 出怪口无拓扑格', spawnPort);
console.error('[CocosBattleMonsterPresenter] alignMxEntity: 出怪口无拓扑格', spawnPort);
return;
}
const grid = map.getMapGrid(cell.row, cell.col);
if (!grid) {
console.log('[CocosBattleMonsterPresenter] alignMxEntity: 地图格缺失', cell.row, cell.col);
console.error('[CocosBattleMonsterPresenter] alignMxEntity: 地图格缺失', cell.row, cell.col);
return;
}
const tf = session.world.getComponent(entityId, TransformComp);
@ -2239,4 +2342,14 @@ function alignMxEntityToMapGrid(
move.worldX = w.x;
move.worldY = w.y;
session.movement.assignPath(entityId);
const after = session.world.getComponent(entityId, MovementComp);
if (after && after.reachedGoal && after.hasAttackGoal && !(after.path?.length > 0)) {
console.error(
'[CocosBattleMonsterPresenter] 出怪后空路径且已到攻击点,将漏怪 despawn',
entityId,
spawnPort,
grid.Row,
grid.Col,
);
}
}

@ -26,6 +26,11 @@ export interface IBattleMonsterView extends Component {
playParatroopLand?(): void;
setData(data: ITableMonster, hp: number): void;
playWalk(speed?: number): void;
/**
* 播契约死亡轨(五行=`{系}小怪2`);无契约返回 false。
* @param onComplete 播完
*/
playDeath?(onComplete?: () => void): boolean;
/** 绑定 MapManager 后计算寻路目标(须在 MapGridsReady 之后) */
bindPathTarget(): void;
/** 同步 MX 层 HP(受击/治疗后由表现层调用,刷新血条) */

@ -7,6 +7,7 @@ import { GBundle, GPath } from '../../game/ConfigRes';
import { BATTLE_HERO_BUFF_PATH, BATTLE_HERO_NUM_PATH } from '../res/BattleHeroResCollector';
import { TableNames } from '../../game/ConfigTableData';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { WEAPON_LEFT_OFFSET_PX } from './HeroWeaponMount';
import { PEACH_TREE_WEAPON2_ID } from '../bag/BagPrepConfig';
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
import { textIconName } from '../config/BattleHeroTable';
@ -189,10 +190,10 @@ export class TextGrid extends Component {
private _aiMapLite = false;
/**
* 武器本地 X:索引 = 显示等级 - 1。
* 武器本地 X(须为负=靠左):索引 = 显示等级 - 1。
* 数字:1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192
*/
wPosX = [0, 0, 0, 0, -18, 18, 18, -24, -24, -24, -30, -30, -30, -12];
wPosX = [-29, -29, -29, -29, -18, -18, -18, -24, -24, -24, -30, -30, -30, -12];
protected onLoad(): void {
this._skinNode = this.node.getChildByName('skin');
@ -1817,13 +1818,22 @@ export class TextGrid extends Component {
}
/**
* 武器本地 X(wPosX[等级-1])。
* 武器本地 X(wPosX[等级-1],须靠左为负)。
*/
weaponLocalX(): number {
const fallback = -WEAPON_LEFT_OFFSET_PX;
const arr = this.wPosX;
if (!arr?.length) return 0;
if (!arr?.length) {
console.error('[TextGrid] wPosX 未配置', this.weapon2Id, this.level);
return fallback;
}
const idx = Math.max(0, Math.min(Math.floor(this.level) - 1, arr.length - 1));
return Number(arr[idx]) || 0;
const x = Number(arr[idx]);
if (!Number.isFinite(x) || x >= 0) {
console.error('[TextGrid] wPosX 须为负(靠左)', this.weapon2Id, this.level, arr[idx]);
return fallback;
}
return x;
}
/** 把数字 Spine / 武器 X 同步到 HeroActor 视觉通道 */

@ -26,6 +26,24 @@ export enum MonsterType {
TreasureBox = 8,
}
/** icon「小怪金」→ 行进「金小怪」 */
const ELEMENT_MARCH_BY_ICON: Readonly<Record<string, string>> = {
小怪金: '金小怪',
小怪木: '木小怪',
小怪水: '水小怪',
小怪火: '火小怪',
小怪土: '土小怪',
};
/** 新 Boss 预制体 icon(仅 default 皮,行进=走路) */
const NEW_BOSS_ICONS: Readonly<Record<string, boolean>> = {
Boss金: true,
Boss木: true,
Boss水: true,
Boss火: true,
Boss土: true,
};
/** 怪物表数据管理 */
export class MonsterDataManager {
/** 获取怪物配表列表 */
@ -43,17 +61,84 @@ export class MonsterDataManager {
}
/**
* 战斗 Spine 皮肤名(按出怪阵营)
* - type1 小怪 / type3 召唤:己方口=`icon`,敌方口=`icon`+`2`(如 哮天犬 / 哮天犬2)
* - type2 Boss:己方口=`皮肤字1_1`,敌方口=`皮肤字2_1`(资源无「皮肤字_」中间下划线)
* 是否五行新小怪(不设皮肤、不区分阵营;行进/死亡用系小怪轨)。
* @param row Monster 行
*/
static isElementSmallMonster(row: ITableMonster | null | undefined): boolean {
if (!row || row.type !== MonsterType.SmallMonster) return false;
const icon = String(row.icon ?? '').trim();
return !!ELEMENT_MARCH_BY_ICON[icon];
}
/**
* 是否新 Boss(不设皮肤、不区分阵营;行进=走路)。
* @param row Monster 行
*/
static isNewElementBoss(row: ITableMonster | null | undefined): boolean {
if (!row || !this.isBossType(row.type)) return false;
const icon = String(row.icon ?? '').trim();
return !!NEW_BOSS_ICONS[icon];
}
/**
* 新怪表现:不 setSkin。
* @param row Monster 行
*/
static skipsBattleSpineSkin(row: ITableMonster | null | undefined): boolean {
return this.isElementSmallMonster(row) || this.isNewElementBoss(row);
}
/**
* 行进动画名。五行小怪=`{系}小怪`;新 Boss=`走路`;其它=`走路`。
* @param row Monster 行
*/
static resolveMarchAnim(row: ITableMonster | null | undefined): string {
if (!row) {
console.error('[MonsterDataManager] resolveMarchAnim 无配表');
return '';
}
if (this.isElementSmallMonster(row)) {
const icon = String(row.icon ?? '').trim();
const anim = ELEMENT_MARCH_BY_ICON[icon];
if (!anim) {
console.error('[MonsterDataManager] 五行小怪 icon 无法映射行进动画', row.id, icon);
return '';
}
return anim;
}
return '走路';
}
/**
* 死亡动画名。五行小怪=`{系}小怪2`;无契约轨返回空(走外部死亡特效)。
* @param row Monster 行
*/
static resolveDeathAnim(row: ITableMonster | null | undefined): string {
if (!row) {
console.error('[MonsterDataManager] resolveDeathAnim 无配表');
return '';
}
if (!this.isElementSmallMonster(row)) return '';
const march = this.resolveMarchAnim(row);
if (!march) return '';
return `${march}2`;
}
/**
* @deprecated 新五行怪不设皮肤;仅旧多皮资源(猴/哮天犬等)仍用。
* - type1 小怪 / type3 召唤:己方口=`icon`,敌方口=`icon`+`2`
* - type2 Boss:己方口=`皮肤字1_1`,敌方口=`皮肤字2_1`
* @param row Monster 行
* @param enemyCamp 是否敌方出怪口放出的怪
*/
public static resolveBattleSpineSkin(row: ITableMonster, enemyCamp: boolean): string {
if (this.skipsBattleSpineSkin(row)) {
return '';
}
const icon = String(row.icon ?? '').trim();
if (row.type === MonsterType.SmallMonster || row.type === MonsterType.SummonMonster) {
if (!icon) {
console.log('[MonsterDataManager] 怪物 icon 为空,无法定皮肤', row.id, row.type);
console.error('[MonsterDataManager] 怪物 icon 为空,无法定皮肤', row.id, row.type);
return '';
}
return enemyCamp ? `${icon}2` : icon;
@ -62,7 +147,7 @@ export class MonsterDataManager {
return enemyCamp ? '皮肤字2_1' : '皮肤字1_1';
}
if (!icon) {
console.log('[MonsterDataManager] 怪物 icon 为空', row.id, row.type);
console.error('[MonsterDataManager] 怪物 icon 为空', row.id, row.type);
return '';
}
return icon;
@ -107,5 +192,3 @@ export class MonsterDataManager {
return calcMonsterHpBaseByBattleTime(diff, time, power, powerPara, powerPara2);
}
}

@ -171,15 +171,20 @@ export class UIBattleBoss extends Auto_BattleBoss {
const anim = skin.getChildByName('anim')?.getComponent(sp.Skeleton) ?? null;
if (!anim) {
console.log('[UIBattleBoss] 怪物 Spine 缺失', prefabName, row.id);
console.error('[UIBattleBoss] 怪物 Spine 缺失', prefabName, row.id);
} else {
const skinName = MonsterDataManager.resolveBattleSpineSkin(row, true);
if (!skinName) {
console.log('[UIBattleBoss] Boss 皮肤名为空', row.id);
} else {
anim.setSkin(skinName);
if (!MonsterDataManager.skipsBattleSpineSkin(row)) {
const skinName = MonsterDataManager.resolveBattleSpineSkin(row, true);
if (!skinName) {
console.error('[UIBattleBoss] Boss 皮肤名为空', row.id);
} else {
anim.setSkin(skinName);
}
}
const march = MonsterDataManager.resolveMarchAnim(row) || '走路';
if (!BattleSpinePlayback.play(anim, march, { loop: true })) {
console.error('[UIBattleBoss] Boss 行进动画起播失败', row.id, march);
}
BattleSpinePlayback.play(anim, '待机', { loop: true });
}
this._bossSkinNode = skin;

@ -440,6 +440,10 @@ export class BattleGameMenShenTrapController {
const cocos = logicalWorldToCocos(
session, map, anchor.row, anchor.col, trap.sweepLogicalX, trap.sweepLogicalY,
);
if (!cocos) {
console.error('[MenShenTrap] 门神坐标解析失败', anchor.row, anchor.col);
return;
}
node.setWorldPosition(cocos);
}
@ -472,6 +476,7 @@ export class BattleGameMenShenTrapController {
const pathTouch = monsterIdx >= doorPathIdx - 1 && monsterIdx <= doorPathIdx;
if (!pathTouch) {
const monsterWorld = logicalWorldToCocos(session, map, tf.row, tf.col, move.worldX, move.worldY);
if (!monsterWorld) return;
const dx = monsterWorld.x - this._tmpWorldA.x;
const dy = monsterWorld.y - this._tmpWorldA.y;
if (dx * dx + dy * dy > catchSq) return;

@ -3,7 +3,7 @@ import type { EntityId } from 'db://assets/mx/module/battle';
import { UnitMetaComp } from 'db://assets/mx/module/battle/ecs/Components';
import { GEvent } from 'db://assets/mx/module/event/GEvent';
import { GBundle, GPath } from '../../game/ConfigRes';
import { MonsterType } from '../../manager/MonsterDataManager';
import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager';
import { HitEffectResolver, DEFAULT_HIT_EFFECT } from '../../battle/view/HitEffectResolver';
import { playHitLeanBack } from '../../battle/view/HitLeanBack';
import { playSpineHitFlash } from '../../battle/view/SpineHitFlash';
@ -159,7 +159,7 @@ export class Monster extends Component {
/** 战斗 TimeScale 变化时同步 Spine 播放倍速 */
private _syncAnimBattleTimeScale(): void {
if (!this.anim?.isValid || this.anim.animation === '死亡') return;
if (!this.anim?.isValid || this._isDeathAnimPlaying()) return;
const battleScale = gg.game.CurentBattle?.TimeScale ?? 1;
const want = this._animBaseTimeScale * battleScale;
if (Math.abs(this.anim.timeScale - want) > 1e-4) {
@ -167,6 +167,16 @@ export class Monster extends Component {
}
}
/** 当前是否在播死亡轨(旧「死亡」或五行「系小怪2」) */
private _isDeathAnimPlaying(): boolean {
if (this.IsDead) return true;
if (!this.anim?.isValid) return false;
const cur = String(this.anim.animation ?? '');
if (cur === '死亡') return true;
const death = MonsterDataManager.resolveDeathAnim(this.Data);
return !!death && cur === death;
}
/** 当前皮肤预制体名(icon / name) */
private _resolveSkinPrefabName(): string {
if (!this.Data) return '';
@ -179,7 +189,7 @@ export class Monster extends Component {
this._lastAttackTime = 0;
this.state = MonsterState.STOP;
if (this.Data?.type === MonsterType.TreasureBox) return;
this.playSpine('待机');
this.playWalk();
}
/** 复活后回到出生点并隐藏,等按出怪间隔跳出再放行 */
@ -192,7 +202,7 @@ export class Monster extends Component {
this._reviveHeld = true;
this._lastAttackTime = 0;
if (this.Data?.type !== MonsterType.TreasureBox) {
this.playSpine('待机');
this.playWalk();
}
}
@ -1260,7 +1270,54 @@ export class Monster extends Component {
playWalk(speed = 1) {
if (!this.Data || this.Data.type == MonsterType.TreasureBox) return;
this.playSpine('走路', null, speed);
if (this._isDeathAnimPlaying()) return;
const anim = MonsterDataManager.resolveMarchAnim(this.Data);
if (!anim) {
console.error('[Monster] 行进动画名为空', this.Data.id, this.Data.icon);
return;
}
this.playSpine(anim, null, speed);
}
/**
* 播契约死亡轨(五行=`{系}小怪2`);无契约轨则失败。
* @param onComplete 播完
* @returns 是否成功起播
*/
playDeath(onComplete?: () => void): boolean {
if (!this.Data || this.Data.type === MonsterType.TreasureBox) {
onComplete?.();
return false;
}
const anim = MonsterDataManager.resolveDeathAnim(this.Data);
if (!anim) {
onComplete?.();
return false;
}
this.IsDead = true;
this.state = MonsterState.STOP;
if (!this.anim?.isValid) {
console.error('[Monster] 死亡无 Skeleton', this.Data.id, anim);
onComplete?.();
return false;
}
if (!this.anim.findAnimation(anim)) {
console.error('[Monster] 死亡动画不存在', this.Data.id, anim, this.Data.icon);
onComplete?.();
return false;
}
this._animBaseTimeScale = 1;
const ok = BattleSpinePlayback.play(this.anim, anim, {
loop: false,
timeScale: 1,
onComplete: onComplete ?? undefined,
});
if (!ok) {
console.error('[Monster] 死亡动画起播失败', this.Data.id, anim);
onComplete?.();
return false;
}
return true;
}
/** 空投飞禽降落:播 Spine「降落」(隼 / 鹰) */
@ -1282,28 +1339,20 @@ export class Monster extends Component {
if (!this.node || !this.anim) {
return;
}
if (this.anim.animation == '死亡') return;
if (this._isDeathAnimPlaying()) return;
this._animBaseTimeScale = timeScale;
const animName = this._resolveSpineAnimName(name);
BattleSpinePlayback.play(this.anim, animName, {
if (!this.anim.findAnimation(name)) {
console.error('[Monster] 动画不存在', this.Data?.id, name, this.Data?.icon);
callback?.();
return;
}
BattleSpinePlayback.play(this.anim, name, {
loop,
timeScale,
optional: animName === '待机',
onComplete: callback ? () => callback() : undefined,
});
}
/** 动画不存在时回退 defaultAnimation,避免 Spine 报错且模型不显示 */
private _resolveSpineAnimName(name: string): string {
if (!this.anim) return name;
if (this.anim.findAnimation(name)) return name;
const fallback = '待机';
if (fallback !== name && this.anim.findAnimation(fallback)) {
return fallback;
}
return name;
}
clear() {
this.unscheduleAllCallbacks();
GEvent.Ins.targetOff(this);

@ -24,7 +24,7 @@ import { BattleAudio } from '../../battle/audio/BattleAudio';
import { BattleUiPauseGate } from '../../battle/BattleUiPauseGate';
import { SkillController } from '../../battle/skill/SkillController';
import { SkillPresentationEnv } from '../../battle/skill/SkillPresentationEnv';
import { MonsterType } from '../../manager/MonsterDataManager';
import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager';
import { GEvent } from '../../../mx/module/event/GEvent';
import { BattleIllustratedGuideManager } from '../../manager/BattleIllustratedGuideManager';
import { UIID } from '../../game/ConfigRes';
@ -523,15 +523,16 @@ export class UIBattleGame extends Auto_BattleGame {
return this._monsterPresenter.getEntityWorldPos(victim);
}
/** 击杀时在怪物坐标播 prefab/死亡(优先用节点位,否则用缓存世界坐标) */
/** 击杀时在怪物坐标播 prefab/死亡;五行小怪走自身「系小怪2」轨,不播外部死亡特效 */
private _playMonsterDeathFx(victim: EntityId): void {
const parent = this._deathFxParent;
if (!parent?.isValid) return;
const node = this._monsterPresenter.getEntityNodeMap().get(victim);
if (node?.isValid) {
const view = node.getComponent(BATTLE_MONSTER_CCCLASS) as { Data?: { type?: number } } | null;
const view = node.getComponent(BATTLE_MONSTER_CCCLASS) as { Data?: ITableMonster } | null;
if (view?.Data?.type === MonsterType.TreasureBox) return;
if (view?.Data && MonsterDataManager.isElementSmallMonster(view.Data)) return;
}
const worldPos = this._monsterDeathWorldPos(victim);
if (!worldPos) return;

@ -22,7 +22,7 @@
waveIndex,monsterId,count,spawnInterval
```
`|` 分隔 25 波。例:`1,101,10,1.8`
`|` 分隔 25 波。例:`1,1001,10,1.6`
- 第 4 列:该波每只怪出现间隔(秒)
- **Monster1 行 id = waveIndex**(不是第 4 列)
@ -32,7 +32,7 @@ waveIndex,monsterId,count,spawnInterval
waveIndex|monsterId,...
```
例:`3|105,6|106` → 第 3 波排队出 105,第 6 波排队出 106(在小怪段之后)。
例:`5|1101,10|1101` → 第 5、10 波排队出 Boss 1101(在小怪段之后)。
### Level.bosspro(万分比)
@ -49,15 +49,18 @@ waveIndex|monsterId,...
- **Monster1**:行 = `waveIndex`;列 = `Rank.diffcult` / Level 难度组。
- **血量计算时机**:**出怪当下**套公式 `floor(Monster.hp × hp_multiple × port_hp × Monster1[波次] × 战力修正 × weapon_para)`,不以开局缓存为准。波次怪、营召唤、`atk_type=8`(含哮天犬)、万神幡 Boss 同一套;Monster1 行取**当前波** `waveIndex`。
#### 表现皮肤(出怪口阵营)
#### 表现(五行新怪:不设皮肤、不区分阵营)
| type | 预制体 | 己方口 port1(攻 AI) | 敌方口 port2(攻本机) |
|------|--------|----------------------|------------------------|
| 1 小怪 | `prefab/monster/{icon}` | Spine `{icon}`(如「猴」) | Spine `{icon}2`(如「猴2」) |
| 2 Boss | `prefab/monster/{icon}` | `皮肤字1_1` | `皮肤字2_1` |
| 3 召唤 | `prefab/monster/{icon}` | Spine `{icon}`(如「哮天犬」) | Spine `{icon}2`(如「哮天犬2」) |
Spine 资源仅 `default` 皮;双口同一种表现。
实现:`MonsterDataManager.resolveBattleSpineSkin` + `CocosBattleMonsterPresenter._applyMonsterSpineSkin`(优先 `spawnPort===2` 判敌方)。
| type | 预制体 | 皮肤 | 行进 | 死亡 |
|------|--------|------|------|------|
| 1 五行小怪 | `prefab/monster/{icon}`(如 `小怪金`) | **不 setSkin** | `{系}小怪`(如 `金小怪`) | `{系}小怪2`(如 `金小怪2`) |
| 2 新 Boss | `prefab/monster/{icon}`(如 `Boss金`) | **不 setSkin** | `走路` | 按 Boss 资源轨(缺则 error) |
`Monster.icon` → 行进 / 死亡:`小怪金`→`金小怪`/`金小怪2`,木/水/火/土同理。映射失败或动画不存在 → `console.error`,禁止静默兜底。
实现:`MonsterDataManager.resolveElementMarchAnim` / `resolveElementDeathAnim` + `Monster.playWalk` / 死亡播控;Presenter 对新怪跳过换皮。
#### 营字强化出怪(Hero.id=5)

@ -49,8 +49,8 @@ PlayPresentation cue
| 通道 | 条件 | 职责 |
|------|------|------|
| 字形 | 所有英雄 | 播「攻击」 |
| 武器 | 组合英雄且有 weaponId | 激活后常驻挂 `weaponType*`(首字格中心偏左);攻击跟播 `Skill.animation`(缺省「攻击」);**字形与武器都播完才 finish**。弓:藏 `weaponType4`,显 `武器弓-攻击` 播「攻击」,拉满发射「投掷物」;`拉弓动画` 与 `投掷物/anim` 皮肤=当前 `Weapon.skin`。玉帝飞剑:不播原武器攻击,直接隐藏,飞剑播预制体默认「玉帝普攻」。**主动技能(hero/weapon)动画倍速固定 1;普攻跟攻速**。悟空主动 `Skill.animation=悟空技能`:武器不瞄准,沿组合两字包围盒中心椭圆绕一圈(长半径=挂点 X 到中心,短半径=半格,时长=该动画时长-0.2s) |
| 字形 | 组合英雄 | 播「攻击」;**五行数字不播字形「攻击」**(保持待机皮) |
| 武器 | 组合英雄且有 weaponId;五行数字按 `Hero.herotype` | 激活后常驻挂 `weaponType*`(格子左侧 / 首字格中心偏左),**不切换显隐**;攻击跟播 `Skill.animation`(缺省「攻击」);组合英雄 **字形与武器都播完才 finish**,五行仅等武器。组合弓:藏 `weaponType4`,显 `武器弓-攻击` 播「攻击」,拉满发射「投掷物」;`拉弓动画` 与 `投掷物/anim` 皮肤=当前 `Weapon.skin`。五行弓:常驻 `weaponType4` 播配表动画。玉帝飞剑:不播原武器攻击,直接隐藏,飞剑播预制体默认「玉帝普攻」。**主动技能(hero/weapon)动画倍速固定 1;普攻跟攻速**。悟空主动 `Skill.animation=悟空技能`:武器不瞄准,沿组合两字包围盒中心椭圆绕一圈(长半径=挂点 X 到中心,短半径=半格,时长=该动画时长-0.2s) |
### 4.3 atk_type 通用

Loading…
Cancel
Save