master
humingxing 2 weeks ago
parent e62449c94a
commit d69c1a4f0d
  1. 1211
      assets/bundles/battleGame/prefab/BattleGame.prefab
  2. 13
      assets/bundles/battleGame/prefab/格子锁.prefab
  3. 6
      assets/bundles/battleHero/prefab/skill/剑阵.prefab
  4. 239
      assets/bundles/battleHero/prefab/skill/水弹.prefab
  5. 13
      assets/bundles/battleHero/prefab/skill/水弹.prefab.meta
  6. 26
      assets/bundles/battleHero/prefab/skill/飞剑.prefab
  7. 2
      assets/bundles/battleHero/prefab/skill/飞剑.prefab.meta
  8. 16
      assets/bundles/battleHero/prefab/weapon/weaponType1.prefab
  9. 22
      assets/bundles/battleHero/prefab/weapon/weaponType2.prefab
  10. 14
      assets/bundles/battleHero/prefab/weapon/weaponType3.prefab
  11. 34
      assets/bundles/battleHero/prefab/weapon/weaponType4.prefab
  12. 354
      assets/bundles/battleHero/prefab/weapon/weaponType5.prefab
  13. 13
      assets/bundles/battleHero/prefab/weapon/weaponType5.prefab.meta
  14. 180
      assets/bundles/first/config/Skill.json
  15. 8
      assets/mx/module/merge/config/IComposeConfigProvider.ts
  16. 30
      assets/mx/module/merge/core/MergeSession.ts
  17. 4
      assets/script/battle/ai/bridge/AiBattleBridge.ts
  18. 10
      assets/script/battle/ai/runtime/AiMapTextHost.ts
  19. 11
      assets/script/battle/ai/runtime/AiSkillHost.ts
  20. 15
      assets/script/battle/bag/BattleBagSession.ts
  21. 30
      assets/script/battle/bag/WeaponComposeConfigProvider.ts
  22. 8
      assets/script/battle/combat/TowerLoadoutService.ts
  23. 18
      assets/script/battle/combat/TowerStatResolver.ts
  24. 19
      assets/script/battle/config/BattleHeroTable.ts
  25. 8
      assets/script/battle/config/LevelWaveParser.ts
  26. 6
      assets/script/battle/heros/HeroActor.ts
  27. 24
      assets/script/battle/map/legacy/MapAreaUnlock.ts
  28. 258
      assets/script/battle/presentation/SkillVisualChannel.ts
  29. 5
      assets/script/battle/res/BattleHeroResCollector.ts
  30. 16
      assets/script/battle/skill/SkillController.ts
  31. 129
      assets/script/battle/view/HeroWeaponMount.ts
  32. 5
      assets/script/battle/view/MapDragGuideHelper.ts
  33. 60
      assets/script/battle/view/TextGrid.ts
  34. 3
      assets/script/game/ResPkgBuilder.ts
  35. 39
      assets/script/ui/BattleGame/UIBattleGame.ts
  36. 2
      docs/方案-出怪逻辑实现.md
  37. BIN
      配置表/excelList/技能表.xlsx

File diff suppressed because it is too large Load Diff

@ -92,7 +92,7 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -17.1845703125,
"x": -16.1845703125,
"y": 0,
"z": 0
},
@ -228,7 +228,7 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 12.5,
"x": 13.5,
"y": 0,
"z": 0
},
@ -378,7 +378,7 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 82.369140625,
"width": 84.369140625,
"height": 68
},
"_anchorPoint": {
@ -457,7 +457,7 @@
"height": 40
},
"_startAxis": 0,
"_paddingLeft": 3,
"_paddingLeft": 5,
"_paddingRight": 10,
"_paddingTop": 0,
"_paddingBottom": 0,
@ -473,7 +473,7 @@
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "acA5MrJQNI66W+fTtjgwyu"
"fileId": "70ZyhmoIhBg4ERUGYZrVVb"
},
{
"__type__": "cc.PrefabInfo",
@ -484,7 +484,6 @@
"__id__": 0
},
"fileId": "a01BJWthhKbr3LbCXP7YXR",
"instance": null,
"targetOverrides": null
"instance": null
}
]

@ -164,11 +164,11 @@
"a": 255
},
"_skeletonData": {
"__uuid__": "b08da8a5-db6f-422d-8058-f2ae8c8dc279",
"__uuid__": "7b03a26d-8a9f-4312-a397-d414e1ef4574",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "轩辕剑",
"defaultAnimation": "诛仙剑技能",
"defaultSkin": "default",
"defaultAnimation": "剑阵",
"_premultipliedAlpha": true,
"_timeScale": 1,
"_preCacheMode": 0,

@ -0,0 +1,239 @@
[
{
"__type__": "cc.Prefab",
"_name": "水弹",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "水弹",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 8
}
],
"_prefab": {
"__id__": 10
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "飞行水弹",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
}
],
"_prefab": {
"__id__": 7
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 1,
"w": 6.123233995736766e-17
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 180
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 322,
"height": 122
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "fdzTh+tYZEhI11kKqjFBe7"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "d0abd767-3ed4-49ca-b7dd-e295c41cd319@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": "cfciGdRx1ORZesbsRtCqtj"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "41giWeE6BPxKcxypzVJSnI",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 9
},
"_contentSize": {
"__type__": "cc.Size",
"width": 200,
"height": 200
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "451RzMpQpFLalWlPKOafD2"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "38JEit66ZI+Y7iGQ+17T7W",
"instance": null,
"targetOverrides": null
}
]

@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "4148572e-42b7-48ae-9ef7-df2889a4e162",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "水弹"
}
}

@ -1,7 +1,7 @@
[
{
"__type__": "cc.Prefab",
"_name": "玉帝飞剑",
"_name": "飞剑",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
@ -13,7 +13,7 @@
},
{
"__type__": "cc.Node",
"_name": "玉帝飞剑",
"_name": "飞剑",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
@ -94,8 +94,8 @@
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 1,
"w": 6.123233995736766e-17
"z": -0.7071067811865475,
"w": 0.7071067811865476
},
"_lscale": {
"__type__": "cc.Vec3",
@ -109,7 +109,7 @@
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 180
"z": -90
},
"_id": ""
},
@ -127,13 +127,13 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 29.399999618530273,
"height": 52.79999923706055
"width": 38.47999954223633,
"height": 79.19999694824219
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.8789115812199337,
"y": 0.6850378540073432
"x": 0.47193347358227933,
"y": 0.3867424285481165
},
"_id": ""
},
@ -164,12 +164,12 @@
"a": 255
},
"_skeletonData": {
"__uuid__": "b08da8a5-db6f-422d-8058-f2ae8c8dc279",
"__uuid__": "dcce3833-3da9-44f4-ba04-72175d2ae9db",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "诛仙剑",
"defaultAnimation": "玉帝普攻",
"_premultipliedAlpha": true,
"defaultSkin": "default",
"defaultAnimation": "飞剑",
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 0,
"_cacheMode": 0,

@ -8,6 +8,6 @@
],
"subMetas": {},
"userData": {
"syncNodeName": "玉帝飞剑"
"syncNodeName": "飞剑"
}
}

@ -178,13 +178,13 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 29.399999618530273,
"height": 52.79999923706055
"width": 38.47999954223633,
"height": 79.19999694824219
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.8789115812199337,
"y": 0.6850378540073432
"x": 0.47193347358227933,
"y": 0.3867424285481165
},
"_id": ""
},
@ -215,12 +215,12 @@
"a": 255
},
"_skeletonData": {
"__uuid__": "b08da8a5-db6f-422d-8058-f2ae8c8dc279",
"__uuid__": "dcce3833-3da9-44f4-ba04-72175d2ae9db",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "诛仙剑",
"defaultAnimation": "攻击",
"_premultipliedAlpha": true,
"defaultSkin": "default",
"defaultAnimation": "待机",
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 0,
"_cacheMode": 0,

@ -146,7 +146,7 @@
"x": 0,
"y": 0,
"z": 1,
"w": -6.123233995736766e-17
"w": 6.123233995736766e-17
},
"_lscale": {
"__type__": "cc.Vec3",
@ -158,9 +158,9 @@
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": -180,
"y": -180,
"z": -7.016709298534876e-15
"x": 0,
"y": 0,
"z": 180
},
"_id": ""
},
@ -178,13 +178,13 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 58,
"height": 92
"width": 94.87000274658203,
"height": 108.30000305175781
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5594827717748182,
"y": 0.39749999668287195
"x": 0.48223883920619237,
"y": 0.39464451037956905
},
"_id": ""
},
@ -215,12 +215,12 @@
"a": 255
},
"_skeletonData": {
"__uuid__": "16b7dfbc-1936-4d60-a737-52963d4b85b9",
"__uuid__": "35c3ad81-2132-4358-8e17-a1063d1b12d7",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "木枪",
"defaultSkin": "default",
"defaultAnimation": "待机",
"_premultipliedAlpha": true,
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 0,
"_cacheMode": 0,

@ -178,13 +178,13 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 58.5,
"height": 91.5
"width": 36.45000076293945,
"height": 83.4800033569336
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.6201709193042201,
"y": 0.4168305944223873
"x": 0.49026064376390155,
"y": 0.39758022630640355
},
"_id": ""
},
@ -215,12 +215,12 @@
"a": 255
},
"_skeletonData": {
"__uuid__": "5537a63c-69d8-40db-ae9a-dec12ffb9d73",
"__uuid__": "c9a6d736-aeee-4865-a852-c4998d8827d1",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "木棍",
"defaultSkin": "default",
"defaultAnimation": "待机",
"_premultipliedAlpha": true,
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 0,
"_cacheMode": 0,

@ -181,13 +181,13 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 62.04999923706055,
"height": 63.150001525878906
"width": 75.98999786376953,
"height": 95.4800033569336
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.7932312945349005,
"y": 0.47474265744792593
"x": 0.8398473679290928,
"y": 0.48324254050518767
},
"_id": ""
},
@ -218,12 +218,12 @@
"a": 255
},
"_skeletonData": {
"__uuid__": "9a08eed3-aab7-42cf-8174-f8a06045dce3",
"__uuid__": "2cfd40d1-8308-45e0-9d48-fc9918263deb",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "木弓",
"defaultAnimation": "攻击",
"_premultipliedAlpha": true,
"defaultSkin": "default",
"defaultAnimation": "待机",
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 0,
"_cacheMode": 0,
@ -350,8 +350,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -8.486,
"y": 0,
"x": 0,
"y": 4.903,
"z": 0
},
"_lrot": {
@ -391,13 +391,13 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 62.04999923706055,
"height": 63.150001525878906
"width": 75.98999786376953,
"height": 95.4800033569336
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.7932312945349005,
"y": 0.47474265744792593
"x": 0.8398473679290928,
"y": 0.48324254050518767
},
"_id": ""
},
@ -428,12 +428,12 @@
"a": 255
},
"_skeletonData": {
"__uuid__": "9a08eed3-aab7-42cf-8174-f8a06045dce3",
"__uuid__": "2cfd40d1-8308-45e0-9d48-fc9918263deb",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "木弓",
"defaultSkin": "default",
"defaultAnimation": "飞行箭矢",
"_premultipliedAlpha": true,
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 0,
"_cacheMode": 0,

@ -0,0 +1,354 @@
[
{
"__type__": "cc.Prefab",
"_name": "weaponType5",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "weaponType5",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 14
}
],
"_prefab": {
"__id__": 16
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "weapon",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 3
}
],
"_active": true,
"_components": [
{
"__id__": 11
}
],
"_prefab": {
"__id__": 13
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 1,
"w": 6.123233995736766e-17
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 180
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "skin",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 2
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 4
},
{
"__id__": 6
},
{
"__id__": 8
}
],
"_prefab": {
"__id__": 10
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 1,
"w": 6.123233995736766e-17
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 180
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": {
"__id__": 5
},
"_contentSize": {
"__type__": "cc.Size",
"width": 73.52999877929688,
"height": 96.18000030517578
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5167958742126253,
"y": 0.46049075457472155
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "10Ma4aWlBFk6tBs31iA9DF"
},
{
"__type__": "sp.Skeleton",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": {
"__id__": 7
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_skeletonData": {
"__uuid__": "542e1bba-b475-48e1-93b2-89480d605a79",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "红缨枪",
"defaultAnimation": "待机",
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 0,
"_cacheMode": 0,
"_sockets": [],
"_useTint": false,
"_debugMesh": false,
"_debugBones": false,
"_debugSlots": false,
"_enableBatch": false,
"loop": true,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "53Oqvwb2JMo7/t619sZApZ"
},
{
"__type__": "cc.Sorting2D",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": {
"__id__": 9
},
"_sortingLayer": 3,
"_sortingOrder": 3,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "c5Vivgz8hNoqVvdPwfRfMe"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "5eJtTesAhFjKJGfAwMDj8P",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 12
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "859ErfVzpFGosE8chK2XD9"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "12h5zzJolI66bNtwPs5kJw",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 15
},
"_contentSize": {
"__type__": "cc.Size",
"width": 83,
"height": 83
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "d9phYN8o1N0pHVkixsNcQj"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "15uJpnkDNKApBLkoqMhhsr",
"instance": null,
"targetOverrides": null
}
]

@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "f9ea9d9f-0c4b-4bbd-ae53-0f953ad77b53",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "weaponType5"
}
}

@ -8,7 +8,7 @@
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -30,7 +30,7 @@
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -52,7 +52,7 @@
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -74,7 +74,7 @@
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -96,7 +96,7 @@
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -118,7 +118,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -140,7 +140,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 260,
@ -162,7 +162,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 260,
@ -184,7 +184,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 300,
@ -206,7 +206,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 300,
@ -228,7 +228,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -250,7 +250,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -272,7 +272,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -294,7 +294,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -316,7 +316,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -338,7 +338,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -360,7 +360,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -382,7 +382,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -404,7 +404,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -426,7 +426,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -448,7 +448,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -470,7 +470,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -492,7 +492,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -514,7 +514,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -536,7 +536,7 @@
"level": 0,
"cd": 1,
"priority": 0,
"animation": "攻击",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -558,7 +558,7 @@
"level": 0,
"cd": 2.4,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -580,7 +580,7 @@
"level": 0,
"cd": 1.6,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -602,7 +602,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -624,7 +624,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -646,7 +646,7 @@
"level": 0,
"cd": 1.6,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -668,7 +668,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -690,7 +690,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -712,7 +712,7 @@
"level": 0,
"cd": 1.6,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -734,7 +734,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -756,7 +756,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -778,7 +778,7 @@
"level": 0,
"cd": 1.6,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -800,7 +800,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -822,7 +822,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -844,7 +844,7 @@
"level": 0,
"cd": 16,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -866,7 +866,7 @@
"level": 0,
"cd": 12,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -888,7 +888,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "悟空专属技能效果",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -910,7 +910,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -932,7 +932,7 @@
"level": 0,
"cd": 12,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -954,7 +954,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "悟空专属技能效果",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -976,7 +976,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -998,7 +998,7 @@
"level": 0,
"cd": 12,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1020,7 +1020,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "悟空专属技能效果",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1042,7 +1042,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1064,7 +1064,7 @@
"level": 0,
"cd": 12,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1086,7 +1086,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "悟空专属技能效果",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1108,7 +1108,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1130,7 +1130,7 @@
"level": 0,
"cd": 2,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1152,7 +1152,7 @@
"level": 0,
"cd": 2,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1174,7 +1174,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "暴风雪技能",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1196,7 +1196,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1218,7 +1218,7 @@
"level": 0,
"cd": 2,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1240,7 +1240,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "暴风雪技能",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1262,7 +1262,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1284,7 +1284,7 @@
"level": 0,
"cd": 2,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1306,7 +1306,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "暴风雪技能",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1328,7 +1328,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1350,7 +1350,7 @@
"level": 0,
"cd": 2,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1372,7 +1372,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "暴风雪技能",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1394,7 +1394,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "水弹攻击1",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1416,7 +1416,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1438,7 +1438,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1460,7 +1460,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1482,7 +1482,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1504,7 +1504,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1526,7 +1526,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1548,7 +1548,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1570,7 +1570,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1592,7 +1592,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1614,7 +1614,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1636,7 +1636,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 0,
"range": 0,
@ -1658,7 +1658,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1680,7 +1680,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1702,7 +1702,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 3,
"range": 0,
@ -1724,7 +1724,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击",
"showeffect": "",
"target_stage": 3,
"range": 0,
@ -1746,7 +1746,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1768,7 +1768,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1790,7 +1790,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 3,
"range": 0,
@ -1812,7 +1812,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1834,7 +1834,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1856,7 +1856,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 3,
"range": 0,
@ -1878,7 +1878,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1900,7 +1900,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1922,7 +1922,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 3,
"range": 0,
@ -1944,7 +1944,7 @@
"level": 0,
"cd": 20,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,
@ -1966,7 +1966,7 @@
"level": 0,
"cd": 0,
"priority": 100,
"animation": "",
"animation": "攻击1",
"showeffect": "",
"target_stage": 1,
"range": 0,

@ -12,6 +12,12 @@ export interface IComposeConfigProvider {
passiveSpawnKeepsDigCell(weapon2Id: number): boolean;
/** 保持 Dig 格的字块(蟠桃树)可合成等级上限 */
getKeepDigTileMaxLevel(weapon2Id: number): number;
/** 五行 Text.type 可合成到的最高显示等级(Hero.level 最大值) */
/** 五行显示等级上限:同 herotype 的 Hero.level 最大值 */
getElementMaxLevel(textType: number): number;
/**
* 五行 Hero.id:herotype=Text.type 且 level=显示等级。
* @param textType Text.type(1–5)
* @param level 显示等级
*/
getElementHeroId(textType: number, level: number): number | null;
}

@ -1358,6 +1358,7 @@ export class MergeSession {
if (!used.has(gridIndex)) restLayout.set(gridIndex, weapon2Id);
});
const found = this._matcher.findCompositions(restLayout, this._coordOf);
this._appendElementCompositions(restLayout, found);
this._compositions = preserved.concat(this._filterUniqueMultiHeroes(preserved, found));
const activeKeys = createMembershipSet<string>();
@ -1382,6 +1383,35 @@ export class MergeSession {
this.emit(MergeEventFactory.composeChanged(this._compositions));
}
/**
* 五行单格:按 Text.type + 显示等级解析 Hero.id,作为可部署攻击单位。
* @param restLayout 未被多格组合占用的字块
* @param found 匹配结果(就地追加)
*/
private _appendElementCompositions(restLayout: Map<number, number>, found: ComposedUnit[]): void {
const occupied = createMembershipSet<number>();
for (let i = 0; i < found.length; i++) {
const indices = found[i].gridIndices;
for (let j = 0; j < indices.length; j++) membershipAdd(occupied, indices[j]);
}
restLayout.forEach((weapon2Id, gridIndex) => {
if (occupied.has(gridIndex)) return;
const type = Math.floor(this._config.getWeapon2Type(weapon2Id));
if (!isElementGlyphType(type)) return;
const tile = this.map.getTile(gridIndex);
const level = Math.max(1, Math.floor(tile?.level ?? 1));
const heroId = this._config.getElementHeroId(type, level);
if (!(heroId > 0)) return;
membershipAdd(occupied, gridIndex);
found.push({
weapon1Id: heroId,
level,
gridIndices: [gridIndex],
direction: 'horizontal',
});
});
}
/**
* 多格神将:同 weapon1Id(Hero.id)本方最多 1 个;已有则新匹配保持散字不成第二将。
* @param preserved 已保留组合

@ -242,7 +242,7 @@ export class AiBattleBridge {
this._map = map;
this._profile = resolved;
if (skillParent) {
this._skillHost.bind(battle, map, skillParent);
this._skillHost.bind(battle, map, skillParent, this._mapText.getMapGrids());
} else {
aiLog('[AiBattleBridge] skillParent missing: AI towers may not attack');
}
@ -283,7 +283,7 @@ export class AiBattleBridge {
if (!map || !bag || !battle) return;
const aiPlayer = battle.getPlayer(AiHeadlessBagHost.AI_PLAYER_ID) ?? null;
this._mapText.requestSync(bag, map, aiPlayer);
this._skillHost.refresh(map);
this._skillHost.refresh(map, this._mapText.getMapGrids());
};
/** 刷新拍:乾坤袋回收背包多余字 */

@ -852,4 +852,14 @@ export class AiMapTextHost {
if (rec.grid?.isValid) visitor(rec.grid, rec.gridIndex);
}
}
/** SkillController 用的格 → TextGrid */
getMapGrids(): ReadonlyMap<number, TextGrid> {
const out = new Map<number, TextGrid>();
for (let i = 0; i < this._recs.length; i++) {
const rec = this._recs[i];
if (rec.grid?.isValid) out.set(rec.gridIndex, rec.grid);
}
return out;
}
}

@ -4,6 +4,7 @@ import type { ProjectBattleHandle } from '../../runtime/BattleRuntime';
import type { MapManager } from '../../map/legacy/MapManager';
import { SkillController } from '../../skill/SkillController';
import { AiHeadlessBagHost } from './AiHeadlessBagHost';
import type { TextGrid } from '../../view/TextGrid';
/**
* AI 技能表现宿主:独立 SkillController,承接 AI 塔 PresentationCue。
@ -18,8 +19,9 @@ export class AiSkillHost {
* @param battle 战斗
* @param map 地图
* @param parent UI 节点(挂载组件)
* @param mapGrids AI 地图 TextGrid
*/
bind(battle: ProjectBattleHandle, map: MapManager, parent: Node): boolean {
bind(battle: ProjectBattleHandle, map: MapManager, parent: Node, mapGrids?: ReadonlyMap<number, TextGrid>): boolean {
this.dispose();
const aiPlayer = battle.getPlayer(AiHeadlessBagHost.AI_PLAYER_ID);
if (!aiPlayer) {
@ -38,7 +40,7 @@ export class AiSkillHost {
let sc = host.getComponent(SkillController);
if (!sc) sc = host.addComponent(SkillController);
sc.bind(aiPlayer, battle);
sc.refreshEntitySkillBindings(map);
sc.refreshEntitySkillBindings(map, mapGrids);
aiPlayer.skillController = sc;
this._hostNode = host;
this._controller = sc;
@ -49,9 +51,10 @@ export class AiSkillHost {
/**
* 部署变化后刷新 entity→Skill 映射。
* @param map 地图
* @param mapGrids AI 地图 TextGrid
*/
refresh(map: MapManager): void {
this._controller?.refreshEntitySkillBindings(map);
refresh(map: MapManager, mapGrids?: ReadonlyMap<number, TextGrid>): void {
this._controller?.refreshEntitySkillBindings(map, mapGrids);
}
/** 解除绑定 */

@ -901,6 +901,21 @@ export class BattleBagSession {
this._map.areaUnlock.setLevel(levelId, levelValue);
}
/**
* 拖字是否满足当前解锁区域:Text.type 对齐 unlockland 类型(0 为任意),显示等级达到需求。
* @param gridIndex 落点
* @param weapon2Id 文字 id
* @param level 字块显示等级
*/
canUnlockArea(gridIndex: number, weapon2Id: number, level?: number): boolean {
const grid = this.validator.getGridByIndex(gridIndex);
if (!grid?.isAreaUnlock) return false;
const lv = Math.floor(Number(level));
if (!(lv > 0)) return false;
WeaponComposeConfigProvider.instance.load();
return this._map.areaUnlock.canDrag(grid, this._heroOfGlyph({ weapon2Id, level: lv }));
}
/**
* 五行字拖到当前解锁区域:类型/显示等级符合则整区可放置并播 openSpine,随后字落在当前格。
* @param source 拖拽源

@ -159,9 +159,15 @@ export class WeaponComposeConfigProvider implements IComposeConfigProvider {
const row = this._weapon1List.find((w) => Number(w.id) === id);
if (!row) {
console.log('[WeaponCompose] Hero 缺失,等级上限按 1', id);
} else {
console.log('[WeaponCompose] Hero.level_limit 未配置,等级上限按 1', id);
return 1;
}
const ht = Math.floor(Number(row.herotype));
if (ht >= 1 && ht <= 5) {
const max = this.getElementMaxLevel(ht);
this._maxLevel.set(id, max);
return max;
}
console.log('[WeaponCompose] Hero.level_limit 未配置,等级上限按 1', id);
return 1;
}
@ -205,6 +211,26 @@ export class WeaponComposeConfigProvider implements IComposeConfigProvider {
return max;
}
/**
* 五行 Hero.id:herotype 与显示等级对齐。
* @param textType Text.type(1–5)
* @param level 显示等级
*/
getElementHeroId(textType: number, level: number): number | null {
this.load();
const type = Math.floor(Number(textType));
const lv = Math.floor(Number(level));
if (type < 1 || type > 5 || !(lv > 0)) return null;
for (let i = 0; i < this._weapon1List.length; i++) {
const hero = this._weapon1List[i];
if (Math.floor(Number(hero.herotype)) !== type) continue;
if (Math.floor(Number(hero.level)) !== lv) continue;
return Number(hero.id);
}
console.log('[WeaponCompose] 无五行英雄', type, lv);
return null;
}
/** 蟠桃树合成等级上限(读 Hero.level_limit);其它 weapon2 返回 1 */
getKeepDigTileMaxLevel(weapon2Id: number): number {
this.load();

@ -19,7 +19,7 @@ import type { TowerLoadout } from './TowerQueryTypes';
export type { TowerLoadout };
import { SkillAbilityBuilder } from '../config/baseAtk/SkillAbilityBuilder';
import { heroNormalSkillId, heroUltSkillId } from '../config/BattleHeroTable';
import { heroNormalSkillId, heroUltSkillId, isElementHero } from '../config/BattleHeroTable';
import { dumpKnifeFinalAttack, logKnifeBaseAttack } from '../debug/AtkDebugLogCore';
import { isAtkDebugWeapon } from '../debug/AtkDebugConstants';
import { getPassiveSkillHost } from './PassiveSkillRuntime';
@ -38,21 +38,21 @@ export function setAiHeroWeaponResolver(resolver: ((heroId: number) => number |
_aiHeroWeaponResolver = resolver;
}
/** 解析英雄当前战斗武器 id(本机走装备;AI 走匹配档案掷骰) */
/** 解析英雄当前战斗武器 id(本机走装备;AI 走匹配档案掷骰)。五行数字只用 herotype 预制体,不查 Weapon 表。 */
export function resolveHeroBattleWeaponId(heroId: number, player?: Player | null): number {
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
if (isElementHero(hero)) return 0;
if (player && !player.isLocal) {
if (_aiHeroWeaponResolver) {
const aiId = _aiHeroWeaponResolver(heroId);
if (typeof aiId === 'number' && aiId > 0) return aiId;
}
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
return Number(hero?.startweapon) || 0;
}
try {
const id = gg.equip?.getHeroDisplayWeaponId?.(heroId);
if (typeof id === 'number' && id > 0) return id;
} catch { /* equip 未就绪 */ }
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
return Number(hero?.startweapon) || 0;
}

@ -1,7 +1,8 @@
import { heroNormalSkillId } from '../config/BattleHeroTable';
import { heroNormalSkillId, isElementHero } from '../config/BattleHeroTable';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { TableNames } from '../../game/ConfigTableData';
import type { TowerCombatStats } from './TowerQueryTypes';
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
/** 常量表「攻击力等级系数」id(对齐 ConstNumType.AtkLevelCoef) */
const CONST_ATK_LEVEL_COEF = 13;
@ -130,11 +131,18 @@ function rangeAddFromAtkBuff(raw: string | undefined): number {
* @param battleWeaponId 战斗武器 id;AI/指定装载须传入,缺省才读本机 gg.equip
*/
export function deriveTowerSetupStats(w1: ITableHero, composeLevel: number, battleWeaponId?: number): TowerSetupStats {
if (!(w1.level_limit > 0)) {
console.log('[TowerStatResolver] Hero.level_limit 未配置', w1.id);
const compose = Math.max(1, Math.floor(composeLevel) || 1);
let effectiveLevel: number;
if (isElementHero(w1)) {
const cap = WeaponComposeConfigProvider.instance.getElementMaxLevel(Math.floor(Number(w1.herotype)));
effectiveLevel = Math.min(compose, cap);
} else {
if (!(w1.level_limit > 0)) {
console.log('[TowerStatResolver] Hero.level_limit 未配置', w1.id);
}
const levelLimit = w1.level_limit > 0 ? w1.level_limit : 1;
effectiveLevel = Math.min(compose, levelLimit);
}
const levelLimit = w1.level_limit > 0 ? w1.level_limit : 1;
const effectiveLevel = Math.max(1, Math.min(composeLevel, levelLimit));
const weaponId = resolveSetupWeaponId(w1, battleWeaponId);
const weaponRow = weaponId > 0
? gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, weaponId)

@ -54,6 +54,25 @@ export function isComboHero(hero: ITableHero): boolean {
return Number(hero.herotype) === 2;
}
/** 是否五行数字英雄(herotype 1–5) */
export function isElementHero(hero: ITableHero | null | undefined): boolean {
if (!hero) return false;
const t = Math.floor(Number(hero.herotype));
return t >= 1 && t <= 5;
}
/**
* Hero.id 是否为五行战斗单位(有普攻)。
* @param heroId Hero.id
*/
export function isElementCombatHeroId(heroId: number): boolean {
const id = Math.floor(Number(heroId) || 0);
if (!(id > 0)) return false;
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, id);
if (!isElementHero(hero) || isNonCombatWeapon1(id)) return false;
return heroNormalSkillId(hero!, 1) > 0;
}
/**
* 从塔实体解析 Hero.id(unitDefId / weapon1_ 标签)。
* @param world 战斗世界

@ -8,11 +8,11 @@ import {
makeMonsterUnitDefId,
} from './MonsterHpCalculator';
/** 调试:双方出怪口不出小怪 */
export const NO_SMALL_MONSTER = true;
/** 调试:双方出怪口不出小怪(正式出怪须为 false) */
export const NO_SMALL_MONSTER = false;
/** 调试:双方出怪口不出 Boss */
export const NO_BOSS = true;
/** 调试:双方出怪口不出 Boss(正式出怪须为 false) */
export const NO_BOSS = false;
/** Level.monster 单条:waveIndex,monsterId,count,spawnInterval */
export interface LevelMonsterWaveRow {

@ -1,6 +1,6 @@
import { _decorator, sp } from 'cc';
import type { EntityId } from 'db://assets/mx/module/ecs';
import { isComboHero } from '../config/BattleHeroTable';
import { isComboHero, isElementHero } from '../config/BattleHeroTable';
import { Skill } from '../skill/Skill';
import { HeroSkin } from '../view/HeroSkin';
import { TableNames } from '../../game/ConfigTableData';
@ -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
@ -79,7 +79,7 @@ export class HeroActor extends Skill {
? gg.data.table.getTableData<ITableHero>(TableNames.Hero, this._heroId)
: null);
this._heroId = Number(data?.id) || this._heroId;
const useWeapon = !!data && isComboHero(data);
const useWeapon = isElementHero(data) || (!!data && isComboHero(data));
this.visualChannel.bind(this._heroSkin, this.node, this._heroId, useWeapon);
}

@ -104,11 +104,31 @@ export class MapAreaUnlock {
}
/**
* 把英雄拖到当前解锁区域。类型 0 表示任意 herotype,等级须达到配置等级加段位加值。
* 拖到当前解锁区域是否满足类型与等级(不改状态)。
* 类型 0 表示任意 herotype,等级须达到配置等级加段位加值。
*/
canDrag(grid: MapGrid, hero: AreaUnlockHero | null): boolean {
return this._probe(grid, hero) === 'unlocked';
}
/**
* 把英雄拖到当前解锁区域。条件与 canDrag 相同,成功则整区解锁。
* @param grid 落点格
* @param hero 拖动的英雄;没有对应英雄时传 null
*/
tryDrag(grid: MapGrid, hero: AreaUnlockHero | null): AreaUnlockDrag {
const hit = this._probe(grid, hero);
if (hit !== 'unlocked') return hit;
const index = this._indexOf(this.currentId());
this._done[index] = true;
this._rollCurrent();
return 'unlocked';
}
/**
* 解锁落点探测:skip 非解锁格;blocked 未轮到或类型/等级不符;unlocked 条件满足。
*/
private _probe(grid: MapGrid, hero: AreaUnlockHero | null): AreaUnlockDrag {
if (!grid?.isAreaUnlock) return 'skip';
const current = this.currentId();
if (current <= 0 || grid.unlockAreaId !== current) return 'blocked';
@ -118,8 +138,6 @@ export class MapAreaUnlock {
const typeOk = this._heroTypes[index] === 0 || hero.heroType === this._heroTypes[index];
const levelOk = hero.level >= this._needLevels[index];
if (!typeOk || !levelOk) return 'blocked';
this._done[index] = true;
this._rollCurrent();
return 'unlocked';
}

@ -1,6 +1,6 @@
import type { EntityId } from 'db://assets/mx/module/ecs';
import { instantiate, Node, sp, UITransform, Vec3 } from 'cc';
import { GBundle, GPath } from '../../game/ConfigRes';
import { TableNames } from '../../game/ConfigTableData';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { getTowerLoadoutService, resolveHeroBattleWeaponId } from '../combat/TowerLoadoutService';
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
@ -8,11 +8,12 @@ import { schedulePresentation } from '../skill/BattlePresentationClock';
import { SkillTransformDriver, computeAimAngleDeg } from '../skill/SkillTransformDriver';
import { worldPointAtAimAngle, computeArrowBowAimDeg, ARROW_BOW_OFFSET_DEG } from '../skill/SkillParentAim';
import { HeroSkin } from '../view/HeroSkin';
import { HeroWeaponMount, WEAPON_REST_ANGLE, BOW_ATK_REST_ANGLE, BOW_RAIN_AIM_DEG } from '../view/HeroWeaponMount';
import { HeroWeaponMount, WEAPON_REST_ANGLE, BOW_ATK_REST_ANGLE, BOW_RAIN_AIM_DEG, WEAPON_LEFT_OFFSET_PX } from '../view/HeroWeaponMount';
import { BattleSpinePlayback } from './BattleSpinePlayback';
import { glyphSpineHasSkin } from '../view/GlyphSpineSkin';
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
import { BOW_ATK_ARROW_SPEED, SCATTER_RANGE_MUL, getBowAtkHost } from '../combat/BowAtkRuntime';
import { GBundle, GPath } from '../../game/ConfigRes';
/** 默认命中相对攻击动画进度(@ timeScale=1) */
export const PRES_HIT_AT_BASE = 0.33;
@ -21,8 +22,6 @@ const BOW_RELEASE_AT_BASE = 0.4333;
/** 拉弓表现上限(秒),与英雄弓一致 */
const BOW_ATTACK_MAX_SEC = 0.19;
/** 武器相对组合首字格中心再向左偏移(约 0.35 格) */
const WEAPON_LEFT_OFFSET_PX = BattleMapSpec.CELL_SIZE * 0.35;
/** 悟空主动武器动画:绕两字椭圆一圈 */
const WUKONG_SKILL_ANIM = '悟空技能';
/** 木吒主动武器动画 */
@ -40,10 +39,16 @@ const JINGU_SKILL_HIT_AT = 0.8667;
const WUKONG_ORBIT_RADIUS_Y = BattleMapSpec.CELL_SIZE * 0.5;
/** 绕圈时长 = 动画时长减去该秒数 */
const WUKONG_ORBIT_TRIM_SEC = 0.3;
/** Weapon.type 刀 */
/** Weapon.type 刀 / herotype 金 */
const WEAPON_TYPE_SWORD = 1;
/** Weapon.type 弓 */
/** herotype 木 */
const WEAPON_TYPE_WOOD = 2;
/** herotype 水 */
const WEAPON_TYPE_STAFF = 3;
/** Weapon.type 弓 / herotype 火 */
const WEAPON_TYPE_BOW = 4;
/** herotype 土(老项目枪) */
const WEAPON_TYPE_SPEAR = 5;
/** 控制点沿出弓切线的路程比例(切线相对目标连线偏 ARROW_BOW_OFFSET_DEG) */
const BOW_ARROW_CTRL_T = 0.42;
/** 连发固定三弹道,发次对 3 取余循环 */
@ -60,6 +65,10 @@ const WEAPON_AIM_SEC = 0.2;
const WEAPON_REST_HOLD_SEC = 1;
/** @deprecated 保留别名;请用 WEAPON_AIM_SEC */
const BOW_AIM_SEC = WEAPON_AIM_SEC;
/** battleHero/prefab/skill/水弹 */
const WATER_BALL_PREFAB = '水弹';
/** 水弹飞行像素/秒(与弓箭同量级) */
const WATER_BALL_SPEED = BOW_ATK_ARROW_SPEED;
/** battleHero/prefab/skill/玉帝飞剑 */
const YUDI_FLY_SWORD_PREFAB = '玉帝飞剑';
/** 飞剑预制体默认动画(带光效) */
@ -106,6 +115,39 @@ class VolleyFlyRecycle {
}
}
/**
* 水弹落地回池(类方法,避免函数内闭包)。
*/
class WaterBallRecycle {
private static readonly _pool: WaterBallRecycle[] = [];
private _channel: SkillVisualChannel | null = null;
private _node: Node | null = null;
/** 落地:回池水弹 */
readonly onLand = (): void => {
const channel = this._channel;
const node = this._node;
this._channel = null;
this._node = null;
WaterBallRecycle._pool.push(this);
if (channel && node) channel.recycleWaterBall(node);
};
/**
* 取得绑定。
* @param channel 表现通道
* @param node 水弹
*/
static obtain(channel: SkillVisualChannel, node: Node): WaterBallRecycle {
const rec = WaterBallRecycle._pool.length > 0
? WaterBallRecycle._pool.pop()!
: new WaterBallRecycle();
rec._channel = channel;
rec._node = node;
return rec;
}
}
/** 待发射的弓箭 */
interface BowArrowLaunch {
/** 代数,过期则丢弃 */
@ -231,6 +273,27 @@ export class SkillVisualChannel {
return this._lastThrowFlySec;
}
/** 当前武器表现类型(Hero.herotype / Weapon.type) */
private _visType(): number {
return this._weapon.weaponType;
}
/** 金/木:只左右翻转 */
private _isFlipWeapon(): boolean {
const t = this._visType();
return t === WEAPON_TYPE_SWORD || t === WEAPON_TYPE_WOOD;
}
/** 水:法杖不旋转 */
private _isStaffWeapon(): boolean {
return this._visType() === WEAPON_TYPE_STAFF;
}
/** 火:弓 */
private _isBowWeapon(): boolean {
return this._visType() === WEAPON_TYPE_BOW;
}
/**
* 释放动画倍速:hero/weapon/monster 固定 1;普攻跟攻速;显式 override 优先。
* @param caster 施法实体
@ -258,9 +321,12 @@ export class SkillVisualChannel {
if (!this._useWeapon) this._weapon.setVisible(false);
}
/** 按塔 loadout / 归属玩家装备 / startweapon 挂载武器 */
/** 按塔 loadout / 归属玩家装备 / startweapon 挂载武器;五行数字按 Hero.herotype */
ensureWeapon(): boolean {
if (!this._useWeapon || !this._parent?.isValid || this._heroId <= 0) return false;
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, this._heroId);
const ht = Math.floor(Number(hero?.herotype) || 0);
if (ht >= 1 && ht <= 5) return this._weapon.ensureByHeroType(this._parent, ht);
let weaponId = 0;
if (this._caster != null) {
const loadout = getTowerLoadoutService().getLoadout(this._caster);
@ -310,11 +376,8 @@ export class SkillVisualChannel {
this._weapon.setVisible(false);
return false;
}
// 刀立刻复位;枪/棍由回正插值到位,此处 resetAim 会打断插值
const wt = this._weapon.weaponType;
if (wt === WEAPON_TYPE_SWORD || wt <= 0) {
this._weapon.resetAim();
}
// 刀/木立刻复位左右;法杖保持预制体;枪回正走插值
if (this._isFlipWeapon()) this._weapon.resetAim();
this._weapon.setVisible(this._weaponFlies.length === 0);
this._weapon.playIdle();
return !!this._weapon.root?.active;
@ -380,7 +443,10 @@ export class SkillVisualChannel {
this._castAnimParams = params;
this._castAnimLeft = 0;
this._lastThrowFlySec = 0;
const weaponAnim = String(params.animation ?? '').trim() || GLYPH_CAST_ANIM;
const weaponAnim = String(params.animation ?? '').trim();
if (!weaponAnim) {
console.warn('[SkillVisualChannel] Skill.animation 为空', params.caster);
}
const follow = params.followWeapon && this._useWeapon && this.ensureWeapon();
if (follow) {
this._cancelRestHold();
@ -389,21 +455,23 @@ export class SkillVisualChannel {
this.syncWeaponPivot(params.caster);
if (params.throwWeapon) {
this._lastThrowFlySec = this._launchWeaponThrow(params.primaryTarget ?? 0);
} else if (this._weapon.weaponType === WEAPON_TYPE_BOW) {
} else if (this._isBowWeapon()) {
this._weapon.showBowAtk();
} else if (this._weaponFlies.length === 0) {
this._weapon.setVisible(true);
}
this._weaponCasting = true;
const bowAnimTs = this._weapon.weaponType === WEAPON_TYPE_BOW
const bowAnimTs = this._isBowWeapon()
? Math.max(animScale, BOW_RELEASE_AT_BASE / BOW_ATTACK_MAX_SEC)
: animScale;
const orbitStaff = this._isWukongSkillOrbit(params);
if (params.keepRestAim || orbitStaff) {
this._weapon.resetAim();
} else if (this._weapon.weaponType === WEAPON_TYPE_BOW) {
if (!this._isStaffWeapon()) this._weapon.resetAim();
} else if (this._isBowWeapon()) {
this._aimWeapon(params.primaryTarget, bowAnimTs, params.aimWorldX, params.aimWorldY, params.fixedAimDeg);
if (!params.skipBowArrow) this._armBowArrow(params.primaryTarget, bowAnimTs);
} else if (this._isStaffWeapon()) {
/* 法杖不朝向 */
} else if (!params.throwWeapon) {
this._aimWeapon(params.primaryTarget, animScale, params.aimWorldX, params.aimWorldY);
}
@ -414,7 +482,7 @@ export class SkillVisualChannel {
this._onFollowCastAllComplete();
return animScale;
}
const held = this._weapon.weaponType === WEAPON_TYPE_BOW
const held = this._isBowWeapon()
? this._weapon.playBowDraw(bowAnimTs, this._onFollowCastAllComplete, weaponAnim)
: this._weapon.playAttack(animScale, this._onFollowCastAllComplete, weaponAnim);
if (!held) {
@ -432,7 +500,7 @@ export class SkillVisualChannel {
return animScale;
}
this._castAnimLeft++;
const played = this._weapon.weaponType === WEAPON_TYPE_BOW
const played = this._isBowWeapon()
? this._weapon.playBowDraw(bowAnimTs, this._onCastAnimPartComplete, weaponAnim)
: this._weapon.playAttack(animScale, this._onCastAnimPartComplete, weaponAnim);
if (!played) {
@ -462,11 +530,10 @@ export class SkillVisualChannel {
this.playIdle();
}
/** 弓/枪/棍等旋转武器回待机须插值;刀左右翻转立刻复位 */
/** 弓/枪回待机须插值;刀/木立刻复位;法杖不转 */
private _shouldTweenRestAim(): boolean {
const t = this._weapon.weaponType;
if (t === WEAPON_TYPE_SWORD || t <= 0) return false;
return true;
const t = this._visType();
return t === WEAPON_TYPE_BOW || t === WEAPON_TYPE_SPEAR;
}
/** 枪/棍:攻击2 定格末帧,1 秒内无下一刀再回待机(避免每刀闪回字形) */
@ -536,7 +603,7 @@ export class SkillVisualChannel {
this._weaponCasting = false;
const aspd = Math.max(0.01, SkillPresentationEnv.getFinalAttackSpeed(params.caster));
const restScale = params.animTimeScale != null && params.animTimeScale > 0 ? params.animTimeScale : aspd;
if (this._weapon.weaponType === WEAPON_TYPE_BOW && this._weapon.bowAtkShowing) {
if (this._isBowWeapon() && this._weapon.bowAtkShowing) {
this._weapon.freezeBowDraw();
this._restWeaponAim(restScale);
} else if (this._shouldTweenRestAim()) {
@ -561,15 +628,20 @@ export class SkillVisualChannel {
if (!root?.isValid || !parent?.isValid) return false;
const id = caster ?? this._caster;
const world = this._resolveFirstGlyphWorld(id, quiet);
if (!world) return false;
if (!world) {
if (this._weapon.weaponId <= 0) {
root.setPosition(-WEAPON_LEFT_OFFSET_PX, 0, 0);
return true;
}
return false;
}
this._weaponWorld.set(world.x - WEAPON_LEFT_OFFSET_PX, world.y, world.z);
parent.inverseTransformPoint(this._weaponWorld, this._weaponWorld);
root.setPosition(this._weaponWorld);
root.angle = 0;
if (this._weapon.bowAtkShowing) this._weapon.syncBowAtkPivot();
// 仅刀在非施法时立刻复位;枪/棍回正走插值,此处 resetAim 会硬切
if (!this._weaponCasting
&& (this._weapon.weaponType === WEAPON_TYPE_SWORD || this._weapon.weaponType <= 0)) {
if (!this._weaponCasting && this._isFlipWeapon()) {
this._weapon.resetAim();
}
return true;
@ -583,7 +655,7 @@ export class SkillVisualChannel {
if (String(params.skillKind ?? '') !== 'hero') return false;
if (String(params.animation ?? '').trim() !== WUKONG_SKILL_ANIM) return false;
if (params.throwWeapon) return false;
return this._weapon.weaponType !== WEAPON_TYPE_BOW;
return !this._isBowWeapon();
}
/**
@ -686,7 +758,7 @@ export class SkillVisualChannel {
landId: params?.projectileLandId ?? 0,
onLand: params?.onProjectileLand ?? null,
});
const releaseAt = this._weapon.weaponType === WEAPON_TYPE_BOW ? BOW_RELEASE_AT_BASE : PRES_HIT_AT_BASE;
const releaseAt = this._isBowWeapon() ? BOW_RELEASE_AT_BASE : PRES_HIT_AT_BASE;
schedulePresentation(releaseAt / Math.max(0.01, aspd), this._onBowLaunchDue);
}
@ -716,7 +788,7 @@ export class SkillVisualChannel {
if (job.onLand) job.onLand(job.target, job.landId);
return;
}
if (this._weapon.weaponType === WEAPON_TYPE_BOW) {
if (this._isBowWeapon()) {
this._launchBowArrow(job.target, job.landId, job.onLand);
} else {
this._launchWeaponThrow(job.target);
@ -823,7 +895,15 @@ export class SkillVisualChannel {
*/
resolveBowShapeLaunchDelay(animScale: number): number {
const scale = Math.max(0.01, animScale);
if (this._weapon.weaponType === WEAPON_TYPE_BOW) {
if (this._isStaffWeapon()) {
const name = String(this._castAnimParams?.animation ?? '').trim();
const anim = this._weapon.skinAnim;
const dur = name && anim?.isValid ? BattleSpinePlayback.getAnimDuration(anim, name) : null;
if (dur != null && dur > 0) return dur / scale;
console.warn('[SkillVisualChannel] 水弹出手延迟无动画时长', name);
return PRES_HIT_AT_BASE / scale;
}
if (this._isBowWeapon()) {
const bowTs = Math.max(scale, BOW_RELEASE_AT_BASE / BOW_ATTACK_MAX_SEC);
return BOW_RELEASE_AT_BASE / bowTs;
}
@ -843,7 +923,10 @@ export class SkillVisualChannel {
* @returns 是否发出
*/
launchVolleyShot(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string, shotIndex: number, aimWorld: Readonly<Vec3>): boolean {
if (this._weapon.weaponType === WEAPON_TYPE_BOW && !this._weapon.bowAtkShowing) {
if (this._isStaffWeapon()) {
return this._launchWaterBall(caster, abilityId, target, scale, buff1, buff2, aimWorld);
}
if (this._isBowWeapon() && !this._weapon.bowAtkShowing) {
this._weapon.showBowAtk();
}
this._weaponCasting = true;
@ -909,6 +992,14 @@ export class SkillVisualChannel {
this._recycleBowArrow(node);
}
/**
* 回收飞行水弹。
* @param node 水弹根
*/
recycleWaterBall(node: Node): void {
this._recycleWeaponFly(node);
}
/**
* 克隆箭到飞行层。
* @param fromWorld 起点世界坐标
@ -1028,6 +1119,92 @@ export class SkillVisualChannel {
if (onLand) onLand(target, landId);
}
/**
* 法杖普攻:实例化「水弹」直线飞向目标,落地结算。
* @param caster 施法者
* @param abilityId Ability id
* @param target 锁定目标
* @param scale 伤害倍率
* @param buff1 atk_buff1
* @param buff2 atk_buff2
* @param aimWorld 瞄准点
* @returns 是否发出
*/
private _launchWaterBall(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string, aimWorld: Readonly<Vec3>): boolean {
this._weaponCasting = true;
const fromNode = this._weapon.skinAnim?.node ?? this._weapon.aim ?? this._weapon.root;
if (!fromNode?.isValid) {
console.log('[SkillVisualChannel] 水弹无起点');
return false;
}
const live = target > 0 ? SkillPresentationEnv.getTargetWorldPos(target) : null;
const to = live ?? aimWorld;
if (!to) {
console.log('[SkillVisualChannel] 水弹无目标坐标', target);
return false;
}
this._arrowFrom.set(fromNode.worldPosition);
this._arrowTo.set(to);
const dist = Vec3.distance(this._arrowFrom, this._arrowTo);
const duration = Math.max(0.12, dist / WATER_BALL_SPEED);
const node = this._spawnWaterBall(this._arrowFrom);
if (!node) return false;
SkillTransformDriver.animateParabola({
node,
duration,
fromWorld: this._arrowFrom,
toWorld: this._arrowTo,
arcHeight: 0,
easing: SkillTransformDriver.easeLinear,
faceTangent: true,
});
const land = WaterBallRecycle.obtain(this, node);
getBowAtkHost().spawnVolley({
caster,
target,
abilityId,
wait: duration,
scale,
buff1,
buff2,
onLand: land.onLand,
});
return true;
}
/**
* 取出「水弹」挂到飞行层。
* @param fromWorld 起点
*/
private _spawnWaterBall(fromWorld: Readonly<Vec3>): Node | null {
const flyLayer = SkillPresentationEnv.flyLayer;
if (!flyLayer?.isValid) {
console.log('[SkillVisualChannel] 水弹无飞行层');
return null;
}
let node: Node | null = null;
try {
node = gg.res.getNode(GPath.HeroSkillPrefab(), WATER_BALL_PREFAB, GBundle.BattleHero);
} catch (e) {
console.log('[SkillVisualChannel] 水弹加载异常', String(e));
return null;
}
if (!node?.isValid) {
console.log('[SkillVisualChannel] 水弹预制体未加载');
return null;
}
flyLayer.active = true;
node.parent = flyLayer;
node.active = true;
node.layer = flyLayer.layer;
const flyUt = flyLayer.getComponent(UITransform);
if (flyUt) node.setPosition(flyUt.convertToNodeSpaceAR(fromWorld));
else node.setWorldPosition(fromWorld);
node.setSiblingIndex(flyLayer.children.length - 1);
this._weaponFlies.push(node);
return node;
}
/**
* 取出玉帝飞剑,根节点沿抛物线切线飞向目标。
* @param target 主目标
@ -1303,11 +1480,12 @@ export class SkillVisualChannel {
* @param fixedAimDeg 固定朝向(优先)
*/
private _aimWeapon(primaryTarget: EntityId | null, aspd: number, aimWorldX?: number, aimWorldY?: number, fixedAimDeg?: number): void {
if (this._weapon.weaponType === WEAPON_TYPE_BOW && this._weapon.bowAtkShowing && fixedAimDeg != null && Number.isFinite(fixedAimDeg)) {
if (this._isStaffWeapon()) return;
if (this._isBowWeapon() && this._weapon.bowAtkShowing && fixedAimDeg != null && Number.isFinite(fixedAimDeg)) {
this._tweenWeaponAim(fixedAimDeg, WEAPON_AIM_SEC / Math.max(0.01, aspd));
return;
}
const aimNode = this._weapon.weaponType === WEAPON_TYPE_BOW && this._weapon.bowAtkShowing
const aimNode = this._isBowWeapon() && this._weapon.bowAtkShowing
? (this._weapon.bowAtkRoot ?? this._weapon.aim)
: this._weapon.aim;
if (!aimNode?.isValid) {
@ -1325,18 +1503,18 @@ export class SkillVisualChannel {
if (!targetWorld) {
targetWorld = worldPointAtAimAngle(pivotWorld, WEAPON_REST_ANGLE, 200);
}
if (this._weapon.weaponType === WEAPON_TYPE_SWORD) {
if (this._isFlipWeapon()) {
this._weapon.aimByTargetX(targetWorld.x);
return;
}
const aim = this._weapon.weaponType === WEAPON_TYPE_BOW && !this._aimDirect
const aim = this._isBowWeapon() && !this._aimDirect
? computeArrowBowAimDeg(pivotWorld, targetWorld)
: computeAimAngleDeg(pivotWorld, targetWorld);
if (aim == null) {
console.log('[SkillVisualChannel] 武器朝向为零', this._weapon.weaponId);
return;
}
// 弓转攻效根;枪/棍等转 weapon 节点,均插值,禁止硬切
// 弓转攻效根;枪转 weapon 节点,均插值,禁止硬切
this._tweenWeaponAim(aim, WEAPON_AIM_SEC / Math.max(0.01, aspd));
}
@ -1349,7 +1527,7 @@ export class SkillVisualChannel {
*/
private _restWeaponAim(aspd: number): void {
const duration = WEAPON_AIM_SEC / Math.max(0.01, aspd);
if (this._weapon.weaponType === WEAPON_TYPE_BOW) {
if (this._isBowWeapon()) {
if (this._weapon.bowAtkShowing) {
const fromDeg = this._weapon.bowAtkRoot?.angle ?? BOW_ATK_REST_ANGLE;
if (this._weapon.bowAtkRoot?.isValid) {
@ -1363,11 +1541,11 @@ export class SkillVisualChannel {
this._tweenWeaponAim(WEAPON_REST_ANGLE, duration, this._onWeaponAimRestCompleteBound);
return;
}
if (this._weapon.weaponType === WEAPON_TYPE_SWORD) {
if (this._isFlipWeapon() || this._isStaffWeapon()) {
this._weapon.resetAim();
return;
}
// 枪/棍等:从当前瞄准角插值回待机角,到位后再播待机
// 枪:从当前瞄准角插值回待机角,到位后再播待机
this._tweenWeaponAim(WEAPON_REST_ANGLE, duration, this._onWeaponAimRestCompleteBound);
}

@ -10,7 +10,7 @@ export const BATTLE_HERO_BODY_PATH = 'prefab/hero';
/** 攻效预制体目录(枪/棍/弓等独立攻击 prefab) */
export const BATTLE_HERO_SKILL_PATH = 'prefab/skill';
/** 组合英雄武器类型预制体目录 */
/** 武器类型预制体目录(weaponType1–5) */
export const BATTLE_HERO_WEAPON_PATH = 'prefab/weapon';
/** 局内 buff 预制体目录(缚仙网 / 乾坤袋 / 文昌笔等) */
@ -19,7 +19,7 @@ export const BATTLE_HERO_BUFF_PATH = 'prefab/buff/';
/** battleGameMonster 怪物皮肤目录 */
export const BATTLE_MONSTER_SKIN_PATH = 'prefab/monster';
/** 进战阻塞包:英雄体 + 攻效 + buff(武器目录进局后再静默补) */
/** 进战阻塞包:英雄体 + 攻效 + buff + 武器类型预制体 */
export function appendBattleHeroDirLoads(resPkg: ResPakage): void {
if (!resPkg.dirs) {
resPkg.dirs = [];
@ -28,6 +28,7 @@ export function appendBattleHeroDirLoads(resPkg: ResPakage): void {
{ bundle: BATTLE_HERO_BUNDLE, path: BATTLE_HERO_BODY_PATH, type: Prefab },
{ bundle: BATTLE_HERO_BUNDLE, path: BATTLE_HERO_SKILL_PATH, type: Prefab },
{ bundle: BATTLE_HERO_BUNDLE, path: BATTLE_HERO_BUFF_PATH, type: Prefab },
{ bundle: BATTLE_HERO_BUNDLE, path: BATTLE_HERO_WEAPON_PATH, type: Prefab },
];
for (const dir of dirs) {
resPkg.dirs.push(dir);

@ -22,6 +22,7 @@ import { BattleAudio } from '../audio/BattleAudio';
import { SkillNameFloater, ULT_SKILL_RELEASE_DELAY_SEC } from './SkillNameFloater';
import { HeroActorHost } from '../heros/HeroActorHost';
import { isHeroActorWeapon1 } from '../heros/HeroActorRes';
import { isElementCombatHeroId } from '../config/BattleHeroTable';
import { ensureSpecialSkillPresenters } from '../presentation/SpecialSkillPresenters';
import { getNezhaChargeHost } from '../presentation/NezhaChargePresenter';
import { getPagodaSmashHost } from '../presentation/PagodaSmashPresenter';
@ -394,13 +395,13 @@ export class SkillController extends Component {
*/
private _shouldRetrySkillBind(entityId: EntityId): boolean {
const loadout = getTowerLoadoutService().getLoadout(entityId);
if (loadout && !isHeroActorWeapon1(loadout.weapon1Id)) return false;
if (loadout && !isHeroActorWeapon1(loadout.weapon1Id) && !isElementCombatHeroId(loadout.weapon1Id)) return false;
const session = this._match?.session;
const player = this._player;
const map = this._map;
if (!session || !player || !map) return true;
const comp = findCompositionForEntity(entityId, player, session, map);
if (comp && !isHeroActorWeapon1(comp.weapon1Id)) return false;
if (comp && !isHeroActorWeapon1(comp.weapon1Id) && !isElementCombatHeroId(comp.weapon1Id)) return false;
return true;
}
@ -418,7 +419,16 @@ export class SkillController extends Component {
if (!session || !player || !map) return null;
const comp = findCompositionForEntity(entityId, player, session, map);
if (!comp || !isHeroActorWeapon1(comp.weapon1Id)) return null;
if (!comp) return null;
if (isElementCombatHeroId(comp.weapon1Id)) {
const gridIndex = this._resolveGridIndex(entityId);
const grid = gridIndex != null ? this._mapGrids?.get(gridIndex) : null;
const skill = grid?.ensureNumberSkill(comp.weapon1Id, entityId) ?? null;
if (!skill?.isValid) return null;
this.registerEntitySkill(entityId, skill.node);
return this._skills.get(entityId) ?? skill;
}
if (!isHeroActorWeapon1(comp.weapon1Id)) return null;
HeroActorHost.instance.syncFromMap(entityId, comp, map, !player.isLocal);
const created = HeroActorHost.instance.getActor(entityId);

@ -3,15 +3,17 @@ import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spine
import { GBundle } from '../../game/ConfigRes';
import { TableNames } from '../../game/ConfigTableData';
import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { applyWeaponSpineSkin, spineSkinName } from './GlyphSpineSkin';
/** 武器类型 → 预制体名(Weapon.type:1 刀 / 2 枪 / 3 棍 / 4 弓) */
/** 武器类型 → 预制体名(Hero.herotype / Weapon.type:1–5) */
const WEAPON_TYPE_PREFAB: ReadonlyArray<string> = [
'',
'weaponType1',
'weaponType2',
'weaponType3',
'weaponType4',
'weaponType5',
];
const WEAPON_PREFAB_PATH = 'prefab/weapon/';
@ -24,25 +26,32 @@ const NODE_PROJECTILE_ANIM = 'anim';
/** weapon 节点:刀左右翻转,其余旋转朝向 */
const AIM_NODE = 'weapon';
const SKIN_NODE = 'skin';
/** 技能动画节点(优先于 skin) */
const ANIM_NODE = 'anim';
/** 弓投掷物节点(weaponType4 / weapon 下) */
const ARROW_NODE = '箭';
/** Weapon.type 刀 */
/** Weapon.type / herotype 刀、金 */
const WEAPON_TYPE_SWORD = 1;
/** Weapon.type 弓 */
/** herotype 木:与刀相同只左右翻转 */
const WEAPON_TYPE_WOOD = 2;
/** herotype 水:法杖不旋转 */
const WEAPON_TYPE_STAFF = 3;
/** Weapon.type / herotype 弓、火 */
const WEAPON_TYPE_BOW = 4;
const SPINE_ATTACK_DEFAULT = '攻击';
const SPINE_IDLE = '待机';
/** 四套 weaponType* 的 weapon 节点待机角(预制体 euler.z) */
/** weaponType* 的 weapon 节点待机角(预制体 euler.z) */
export const WEAPON_REST_ANGLE = 180;
/** 相对格心再向左(约 0.35 格,与老项目组合武器一致) */
export const WEAPON_LEFT_OFFSET_PX = BattleMapSpec.CELL_SIZE * 0.35;
/** 「武器弓-攻击」根节点待机角(预制体 euler.z,拉弓动画本地角不动) */
export const BOW_ATK_REST_ANGLE = 0;
/** 万箭齐发:武器弓-攻击朝上 */
export const BOW_RAIN_AIM_DEG = 90;
/**
* 组合英雄战斗武器挂载:按 Weapon.type 加载 weaponType*,按 Weapon.skin 切肤。
* 战斗武器挂载:Weapon.type 或 Hero.herotype 对应 weaponType*。
* 弓攻击藏 weaponType4,改显「武器弓-攻击」;朝向只转该根节点,不改拉弓动画本地角。
* 拉弓动画与投掷物/anim 套当前 Weapon.skin。
*/
export class HeroWeaponMount {
private _root: Node | null = null;
@ -94,11 +103,16 @@ export class HeroWeaponMount {
return this._weaponId;
}
/** 当前武器类型(1–4) */
/** 当前武器类型(1–5);无武器表行时为 herotype */
get weaponType(): number {
return this._type;
}
/** Weapon 表类型;五行挂载(无 weaponId)为 0,避免当成刀/弓 */
get tableWeaponType(): number {
return this._weaponId > 0 ? this._type : 0;
}
/** 当前 Weapon.skin */
get skinName(): string {
return this._skinName;
@ -163,9 +177,9 @@ export class HeroWeaponMount {
this.release();
return false;
}
this._skinAnim = this._aim.getChildByName(SKIN_NODE)?.getComponent(sp.Skeleton) ?? null;
this._skinAnim = this._resolveAnimSpine(root);
if (!this._skinAnim?.isValid) {
console.error('[HeroWeaponMount] 缺少 skin/Spine', prefabName);
console.error('[HeroWeaponMount] 缺少 anim/skin Spine', prefabName);
this.release();
return false;
}
@ -177,11 +191,69 @@ export class HeroWeaponMount {
return true;
}
/** 朝向节点转回预制体待机角;刀额外把 scale.x 复原为 1 */
/**
* 按 Hero.herotype 挂 weaponType*(五行数字,不查 Weapon 表、不套装备皮)。
* @param parent 挂点
* @param herotype Hero.herotype(1–5)
*/
ensureByHeroType(parent: Node, herotype: number): boolean {
if (!parent?.isValid) {
console.error('[HeroWeaponMount] parent 无效');
return false;
}
const type = Math.floor(Number(herotype) || 0);
const prefabName = WEAPON_TYPE_PREFAB[type] ?? '';
if (!prefabName) {
console.error('[HeroWeaponMount] 不支持的 herotype', herotype);
return false;
}
if (this._root?.isValid && this._type === type && this._weaponId <= 0) {
if (this._root.parent !== parent) this._root.parent = parent;
this._root.setPosition(-WEAPON_LEFT_OFFSET_PX, 0, 0);
if (type === WEAPON_TYPE_BOW) {
if (!this._arrow?.isValid) this._bindArrow();
this._ensureBowAtkPrefab();
}
return !!this._skinAnim?.isValid;
}
this.release();
const root = gg.res.getNode(WEAPON_PREFAB_PATH, prefabName, GBundle.BattleHero);
if (!root?.isValid) {
console.error('[HeroWeaponMount] 预制体未加载', prefabName);
return false;
}
root.parent = parent;
root.setPosition(-WEAPON_LEFT_OFFSET_PX, 0, 0);
root.active = false;
this._root = root;
this._type = type;
this._weaponId = 0;
this._aim = root.getChildByName(AIM_NODE);
if (!this._aim?.isValid) {
console.error('[HeroWeaponMount] 缺少 weapon 朝向节点', prefabName);
this.release();
return false;
}
this._skinAnim = this._resolveAnimSpine(root);
if (!this._skinAnim?.isValid) {
console.error('[HeroWeaponMount] 缺少 anim/skin Spine', prefabName);
this.release();
return false;
}
const sd = this._skinAnim.skeletonData;
if (sd?.isValid) ensureSpineSkeletonDataRuntime(sd);
this._bindArrow();
if (type === WEAPON_TYPE_BOW) this._ensureBowAtkPrefab();
if (type !== WEAPON_TYPE_STAFF) this.resetAim();
return true;
}
/** 朝向节点转回待机角;刀/木复位左右缩放。水弹法杖不改预制体角 */
resetAim(): void {
if (!this._aim?.isValid) return;
if (this._type === WEAPON_TYPE_STAFF) return;
this._aim.angle = WEAPON_REST_ANGLE;
if (this._type === WEAPON_TYPE_SWORD) this._setAimScaleX(1);
if (this._isFlipType()) this._setAimScaleX(1);
}
/**
@ -193,13 +265,13 @@ export class HeroWeaponMount {
}
/**
* 刀:保持待机角,按目标左右翻转 weapon.scale.x(左=1,右=-1)。
* 刀/木:保持待机角,按目标左右翻转 weapon.scale.x(左=1,右=-1)。
* @param targetWorldX 目标世界 X
*/
aimByTargetX(targetWorldX: number): void {
if (!this._aim?.isValid) return;
if (this._type !== WEAPON_TYPE_SWORD) {
console.log('[HeroWeaponMount] aimByTargetX 仅刀', this._type, 'weaponId=', this._weaponId);
if (!this._isFlipType()) {
console.log('[HeroWeaponMount] aimByTargetX 仅刀/木', this._type, 'weaponId=', this._weaponId);
return;
}
this._aim.angle = WEAPON_REST_ANGLE;
@ -224,15 +296,21 @@ export class HeroWeaponMount {
return this._playNamed(name, false, timeScale, onComplete);
}
/** 播放武器待机(无则停在 setup) */
/** 播放武器待机(无「待机」则播预制体 defaultAnimation) */
playIdle(timeScale: number = 1): void {
this.hideBowAtk(true);
this.setArrowVisible(false);
const anim = this._skinAnim;
if (!anim?.isValid) return;
if (BattleSpinePlayback.play(anim, SPINE_IDLE, { loop: true, timeScale, optional: true })) return;
BattleSpinePlayback.clearPendingComplete(anim);
BattleSpinePlayback.resetTracks(anim);
const fallback = String(anim.animation ?? '').trim();
if (fallback && BattleSpinePlayback.play(anim, fallback, { loop: true, timeScale, optional: true })) {
if (fallback !== SPINE_IDLE) {
console.warn('[HeroWeaponMount] 无待机,改播预制体默认动画', fallback, 'type=', this._type);
}
return;
}
console.warn('[HeroWeaponMount] 待机未起播', this._type, this._weaponId, fallback);
}
/**
@ -502,6 +580,23 @@ export class HeroWeaponMount {
this._aim.setScale(x, this._aim.scale.y, this._aim.scale.z);
}
/** 刀、木:只左右翻转,不旋转 */
private _isFlipType(): boolean {
return this._type === WEAPON_TYPE_SWORD || this._type === WEAPON_TYPE_WOOD;
}
/**
* 技能动画 Spine:优先 weapon/anim 或根 anim,否则 weapon/skin。
* @param root 武器预制体根
*/
private _resolveAnimSpine(root: Node): sp.Skeleton | null {
const aim = this._aim;
const node = (aim?.isValid ? aim.getChildByName(ANIM_NODE) : null)
?? root.getChildByName(ANIM_NODE)
?? (aim?.isValid ? aim.getChildByName(SKIN_NODE) : null);
return node?.getComponent(sp.Skeleton) ?? null;
}
private _playNamed(
name: string,
loop: boolean,

@ -78,7 +78,7 @@ export class MapDragGuideHelper {
}
/**
* 引导加号语义:普通字仅空 Put 为 valid;蟠桃树空 Dig/Put 为 valid。
* 引导加号语义:五行字空 Put 为 valid;待解锁格须类型(0 任意)与显示等级都达标。
* 已有字块的格不显示 +(升级目标由 isBagDropLineTarget / hover 单独放行)。
*/
static getMapDragDropState(
@ -117,6 +117,9 @@ export class MapDragGuideHelper {
}
if (mg.waitingUnlock && !hasTile) {
if (!session.validator.isAllowedCampForPlace(gridIndex)) return 'blocked';
if (!source || !session.canUnlockArea(gridIndex, source.weapon2Id, source.level)) {
return 'none';
}
return 'valid';
}
if (mg.isCanWalk || mg.isCanBeDig) return 'blocked';

@ -22,6 +22,7 @@ import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spine
import { HERO_GLYPH_RESOURCES, isHeroActorWeapon1, YING_WEAPON1_ID } from '../heros/HeroActorRes';
import { aiPlaceCampForPlayer } from '../ai/runtime/AiCampArea';
import { HeroActorHost } from '../heros/HeroActorHost';
import { HeroActor } from '../heros/HeroActor';
import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
import {
applyGlyphSpineSkin,
@ -157,6 +158,7 @@ export interface TextGridData extends TileEntitySnapshot {
/**
* 背包/地图字块视图(textGrid 预制体)。
* 五行字:不显示 lv;按 Hero(herotype=Text.type, level=当前显示等级).number 打开同名节点,并切金/木/水/火/土皮。
* 地图激活后按当前 Hero.herotype 挂 weaponType*。
* 铲:显示「铲」节点;蟠桃树(102):显示「桃/桃树」Spine。
*/
@ccclass('TextGrid')
@ -1105,6 +1107,7 @@ export class TextGrid extends Component {
this._shovelDigScheduleGen++;
this._shovelDigComplete = null;
this._releaseYingCampDisplay();
this.releaseNumberSkill();
this.clearPositiveBuffFx();
this.clearSleepFx();
this.clearConfusionFx();
@ -1166,6 +1169,7 @@ export class TextGrid extends Component {
}
onDestroy(): void {
this.releaseNumberSkill();
this.clearPositiveBuffFx();
this.clearSleepFx();
this.clearConfusionFx();
@ -2329,10 +2333,12 @@ export class TextGrid extends Component {
this._hideLevelLabel();
if (!this._isElementNumberTile()) {
this._hideNumberNodes();
this._syncNumberSkill();
return;
}
const yingCover = !!this._yingCampDisplay?.isValid && this._yingCampDisplay.active;
this._applyNumberGlyph(!this._shouldHideGridSkin() && !yingCover);
this._syncNumberSkill();
}
/** 显示等级:以 setData 写入的 level 为准(与 tile / 组合数据同步) */
@ -2510,6 +2516,7 @@ export class TextGrid extends Component {
/** 地图字块已组合为攻击单位 → 激活;拆开或未组合 → 关闭;组合字切换 Spine 皮肤 */
private _syncSkillActiveState(): void {
this._syncShadow();
this._syncNumberSkill();
if (this._isPeachTreeTile()) return;
if (this._isShovelTile() && !this._isElementNumberTile()) return;
const composition = this._getComposition();
@ -2689,6 +2696,59 @@ export class TextGrid extends Component {
this._applySkinSpineIcon(this._skinIconName);
}
/**
* 地图五行数字挂 HeroActor:普攻走 Skill 表,武器播 Skill.animation。
* @param heroId Hero.id
* @param entityId 塔实体
*/
ensureNumberSkill(heroId: number, entityId: EntityId): Skill | null {
if (!this._isElementNumberTile() || this.location?.kind !== 'map') return null;
const hid = Math.floor(Number(heroId) || 0);
if (!(hid > 0)) return null;
let actor = this.getComponent(HeroActor);
if (!actor) actor = this.addComponent(HeroActor);
actor.setHeroId(hid);
actor.bindEntity(entityId);
if (!actor.isActive) {
actor.isActive = true;
actor.onUpdateActive(true);
} else if (!actor.isPresentationBusy()) {
actor.onIdle();
}
return actor;
}
/** 收回背包或换非五行字时拆掉数字 HeroActor */
releaseNumberSkill(): void {
const actor = this.getComponent(HeroActor);
if (!actor) return;
actor.isActive = false;
actor.onUpdateActive(false);
actor.visualChannel.release();
actor.destroy();
}
/** 地图激活数字:对账 HeroActor 与武器 */
private _syncNumberSkill(): void {
const onMap = this._isElementNumberTile() && this.location?.kind === 'map';
if (!onMap) {
this.releaseNumberSkill();
return;
}
const herotype = Math.floor(Number(this._entity?.tile?.type));
const hero = this._heroByTypeLevel(herotype, this._resolveDisplayLevel());
if (!hero) {
this.releaseNumberSkill();
return;
}
const loc = this.location;
const entityId = loc?.kind === 'map'
? TowerQueryService.instance.getRuntimeByGridIndex(loc.gridIndex)?.entityId ?? null
: null;
if (entityId == null) return;
this.ensureNumberSkill(hero.id, entityId);
}
/** 当前字块在地图上且属于有效组合(含单格 Weapon1) */
private _isMapComposedAttackUnit(): boolean {
const loc = this.location;

@ -5,8 +5,6 @@ import { ChapterLevelBuilder } from '../battle/config/ChapterLevelBuilder';
import {
appendBattleHeroDirLoads,
appendBattleMonsterDirLoad,
BATTLE_HERO_BUNDLE,
BATTLE_HERO_WEAPON_PATH,
} from '../battle/res/BattleHeroResCollector';
/** 战斗分包名(与 GBundle 一致,避免 ConfigRes 循环依赖) */
@ -92,7 +90,6 @@ export class ResPkgBuilder {
{ bundle: B.BattleGame, path: PrefabPath, name: '受击特效' },
{ bundle: B.BattleGame, path: PrefabPath, name: '受击刀痕' },
{ bundle: B.BattleGame, path: PrefabPath, name: '死亡' },
{ bundle: BATTLE_HERO_BUNDLE, path: BATTLE_HERO_WEAPON_PATH },
];
}

@ -69,6 +69,8 @@ import { BattleFirstGuidePresenter } from './BattleFirstGuidePresenter';
import { LangTxt } from 'db://assets/mx/module/lang/LangTxt';
import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
import { PerfSampler } from '../../game/PerfSampler';
import { isElementGlyphType, MergeEventType, type MergeEvent } from 'db://assets/mx/module/merge';
import { WeaponComposeConfigProvider } from '../../battle/bag/WeaponComposeConfigProvider';
const { ccclass } = _decorator;
@ -419,15 +421,41 @@ export class UIBattleGame extends Auto_BattleGame {
getBattleGameMonsterCrisisHintPresenter().bind(battle, map);
this._firstGuide.bind(this.ui_guide, battle.bagSession);
PerfSampler.beginBattle();
this._listenFirstPlaceCombat();
this._refreshWaveUi();
}
if (!this._combatStarted) {
this._combatStarted = true;
gg.battle.startCombat();
this._refreshWaveUi();
/** 开局不立刻出怪:等本机玩家第一次把数字放到地图后再 startCombat */
private _listenFirstPlaceCombat(): void {
this._unbindFirstPlaceCombat();
const bag = this._battle?.bagSession;
if (!bag) {
console.error('[UIBattleGame] 无背包会话,无法按首次放字开怪');
return;
}
this._refreshWaveUi();
bag.on(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced);
}
/** 卸掉首次放字开怪监听 */
private _unbindFirstPlaceCombat(): void {
this._battle?.bagSession?.off(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced);
}
/**
* 本机首次放下五行数字后开波。
* @param event 地图落子事件
*/
private readonly _onFirstPlayerNumberPlaced = (event: MergeEvent): void => {
if (this._combatStarted) return;
if (event.type !== MergeEventType.TilePlaced) return;
const tileType = Math.floor(Number(WeaponComposeConfigProvider.instance.getWeapon2Type(event.weapon2Id)));
if (!isElementGlyphType(tileType)) return;
this._unbindFirstPlaceCombat();
this._combatStarted = true;
gg.battle.startCombat();
this._refreshWaveUi();
};
/** 绑定 HitAfter/Buff/死亡等战斗事件(须在 monsterPresenter 之后) */
private _bindBattleEvents(battle: ProjectBattleHandle): void {
if (this._onBattleEventBound) {
@ -1139,6 +1167,7 @@ export class UIBattleGame extends Auto_BattleGame {
this._passiveSkillView = null;
this._otherSkillView = null;
this._otherOwnedDesc = null;
this._unbindFirstPlaceCombat();
this._firstGuide.unbind();
this._bagView?.unbind();
this._bagView = null;

@ -103,7 +103,7 @@ UIPanelHome ← pickRandomBattleLevelId 进关
2. `GameController.enterBattle` → 预载 → `gg.battle.start`
3. `ChapterLevelBuilder.build` → `LevelWaveParser.toWaveDefs`
4. `UIBattleGame` → `loadMap` → `applyMapTopologyToSession(..., 'pvp')`
5. `CocosBattleMonsterPresenter.bind` + `startCombat` → 双口出怪 + 寻路表现
5. `CocosBattleMonsterPresenter.bind`;**本机玩家第一次把数字放到地图后** `startCombat` → 双口出怪 + 寻路表现
## 4. 默认参数

Loading…
Cancel
Save