From 35ffa6281bafeb961e67e363117cfb2d44f8821d Mon Sep 17 00:00:00 2001 From: humingxing <930621236@qq.com> Date: Sat, 10 Oct 2026 15:40:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=87=BA=E6=80=AA=E9=80=BB?= =?UTF-8?q?=E8=BE=91bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bundles/comm/prefab/PopCoinToast.prefab | 432 ++++-------------- assets/bundles/first/config/Const.json | 4 +- assets/bundles/first/config/Monster.json | 414 ++--------------- assets/mx/module/battle/unit/WaveSpawner.ts | 7 +- .../bridge/CocosBattleMovementBridge.ts | 79 ++-- .../script/battle/combat/MonsterSkillHost.ts | 11 +- .../config/TableBattleConfigProvider.ts | 6 - assets/script/battle/heros/HeroActor.ts | 12 +- .../battle/presentation/SkillVisualChannel.ts | 137 ++---- .../presentation/ThrownCircleAtkPresenter.ts | 1 + .../view/CocosBattleMonsterPresenter.ts | 129 +++++- .../script/battle/view/IBattleMonsterView.ts | 5 + assets/script/battle/view/TextGrid.ts | 20 +- assets/script/manager/MonsterDataManager.ts | 97 +++- assets/script/ui/BattleBoss/UIBattleBoss.ts | 19 +- .../BattleGameMenShenTrapController.ts | 5 + assets/script/ui/BattleGame/Monster.ts | 89 +++- assets/script/ui/BattleGame/UIBattleGame.ts | 7 +- docs/方案-出怪逻辑实现.md | 21 +- docs/方案-战斗表现数据驱动.md | 4 +- 20 files changed, 547 insertions(+), 952 deletions(-) diff --git a/assets/bundles/comm/prefab/PopCoinToast.prefab b/assets/bundles/comm/prefab/PopCoinToast.prefab index b3316a9..995d951 100644 --- a/assets/bundles/comm/prefab/PopCoinToast.prefab +++ b/assets/bundles/comm/prefab/PopCoinToast.prefab @@ -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": "" }, diff --git a/assets/bundles/first/config/Const.json b/assets/bundles/first/config/Const.json index 545a357..4d1738e 100644 --- a/assets/bundles/first/config/Const.json +++ b/assets/bundles/first/config/Const.json @@ -20,13 +20,13 @@ { "id": 4, "desc": "战斗初始刷新消耗", - "para": 10, + "para": 50, "para1": "" }, { "id": 5, "desc": "战斗刷新消耗递增值", - "para": 2, + "para": 0, "para1": "" }, { diff --git a/assets/bundles/first/config/Monster.json b/assets/bundles/first/config/Monster.json index da18675..648d29f 100644 --- a/assets/bundles/first/config/Monster.json +++ b/assets/bundles/first/config/Monster.json @@ -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 } ] \ No newline at end of file diff --git a/assets/mx/module/battle/unit/WaveSpawner.ts b/assets/mx/module/battle/unit/WaveSpawner.ts index 569104d..637ec91 100644 --- a/assets/mx/module/battle/unit/WaveSpawner.ts +++ b/assets/mx/module/battle/unit/WaveSpawner.ts @@ -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; } } diff --git a/assets/script/battle/bridge/CocosBattleMovementBridge.ts b/assets/script/battle/bridge/CocosBattleMovementBridge.ts index 2bc1e48..e2aacfb 100644 --- a/assets/script/battle/bridge/CocosBattleMovementBridge.ts +++ b/assets/script/battle/bridge/CocosBattleMovementBridge.ts @@ -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; } diff --git a/assets/script/battle/combat/MonsterSkillHost.ts b/assets/script/battle/combat/MonsterSkillHost.ts index 9badac5..96fee58 100644 --- a/assets/script/battle/combat/MonsterSkillHost.ts +++ b/assets/script/battle/combat/MonsterSkillHost.ts @@ -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); diff --git a/assets/script/battle/config/TableBattleConfigProvider.ts b/assets/script/battle/config/TableBattleConfigProvider.ts index 3aff90a..d6f7435 100644 --- a/assets/script/battle/config/TableBattleConfigProvider.ts +++ b/assets/script/battle/config/TableBattleConfigProvider.ts @@ -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'); - } - } } diff --git a/assets/script/battle/heros/HeroActor.ts b/assets/script/battle/heros/HeroActor.ts index 92122ca..38e5908 100644 --- a/assets/script/battle/heros/HeroActor.ts +++ b/assets/script/battle/heros/HeroActor.ts @@ -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++; diff --git a/assets/script/battle/presentation/SkillVisualChannel.ts b/assets/script/battle/presentation/SkillVisualChannel.ts index 3540dd8..bd114ce 100644 --- a/assets/script/battle/presentation/SkillVisualChannel.ts +++ b/assets/script/battle/presentation/SkillVisualChannel.ts @@ -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(); diff --git a/assets/script/battle/presentation/ThrownCircleAtkPresenter.ts b/assets/script/battle/presentation/ThrownCircleAtkPresenter.ts index 93de389..4afe0c4 100644 --- a/assets/script/battle/presentation/ThrownCircleAtkPresenter.ts +++ b/assets/script/battle/presentation/ThrownCircleAtkPresenter.ts @@ -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; } diff --git a/assets/script/battle/view/CocosBattleMonsterPresenter.ts b/assets/script/battle/view/CocosBattleMonsterPresenter.ts index 57f16b8..ec0a7af 100644 --- a/assets/script/battle/view/CocosBattleMonsterPresenter.ts +++ b/assets/script/battle/view/CocosBattleMonsterPresenter.ts @@ -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, + ); + } } diff --git a/assets/script/battle/view/IBattleMonsterView.ts b/assets/script/battle/view/IBattleMonsterView.ts index 67cd482..457cc44 100644 --- a/assets/script/battle/view/IBattleMonsterView.ts +++ b/assets/script/battle/view/IBattleMonsterView.ts @@ -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(受击/治疗后由表现层调用,刷新血条) */ diff --git a/assets/script/battle/view/TextGrid.ts b/assets/script/battle/view/TextGrid.ts index c6e9270..449a07e 100644 --- a/assets/script/battle/view/TextGrid.ts +++ b/assets/script/battle/view/TextGrid.ts @@ -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 视觉通道 */ diff --git a/assets/script/manager/MonsterDataManager.ts b/assets/script/manager/MonsterDataManager.ts index 77c73b9..e00a38b 100644 --- a/assets/script/manager/MonsterDataManager.ts +++ b/assets/script/manager/MonsterDataManager.ts @@ -26,6 +26,24 @@ export enum MonsterType { TreasureBox = 8, } +/** icon「小怪金」→ 行进「金小怪」 */ +const ELEMENT_MARCH_BY_ICON: Readonly> = { + 小怪金: '金小怪', + 小怪木: '木小怪', + 小怪水: '水小怪', + 小怪火: '火小怪', + 小怪土: '土小怪', +}; + +/** 新 Boss 预制体 icon(仅 default 皮,行进=走路) */ +const NEW_BOSS_ICONS: Readonly> = { + 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); } } - - diff --git a/assets/script/ui/BattleBoss/UIBattleBoss.ts b/assets/script/ui/BattleBoss/UIBattleBoss.ts index a97bba8..bd2d87d 100644 --- a/assets/script/ui/BattleBoss/UIBattleBoss.ts +++ b/assets/script/ui/BattleBoss/UIBattleBoss.ts @@ -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; diff --git a/assets/script/ui/BattleGame/BattleGameMenShenTrapController.ts b/assets/script/ui/BattleGame/BattleGameMenShenTrapController.ts index 57dfd35..b67aa31 100644 --- a/assets/script/ui/BattleGame/BattleGameMenShenTrapController.ts +++ b/assets/script/ui/BattleGame/BattleGameMenShenTrapController.ts @@ -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; diff --git a/assets/script/ui/BattleGame/Monster.ts b/assets/script/ui/BattleGame/Monster.ts index a226ada..2566481 100644 --- a/assets/script/ui/BattleGame/Monster.ts +++ b/assets/script/ui/BattleGame/Monster.ts @@ -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); diff --git a/assets/script/ui/BattleGame/UIBattleGame.ts b/assets/script/ui/BattleGame/UIBattleGame.ts index 1629fd0..963946a 100644 --- a/assets/script/ui/BattleGame/UIBattleGame.ts +++ b/assets/script/ui/BattleGame/UIBattleGame.ts @@ -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; diff --git a/docs/方案-出怪逻辑实现.md b/docs/方案-出怪逻辑实现.md index 6b6ec93..0955ddc 100644 --- a/docs/方案-出怪逻辑实现.md +++ b/docs/方案-出怪逻辑实现.md @@ -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) diff --git a/docs/方案-战斗表现数据驱动.md b/docs/方案-战斗表现数据驱动.md index c7e180c..e4dcca0 100644 --- a/docs/方案-战斗表现数据驱动.md +++ b/docs/方案-战斗表现数据驱动.md @@ -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 通用