master
chenanghui 2 weeks ago
parent 6b978c775e
commit 12260863b4
  1. 1776
      assets/bundles/battleGame/prefab/BattleGame.prefab
  2. 146
      assets/bundles/battleGame/prefab/bagGrid.prefab
  3. 8416
      assets/bundles/battleMap/prefab/地图/mapLayer1.prefab
  4. 172
      assets/bundles/battleMap/prefab/地图/mapLayer2.prefab
  5. 172
      assets/bundles/battleMap/prefab/地图/mapLayer3.prefab
  6. 172
      assets/bundles/battleMap/prefab/地图/mapLayer4.prefab
  7. 172
      assets/bundles/battleMap/prefab/地图/mapLayer5.prefab
  8. 185
      assets/bundles/first/config/Level.json
  9. 4088
      assets/bundles/first/config/Skill.json
  10. 42
      assets/bundles/first/config/Text.json
  11. 2
      assets/mx/module/merge/config/IComposeConfigProvider.ts
  12. 61
      assets/mx/module/merge/core/MergeSession.ts
  13. BIN
      assets/res/battleGame/texture/ui/缚仙网.png
  14. 134
      assets/res/battleGame/texture/ui/缚仙网.png.meta
  15. 2
      assets/res/font/str.txt
  16. 213
      assets/script/battle/bag/BattleBagSession.ts
  17. 2
      assets/script/battle/bag/KeepHeroClearRecord.ts
  18. 121
      assets/script/battle/bag/PlayerBagTextPool.ts
  19. 21
      assets/script/battle/bag/WeaponComposeConfigProvider.ts
  20. 17
      assets/script/battle/config/ChapterLevelBuilder.ts
  21. 288
      assets/script/battle/view/CocosBagBoardView.ts
  22. 263
      assets/script/battle/view/TextGrid.ts
  23. 198
      assets/script/battle/view/TextListLandHost.ts
  24. 16
      assets/script/interface/AutoTable.ts
  25. 21
      assets/script/ui/BattleGame/UIBattleGame.ts
  26. 9
      assets/script/ui/grid17.meta
  27. 33
      assets/script/ui/grid17/Auto_grid17.ts
  28. 9
      assets/script/ui/grid17/Auto_grid17.ts.meta
  29. 30
      assets/script/ui/grid17/UIgrid17.ts
  30. 9
      assets/script/ui/grid17/UIgrid17.ts.meta

File diff suppressed because it is too large Load Diff

@ -29,16 +29,19 @@
},
{
"__id__": 20
},
{
"__id__": 26
}
],
"_active": true,
"_components": [
{
"__id__": 26
"__id__": 32
}
],
"_prefab": {
"__id__": 28
"__id__": 34
},
"_lpos": {
"__type__": "cc.Vec3",
@ -613,6 +616,142 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "待开启",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 27
},
{
"__id__": 29
}
],
"_prefab": {
"__id__": 31
},
"_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.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 26
},
"_enabled": true,
"__prefab": {
"__id__": 28
},
"_contentSize": {
"__type__": "cc.Size",
"width": 80,
"height": 80
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "d9Nn96HlRLTqhdrCX/1tNV"
},
{
"__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__": "ab20f802-a0e9-498a-8847-b8ef7686abfa@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "8dbdH0nnlGGYKzv7efYea0"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "bbzuKvaCJNCrSf4IjfOz70",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
@ -623,7 +762,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 27
"__id__": 33
},
"_contentSize": {
"__type__": "cc.Size",
@ -650,6 +789,7 @@
"__id__": 0
},
"fileId": "6bd0awS7RFtK42rXtq0akC",
"instance": null,
"targetOverrides": null
}
]

File diff suppressed because it is too large Load Diff

@ -35,19 +35,22 @@
},
{
"__id__": 2442
},
{
"__id__": 2456
}
],
"_active": true,
"_components": [
{
"__id__": 2456
"__id__": 2462
},
{
"__id__": 2458
"__id__": 2464
}
],
"_prefab": {
"__id__": 2460
"__id__": 2466
},
"_lpos": {
"__type__": "cc.Vec3",
@ -33312,6 +33315,165 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 2457
},
{
"__id__": 2459
}
],
"_prefab": {
"__id__": 2461
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -15.919,
"y": 756.136,
"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__": 2456
},
"_enabled": true,
"__prefab": {
"__id__": 2458
},
"_contentSize": {
"__type__": "cc.Size",
"width": 160,
"height": 50.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "caezd9a6RGkrvwr11WQ04R"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2456
},
"_enabled": true,
"__prefab": {
"__id__": 2460
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 0,
"b": 0,
"a": 255
},
"_string": "木系地图",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 40,
"_fontSize": 40,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "92owF5oT1NBb3oKKjfd5mJ"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "1b8GogMMZCEYV4r7v1jBhf",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
@ -33322,7 +33484,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 2457
"__id__": 2463
},
"_contentSize": {
"__type__": "cc.Size",
@ -33350,7 +33512,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 2459
"__id__": 2465
},
"mapRows": 13,
"mapCols": 8,

@ -35,19 +35,22 @@
},
{
"__id__": 2442
},
{
"__id__": 2456
}
],
"_active": true,
"_components": [
{
"__id__": 2456
"__id__": 2462
},
{
"__id__": 2458
"__id__": 2464
}
],
"_prefab": {
"__id__": 2460
"__id__": 2466
},
"_lpos": {
"__type__": "cc.Vec3",
@ -33312,6 +33315,165 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 2457
},
{
"__id__": 2459
}
],
"_prefab": {
"__id__": 2461
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -15.919,
"y": 756.136,
"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__": 2456
},
"_enabled": true,
"__prefab": {
"__id__": 2458
},
"_contentSize": {
"__type__": "cc.Size",
"width": 160,
"height": 50.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "2cwjqoab1Il7rrErj7Dj57"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2456
},
"_enabled": true,
"__prefab": {
"__id__": 2460
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 0,
"b": 0,
"a": 255
},
"_string": "水系地图",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 40,
"_fontSize": 40,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "c9KJhNnklEjIVRztfPxOus"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "5avYpyiUFDq70NWbFuRiHx",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
@ -33322,7 +33484,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 2457
"__id__": 2463
},
"_contentSize": {
"__type__": "cc.Size",
@ -33350,7 +33512,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 2459
"__id__": 2465
},
"mapRows": 13,
"mapCols": 8,

@ -35,19 +35,22 @@
},
{
"__id__": 2442
},
{
"__id__": 2456
}
],
"_active": true,
"_components": [
{
"__id__": 2456
"__id__": 2462
},
{
"__id__": 2458
"__id__": 2464
}
],
"_prefab": {
"__id__": 2460
"__id__": 2466
},
"_lpos": {
"__type__": "cc.Vec3",
@ -33312,6 +33315,165 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 2457
},
{
"__id__": 2459
}
],
"_prefab": {
"__id__": 2461
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -15.919,
"y": 756.136,
"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__": 2456
},
"_enabled": true,
"__prefab": {
"__id__": 2458
},
"_contentSize": {
"__type__": "cc.Size",
"width": 160,
"height": 50.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "4bMcm43DJANZvdfRhP3/cW"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2456
},
"_enabled": true,
"__prefab": {
"__id__": 2460
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 0,
"b": 0,
"a": 255
},
"_string": "火系地图",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 40,
"_fontSize": 40,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "e9vnBL0yRB76gehxQQc1kQ"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "a65kJsXalLaalMRVeTz5bz",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
@ -33322,7 +33484,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 2457
"__id__": 2463
},
"_contentSize": {
"__type__": "cc.Size",
@ -33350,7 +33512,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 2459
"__id__": 2465
},
"mapRows": 13,
"mapCols": 8,

@ -35,19 +35,22 @@
},
{
"__id__": 2442
},
{
"__id__": 2456
}
],
"_active": true,
"_components": [
{
"__id__": 2456
"__id__": 2462
},
{
"__id__": 2458
"__id__": 2464
}
],
"_prefab": {
"__id__": 2460
"__id__": 2466
},
"_lpos": {
"__type__": "cc.Vec3",
@ -33312,6 +33315,165 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 2457
},
{
"__id__": 2459
}
],
"_prefab": {
"__id__": 2461
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -15.919,
"y": 756.136,
"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__": 2456
},
"_enabled": true,
"__prefab": {
"__id__": 2458
},
"_contentSize": {
"__type__": "cc.Size",
"width": 160,
"height": 50.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "a70VvsYK1AM4I+g/cOR0h8"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2456
},
"_enabled": true,
"__prefab": {
"__id__": 2460
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 0,
"b": 0,
"a": 255
},
"_string": "土系地图",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 40,
"_fontSize": 40,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "8d062c72xLvYDGycnPc/rC"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "e6nDET2w1EHY1dfruYTDZa",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
@ -33322,7 +33484,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 2457
"__id__": 2463
},
"_contentSize": {
"__type__": "cc.Size",
@ -33350,7 +33512,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 2459
"__id__": 2465
},
"mapRows": 13,
"mapCols": 8,

@ -1,181 +1,6 @@
[
{
"id": 1,
"name": "花果山",
"desc": "",
"map": "mapLayer1",
"startnumber": 20,
"diffcult": 1,
"reward": "1|20",
"loserreward": "1|5",
"monster": "1,101,10,1.6|2,101,11,1.6|3,101,12,1.6|4,101,13,1.6|5,101,15,1.6|6,101,16,1.6|7,101,18,1.6|8,101,19,1.6|9,101,21,1.6|10,101,24,1.6|11,101,26,1.6|12,101,29,1.6|13,101,30,1.6|14,101,35,1.6|15,101,38,1.6|16,101,42,1.6|17,101,45,1.6|18,101,50,1.6|19,101,50,1.6|20,101,50,1.6|21,101,50,1.6|22,101,50,1.6|23,101,50,1.6|24,101,50,1.6|25,101,50,1.6",
"boss": "3|105,6|106,9|107,12|105,15|106,18|107,21|106,24|107",
"bosspro": 1200,
"unlockland1": "",
"unlockland2": "",
"unlockland3": "",
"unlockland4": "",
"unlockland5": "",
"unlockland6": "",
"normalland": [],
"tolockland": [],
"startmonster": "猴",
"startdistance": 4,
"startmovetime": 2,
"starttalk": "孩儿们,随我冲!|猴儿们,列阵!今日杀个痛快!|我花果山,岂容他猴放肆!|犯我花果山,虽远必诛!|石猴一脉,无所畏惧,全军压上!"
},
{
"id": 2,
"name": "东海龙宫",
"desc": "",
"map": "mapLayer2",
"startnumber": 20,
"diffcult": 1,
"reward": "1|20",
"loserreward": "1|5",
"monster": "1,201,10,1.6|2,201,11,1.6|3,201,12,1.6|4,201,13,1.6|5,201,15,1.6|6,201,16,1.6|7,201,18,1.6|8,201,19,1.6|9,201,21,1.6|10,201,24,1.6|11,201,26,1.6|12,201,29,1.6|13,201,30,1.6|14,201,35,1.6|15,201,38,1.6|16,201,42,1.6|17,201,45,1.6|18,201,50,1.6|19,201,50,1.6|20,201,50,1.6|21,201,50,1.6|22,201,50,1.6|23,201,50,1.6|24,201,50,1.6|25,201,50,1.6",
"boss": "3|205,6|206,9|207,12|205,15|206,18|207,21|206,24|207",
"bosspro": 1200,
"unlockland1": "",
"unlockland2": "",
"unlockland3": "",
"unlockland4": "",
"unlockland5": "",
"unlockland6": "",
"normalland": [],
"tolockland": [],
"startmonster": "虾",
"startdistance": 4,
"startmovetime": 2,
"starttalk": "虾兵听令!随本王杀出水晶宫!|众虾列阵,向前冲锋!|四海归我,碧波为甲,虾兵齐进!|水晶宫不容侵扰,击溃来敌!|擎浪御敌,捍卫龙宫,众虾向前!"
},
{
"id": 3,
"name": "幽冥地府",
"desc": "",
"map": "mapLayer3",
"startnumber": 20,
"diffcult": 1,
"reward": "1|20",
"loserreward": "1|5",
"monster": "1,301,10,1.6|2,301,11,1.6|3,301,12,1.6|4,301,13,1.6|5,301,15,1.6|6,301,16,1.6|7,301,18,1.6|8,301,19,1.6|9,301,21,1.6|10,301,24,1.6|11,301,26,1.6|12,301,29,1.6|13,301,30,1.6|14,301,35,1.6|15,301,38,1.6|16,301,42,1.6|17,301,45,1.6|18,301,50,1.6|19,301,50,1.6|20,301,50,1.6|21,301,50,1.6|22,301,50,1.6|23,301,50,1.6|24,301,50,1.6|25,301,50,1.6",
"boss": "3|305,6|306,9|307,12|305,15|306,18|307,21|306,24|307",
"bosspro": 1200,
"unlockland1": "",
"unlockland2": "",
"unlockland3": "",
"unlockland4": "",
"unlockland5": "",
"unlockland6": "",
"normalland": [],
"tolockland": [],
"startmonster": "魂",
"startdistance": 4,
"startmovetime": 2,
"starttalk": "踏出幽冥,向前冲杀!|阴魂列阵,讨伐来敌!|黄泉为路,幽冥作甲,众魂出击!|跨过奈何,忘却轮回,杀!|阴风吹彻,冥火随行,出击!"
},
{
"id": 4,
"name": "天庭",
"desc": "",
"map": "mapLayer4",
"startnumber": 20,
"diffcult": 1,
"reward": "1|20",
"loserreward": "1|5",
"monster": "1,401,10,1.6|2,401,11,1.6|3,401,12,1.6|4,401,13,1.6|5,401,15,1.6|6,401,16,1.6|7,401,18,1.6|8,401,19,1.6|9,401,21,1.6|10,401,24,1.6|11,401,26,1.6|12,401,29,1.6|13,401,30,1.6|14,401,35,1.6|15,401,38,1.6|16,401,42,1.6|17,401,45,1.6|18,401,50,1.6|19,401,50,1.6|20,401,50,1.6|21,401,50,1.6|22,401,50,1.6|23,401,50,1.6|24,401,50,1.6|25,401,50,1.6",
"boss": "3|405,6|406,9|407,12|405,15|406,18|407,21|406,24|407",
"bosspro": 1200,
"unlockland1": "",
"unlockland2": "",
"unlockland3": "",
"unlockland4": "",
"unlockland5": "",
"unlockland6": "",
"normalland": [],
"tolockland": [],
"startmonster": "兵",
"startdistance": 2,
"startmovetime": 1,
"starttalk": "仙旗高扬,战鼓雷鸣,冲锋!|宵小之辈,速速受缚!|执掌九霄秩序,挥师出征!|天道威严不可辱,众天兵,杀穿他们!|九天天兵,随我冲锋!"
},
{
"id": 5,
"name": "花果山",
"desc": "",
"map": "mapLayer1",
"startnumber": 20,
"diffcult": 1,
"reward": "1|20",
"loserreward": "1|5",
"monster": "1,101,10,1.6|2,101,11,1.6|3,101,12,1.6|4,101,13,1.6|5,101,15,1.6|6,101,16,1.6|7,101,18,1.6|8,101,19,1.6|9,101,21,1.6|10,101,24,1.6|11,101,26,1.6|12,101,29,1.6|13,101,30,1.6|14,101,35,1.6|15,101,38,1.6|16,101,42,1.6|17,101,45,1.6|18,101,50,1.6|19,101,50,1.6|20,101,50,1.6|21,101,50,1.6|22,101,50,1.6|23,101,50,1.6|24,101,50,1.6|25,101,50,1.6",
"boss": "3|105,6|106,9|107,12|105,15|106,18|107,21|106,24|107",
"bosspro": 1200,
"unlockland1": "",
"unlockland2": "",
"unlockland3": "",
"unlockland4": "",
"unlockland5": "",
"unlockland6": "",
"normalland": [],
"tolockland": [],
"startmonster": "猴",
"startdistance": 4,
"startmovetime": 2,
"starttalk": "孩儿们,随我冲!|猴儿们,列阵!今日杀个痛快!|我花果山,岂容他猴放肆!|犯我花果山,虽远必诛!|石猴一脉,无所畏惧,全军压上!"
},
{
"id": 6,
"name": "东海龙宫",
"desc": "",
"map": "mapLayer2",
"startnumber": 20,
"diffcult": 1,
"reward": "1|20",
"loserreward": "1|5",
"monster": "1,201,10,1.6|2,201,11,1.6|3,201,12,1.6|4,201,13,1.6|5,201,15,1.6|6,201,16,1.6|7,201,18,1.6|8,201,19,1.6|9,201,21,1.6|10,201,24,1.6|11,201,26,1.6|12,201,29,1.6|13,201,30,1.6|14,201,35,1.6|15,201,38,1.6|16,201,42,1.6|17,201,45,1.6|18,201,50,1.6|19,201,50,1.6|20,201,50,1.6|21,201,50,1.6|22,201,50,1.6|23,201,50,1.6|24,201,50,1.6|25,201,50,1.6",
"boss": "3|205,6|206,9|207,12|205,15|206,18|207,21|206,24|207",
"bosspro": 1200,
"unlockland1": "",
"unlockland2": "",
"unlockland3": "",
"unlockland4": "",
"unlockland5": "",
"unlockland6": "",
"normalland": [],
"tolockland": [],
"startmonster": "虾",
"startdistance": 4,
"startmovetime": 2,
"starttalk": "虾兵听令!随本王杀出水晶宫!|众虾列阵,向前冲锋!|四海归我,碧波为甲,虾兵齐进!|水晶宫不容侵扰,击溃来敌!|擎浪御敌,捍卫龙宫,众虾向前!"
},
{
"id": 7,
"name": "幽冥地府",
"desc": "",
"map": "mapLayer3",
"startnumber": 20,
"diffcult": 1,
"reward": "1|20",
"loserreward": "1|5",
"monster": "1,301,10,1.6|2,301,11,1.6|3,301,12,1.6|4,301,13,1.6|5,301,15,1.6|6,301,16,1.6|7,301,18,1.6|8,301,19,1.6|9,301,21,1.6|10,301,24,1.6|11,301,26,1.6|12,301,29,1.6|13,301,30,1.6|14,301,35,1.6|15,301,38,1.6|16,301,42,1.6|17,301,45,1.6|18,301,50,1.6|19,301,50,1.6|20,301,50,1.6|21,301,50,1.6|22,301,50,1.6|23,301,50,1.6|24,301,50,1.6|25,301,50,1.6",
"boss": "3|305,6|306,9|307,12|305,15|306,18|307,21|306,24|307",
"bosspro": 1200,
"unlockland1": "",
"unlockland2": "",
"unlockland3": "",
"unlockland4": "",
"unlockland5": "",
"unlockland6": "",
"normalland": [],
"tolockland": [],
"startmonster": "魂",
"startdistance": 4,
"startmovetime": 2,
"starttalk": "踏出幽冥,向前冲杀!|阴魂列阵,讨伐来敌!|黄泉为路,幽冥作甲,众魂出击!|跨过奈何,忘却轮回,杀!|阴风吹彻,冥火随行,出击!"
},
{
"id": 11,
"name": "金系地图",
"desc": "",
"map": "mapLayer1",
@ -211,7 +36,7 @@
"starttalk": ""
},
{
"id": 12,
"id": 2,
"name": "木系地图",
"desc": "",
"map": "mapLayer2",
@ -247,7 +72,7 @@
"starttalk": ""
},
{
"id": 13,
"id": 3,
"name": "水系地图",
"desc": "",
"map": "mapLayer3",
@ -283,7 +108,7 @@
"starttalk": ""
},
{
"id": 14,
"id": 4,
"name": "火系地图",
"desc": "",
"map": "mapLayer4",
@ -319,10 +144,10 @@
"starttalk": ""
},
{
"id": 15,
"id": 5,
"name": "土系地图",
"desc": "",
"map": "mapLayer1",
"map": "mapLayer5",
"startnumber": 20,
"diffcult": 1,
"reward": "",

File diff suppressed because it is too large Load Diff

@ -32,7 +32,7 @@
"id": 1002,
"type": 1,
"level": 2,
"weight": 30,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -61,7 +61,7 @@
"id": 1003,
"type": 1,
"level": 3,
"weight": 10,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -90,7 +90,7 @@
"id": 1004,
"type": 1,
"level": 4,
"weight": 4,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -119,7 +119,7 @@
"id": 1005,
"type": 2,
"level": 1,
"weight": 55,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -148,7 +148,7 @@
"id": 1006,
"type": 2,
"level": 2,
"weight": 30,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -177,7 +177,7 @@
"id": 1007,
"type": 2,
"level": 3,
"weight": 10,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -206,7 +206,7 @@
"id": 1008,
"type": 2,
"level": 4,
"weight": 4,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -235,7 +235,7 @@
"id": 1009,
"type": 3,
"level": 1,
"weight": 55,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -264,7 +264,7 @@
"id": 1010,
"type": 3,
"level": 2,
"weight": 30,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -293,7 +293,7 @@
"id": 1011,
"type": 3,
"level": 3,
"weight": 10,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -322,7 +322,7 @@
"id": 1012,
"type": 3,
"level": 4,
"weight": 4,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -351,7 +351,7 @@
"id": 1013,
"type": 4,
"level": 1,
"weight": 55,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -380,7 +380,7 @@
"id": 1014,
"type": 4,
"level": 2,
"weight": 30,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -409,7 +409,7 @@
"id": 1015,
"type": 4,
"level": 3,
"weight": 10,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -438,7 +438,7 @@
"id": 1016,
"type": 4,
"level": 4,
"weight": 4,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -467,7 +467,7 @@
"id": 1017,
"type": 5,
"level": 1,
"weight": 55,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -496,7 +496,7 @@
"id": 1018,
"type": 5,
"level": 2,
"weight": 30,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -525,7 +525,7 @@
"id": 1019,
"type": 5,
"level": 3,
"weight": 10,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -554,7 +554,7 @@
"id": 1020,
"type": 5,
"level": 4,
"weight": 4,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -583,7 +583,7 @@
"id": 2001,
"type": 10,
"level": 0,
"weight": 3,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",
@ -612,7 +612,7 @@
"id": 2002,
"type": 11,
"level": 0,
"weight": 2,
"weight": 0,
"group": "",
"quality": 0,
"icon": "",

@ -12,4 +12,6 @@ export interface IComposeConfigProvider {
passiveSpawnKeepsDigCell(weapon2Id: number): boolean;
/** 保持 Dig 格的字块(蟠桃树)可合成等级上限 */
getKeepDigTileMaxLevel(weapon2Id: number): number;
/** 五行 Text.type 可合成到的最高显示等级(Hero.level 最大值) */
getElementMaxLevel(textType: number): number;
}

@ -148,13 +148,13 @@ export class MergeSession {
* 指定背包槽放入 weapon2(槽位须为空)。
* @returns null 表示成功
*/
grantWeaponInBagSlot(slotIndex: number, weapon2Id: number): PlaceRejectReason | null {
grantWeaponInBagSlot(slotIndex: number, weapon2Id: number, level?: number): PlaceRejectReason | null {
if (!this._prepActive) return PlaceRejectReason.NotPrepPhase;
if (slotIndex < 0 || slotIndex >= this.bag.slotCount) {
return PlaceRejectReason.InvalidMerge;
}
if (this.bag.getSlot(slotIndex)) return PlaceRejectReason.BagFull;
this.bag.setSlot(slotIndex, { weapon2Id });
this.bag.setSlot(slotIndex, level != null ? { weapon2Id, level } : { weapon2Id });
this.emit(MergeEventFactory.bagRefreshed(false));
return null;
}
@ -354,6 +354,34 @@ export class MergeSession {
return PlaceRejectReason.WrongCellKind;
}
/**
* 五行同 Text.type、同显示等级:目标格等级 +1。
* 不是五行对,或类型/等级不同,返回 skip。
*/
private _mergeElementOntoBag(
source: { weapon2Id: number; level?: number },
toSlot: number,
target: BagSlotRecord,
clearSource: () => void,
): PlaceRejectReason | null | 'skip' {
const srcType = Math.floor(this._config.getWeapon2Type(source.weapon2Id));
const dstType = Math.floor(this._config.getWeapon2Type(target.weapon2Id));
if (srcType < 1 || srcType > 5 || dstType < 1 || dstType > 5) return 'skip';
const srcLevel = Math.floor(source.level ?? 1);
const dstLevel = Math.floor(target.level ?? 1);
if (srcType !== dstType || srcLevel !== dstLevel) return 'skip';
const max = this._config.getElementMaxLevel(srcType);
if (srcLevel >= max) return PlaceRejectReason.MaxLevel;
const newLevel = srcLevel + 1;
clearSource();
this.bag.setSlot(toSlot, { ...target, level: newLevel });
const weapon1Id = target.weapon1Id
?? this._config.getWeapon1ByGroupSequence([target.weapon2Id])
?? 0;
this.emit(MergeEventFactory.bagMerged(toSlot, weapon1Id, newLevel));
return null;
}
private _dropBagToBag(fromSlot: number, toSlot: number): PlaceRejectReason | null {
if (fromSlot === toSlot) return null;
@ -366,6 +394,10 @@ export class MergeSession {
this.emit(MergeEventFactory.bagSlotMoved(fromSlot, toSlot));
return null;
}
const element = this._mergeElementOntoBag(src, toSlot, dst, () => {
this.bag.clearSlot(fromSlot);
});
if (element !== 'skip') return element;
if (this._config.passiveSpawnKeepsDigCell(src.weapon2Id)
&& this._config.passiveSpawnKeepsDigCell(dst.weapon2Id)) {
const merged = this._tryKeepDigBagBagMerge(fromSlot, toSlot, src, dst);
@ -558,6 +590,13 @@ export class MergeSession {
}
if (bagSlot) {
const element = this._mergeElementOntoBag(tile, toSlot, bagSlot, () => {
this.map.removeTile(fromGrid);
this.map.setCellKind(fromGrid, cellKindAfterReturnToBag());
this._recompose();
});
if (element !== 'skip') return element;
const merged = this._tryMapBagSingleMerge(fromGrid, toSlot, tile, bagSlot);
if (merged === null) return null;
if (merged !== 'skip') return merged;
@ -1638,6 +1677,24 @@ export class MergeSession {
return null;
}
/**
* 拖到空装饰格:删掉该地图字。type3 已在拖起时撤销,这里只清挂起,不再还原。
* @param gridIndex 来源地图格
*/
discardDraggedMapGlyph(gridIndex: number): boolean {
const tile = this.map.getTile(gridIndex);
this._pendingDragTile = null;
if (!tile) {
console.log('[MergeSession] 地图格无字可删', gridIndex);
return false;
}
this.map.removeTile(gridIndex);
this.map.setCellKind(gridIndex, cellKindAfterReturnToBag());
this._recompose();
this.emit(MergeEventFactory.composeChanged(this._compositions));
return true;
}
/**
* 技能损毁:移除单格字块(不限 Weapon2 类型;多格组合免疫)。
* @returns null 表示成功

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "ab20f802-a0e9-498a-8847-b8ef7686abfa",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "ab20f802-a0e9-498a-8847-b8ef7686abfa@6c48a",
"displayName": "缚仙网",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "ab20f802-a0e9-498a-8847-b8ef7686abfa",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "ab20f802-a0e9-498a-8847-b8ef7686abfa@f9941",
"displayName": "缚仙网",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0.5,
"trimX": 22,
"trimY": 26,
"width": 156,
"height": 147,
"rawWidth": 200,
"rawHeight": 200,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-78,
-73.5,
0,
78,
-73.5,
0,
-78,
73.5,
0,
78,
73.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
22,
174,
178,
174,
22,
27,
178,
27
],
"nuv": [
0.11,
0.135,
0.89,
0.135,
0.11,
0.87,
0.89,
0.87
],
"minPos": [
-78,
-73.5,
0
],
"maxPos": [
78,
73.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "ab20f802-a0e9-498a-8847-b8ef7686abfa@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "ab20f802-a0e9-498a-8847-b8ef7686abfa@6c48a"
}
}

File diff suppressed because one or more lines are too long

@ -22,6 +22,7 @@ import type { MapGrid } from '../map/legacy/MapGrid';
import {
formatBagRollEntries,
formatIdList,
formatWeapon2Brief,
logBagRefreshDebug,
logBagRefreshSection,
} from '../debug/BagRefreshDebugLog';
@ -31,7 +32,7 @@ import {
} from './BagPrepConfig';
import { MapPlaceValidator } from './MapPlaceValidator';
import { MapDragGuideHelper } from '../view/MapDragGuideHelper';
import { FIXED_MAP_LEVEL_ID } from '../config/ChapterLevelBuilder';
import { TextListCellState } from '../view/TextListLandHost';
import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider';
import { isAttackGlyph } from '../config/BattleHeroTable';
import { ComboLosePity, FirstBattleHeroPity } from './BagHeroRefreshAlgorithm';
@ -51,6 +52,9 @@ import { getTowerLevelDownHost } from '../combat/TowerLevelDownHost';
import { getTowerRuinHost } from '../combat/TowerRuinHost';
import { getTowerExpHost } from '../combat/TowerExpHost';
import { findEntityIdByGridIndex } from '../runtime/DeployEntityQuery';
/** 召唤按钮一次最多补的空开启格数量 */
const REFRESH_EMPTY_MAX = 5;
/** 项目层背包会话:桥接 MergeSession 与 MapManager */
export class BattleBagSession {
readonly merge: MergeSession;
@ -66,16 +70,20 @@ export class BattleBagSession {
private readonly _ownerPlayerId: number;
/** 本局已成功普通刷新次数(用于刷新消耗递增) */
private _normalRefreshUsed = 0;
/** 最近一次空格刷新写入的槽下标 */
private _lastFilledSlots: number[] = [];
/** PopGM 预设 Weapon1 → Weapon2 顺序队列(每次刷新从队首取,取完走正常逻辑) */
private _gmWeaponPresetQueue: number[] = [];
/** 视图层将自行播放 openSpine 的地图格(null=无) */
private _deferOpenFxGridIndex: number | null = null;
/** 槽位地块状态查询(视图绑定 ui_textList 后注入);null 视为全部开启 */
landStateOf: ((slotIndex: number) => TextListCellState) | null = null;
/** dropLogical(AI)跳过开挖 openSpine,只切格态 */
private _skipOpenEffect = false;
/** 本局携带被动:召唤消耗降低万分比(functiontype=26,开局快照) */
private readonly _summonCostReduceWan: number;
constructor(map: MapManager, _chapterId: number, session?: import('db://assets/mx/module/battle').BattleSession, bagSlotCount = 5, opts?: {
constructor(map: MapManager, chapterId: number, session?: import('db://assets/mx/module/battle').BattleSession, bagSlotCount = 5, opts?: {
/** 字池所用段位 id;缺省玩家当前段位 */
rankId?: number;
/** 扣桃所属玩家槽;缺省 0(本机) */
@ -93,7 +101,7 @@ export class BattleBagSession {
const rankId = opts?.rankId != null
? Math.floor(Number(opts.rankId) || 0)
: RankDataManager.getPlayerRankId();
this._bindUnlockLevel(FIXED_MAP_LEVEL_ID, rankId);
this._bindUnlockLevel(chapterId, rankId);
// 携带被动按 ownerPlayerId:本机 DailyShop,AI 读 CarryStore
if (this._ownerPlayerId === 0) {
const heroSummonAdjust = resolveCarriedHeroSummonRatePoolAdjust(DailyShopDataManager.getOwnedPassive());
@ -128,8 +136,8 @@ export class BattleBagSession {
this._initMapCells();
this._map.areaUnlock.sync(this._map.mapGrids);
// 开局不自动 roll,玩家手动点刷新
this.merge.beginPrepRound();
// 开局字块由视图绑定地块状态后调用 autoFillOpenSlots:只填 tolockland
}
/** 刷新扣桃所属 playerId */
@ -273,11 +281,82 @@ export class BattleBagSession {
if (firstRefresh) this._textPool.shuffleEntries(slots);
}
logBagRefreshDebug('最终背包', formatBagRollEntries(slots, config));
// 按槽位地块状态修正:未解锁槽留空(退还),待解锁槽换成 type1-5 抽取
for (let i = 0; i < slots.length; i++) {
const rec = slots[i];
if (!rec) continue;
const state = this.landStateOf?.(i) ?? TextListCellState.Open;
if (state === TextListCellState.Locked || state === TextListCellState.Blank) {
this._textPool.tryRefundWeapon2Id(rec.weapon2Id);
delete slots[i];
continue;
}
if (state === TextListCellState.OpenNoDrag) {
const roll = this._textPool.rollOneForLand(true);
if (roll != null && roll.weapon2Id !== rec.weapon2Id) {
this._textPool.tryRefundWeapon2Id(rec.weapon2Id);
slots[i] = { weapon2Id: roll.weapon2Id, level: roll.level };
}
}
}
logBagRefreshDebug('最终背包', slots.map((e, i) => `${i}:${e ? formatWeapon2Brief(e.weapon2Id, config) : '空'}`).join(', '));
logBagRefreshSection('背包普通刷新结束');
this.merge.refreshBag(slots);
}
/**
* 开局只给 tolockland(待解锁)填随机字。
* normalland 开启但留空;未解锁与 grid17 不填。不占刷新次数;仅本机玩家背包。
*/
autoFillOpenSlots(): void {
if (this._ownerPlayerId !== 0) return;
if (!this.landStateOf) {
console.log('[BattleBagSession] 未绑定地块状态,跳过开局填字');
return;
}
const slotCount = this.merge.bag.slotCount;
const filled: string[] = [];
for (let i = 0; i < slotCount; i++) {
if (this.merge.bag.getSlot(i)) continue;
if (this.landStateOf(i) !== TextListCellState.OpenNoDrag) continue;
const roll = this._textPool.rollOneForLand(true);
if (roll == null) continue;
this.merge.bag.setSlot(i, { weapon2Id: roll.weapon2Id, level: roll.level });
filled.push(`${i}:${roll.weapon2Id}(Lv${roll.level})`);
}
console.log(`[BattleBagSession] 开局填充槽位 ${filled.length ? filled.join(', ') : '无'} slotCount=${slotCount} 池就绪=${this._textPool.ready}`);
}
/**
* 新的待开启格补一个五行随机字(不广播整包刷新)。
* @param slotIndex 待开启槽下标
* @returns 是否写入成功
*/
fillGateUnlockSlot(slotIndex: number): boolean {
if (this.landStateOf?.(slotIndex) !== TextListCellState.OpenNoDrag) {
console.log('[BattleBagSession] 非待开启格不出字', slotIndex);
return false;
}
const roll = this._textPool.rollOneForLand(true);
if (roll == null) {
console.log('[BattleBagSession] 待开启格字池抽空', slotIndex);
return false;
}
this.merge.bag.setSlot(slotIndex, { weapon2Id: roll.weapon2Id, level: roll.level });
return true;
}
/** 两个字是否五行类型相同且显示等级相同 */
private _sameElementLevel(a: { weapon2Id: number; level?: number }, b: { weapon2Id: number; level?: number }): boolean {
const provider = WeaponComposeConfigProvider.instance;
provider.load();
const typeA = Math.floor(provider.getWeapon2Type(a.weapon2Id));
const typeB = Math.floor(provider.getWeapon2Type(b.weapon2Id));
if (typeA < 1 || typeA > 5 || typeA !== typeB) return false;
return Math.floor(a.level ?? 1) === Math.floor(b.level ?? 1);
}
/** 本批是否已有可攻击单字 */
private _slotsHaveAttacker(slots: readonly { weapon2Id: number }[]): boolean {
for (let i = 0; i < slots.length; i++) {
@ -369,6 +448,75 @@ export class BattleBagSession {
}
}
/** 最近一次空格刷新写入的槽 */
lastFilledSlotIndices(): readonly number[] {
return this._lastFilledSlots;
}
/**
* 召唤:只给当前空的开启格补随机字,最多 5 个。已有字的格子不动。
* 没有空开启格时不扣桃。
*/
tryFillEmptyOpenSlots(battle: ProjectBattleHandle): 'ok' | 'not_enough' | 'failed' | 'pool_empty' | 'no_slot' {
this._lastFilledSlots = [];
if (!this.landStateOf) {
console.log('[BattleBagSession] 未绑定地块状态,不能按空格刷新');
return 'failed';
}
const empty = this._emptyOpenSlots();
if (empty.length === 0) return 'no_slot';
const count = Math.min(REFRESH_EMPTY_MAX, empty.length);
if (!this._textPool.canRollSlots(count)) return 'pool_empty';
const slot = battle.session.players[this._ownerPlayerId] ?? (this._ownerPlayerId === 0 ? battle.localSlot : undefined);
if (!slot) {
console.log('[BattleBagSession] tryFillEmptyOpenSlots: player slot missing', this._ownerPlayerId);
return 'failed';
}
const cost = this.normalRefreshCost;
if (slot.gold < cost) return 'not_enough';
if (!slot.spendGold(cost)) return 'not_enough';
if (this._ownerPlayerId === 0) {
battle.CurentCoinNum = slot.gold;
}
try {
const rolled = this._textPool.rollSlots(count, false, false);
if (!rolled || rolled.length < count) {
throw new Error(`[BattleBagSession] 空格抽取不足 need=${count}`);
}
const filled: number[] = [];
for (let i = 0; i < count; i++) {
const index = empty[i];
this.merge.bag.setSlot(index, { weapon2Id: rolled[i].weapon2Id, level: rolled[i].level });
filled.push(index);
}
this._lastFilledSlots = filled;
this._normalRefreshUsed++;
logBagRefreshSection(`空格刷新 ${filled.join(',')} 共${count}`);
return 'ok';
} catch (e) {
slot.addGold(cost);
if (this._ownerPlayerId === 0) {
battle.CurentCoinNum = slot.gold;
}
console.error('[BattleBagSession] tryFillEmptyOpenSlots failed', String(e));
return 'failed';
}
}
/** 当前没有字的开启格,按下标升序 */
private _emptyOpenSlots(): number[] {
const out: number[] = [];
if (!this.landStateOf) return out;
const slotCount = this.merge.bag.slotCount;
for (let i = 0; i < slotCount; i++) {
if (this.landStateOf(i) !== TextListCellState.Open) continue;
if (this.merge.bag.getSlot(i)) continue;
out.push(i);
}
return out;
}
/**
* AI 逻辑落子:只改无头 Merge,不走底栏拖拽表现 / GEvent。
* 仍须同步 MapGrid 格语义(铲 Dig→Put 等),否则引导/落子探测与逻辑层脱节。
@ -427,7 +575,8 @@ export class BattleBagSession {
grantShovel(_chapterId: number): PlaceRejectReason | null {
if (!this.merge.prepActive) return PlaceRejectReason.NotPrepPhase;
if (this.merge.grantGridInBag(SHOVEL_WEAPON2_ID)) {
const shovelSlot = this.findEmptyBagSlot();
if (shovelSlot != null && this.merge.grantWeaponInBagSlot(shovelSlot, SHOVEL_WEAPON2_ID) == null) {
return null;
}
@ -461,6 +610,7 @@ export class BattleBagSession {
const slotCount = this.merge.bag.slotCount;
for (let i = 0; i < slotCount; i++) {
if (excludeSlotIndices && excludeSlotIndices.indexOf(i) >= 0) continue;
if (this.landStateOf?.(i) === TextListCellState.Blank) continue;
if (this.merge.bag.getSlot(i)) continue;
return i;
}
@ -663,6 +813,40 @@ export class BattleBagSession {
this._onWalkabilityCellChanged(gridIndex, true);
}
/**
* 地图字拖到空装饰格时删掉,不退回字池。
* @param gridIndex 来源地图格
*/
discardMapGlyph(gridIndex: number): boolean {
const before = this.merge.map.getCellKind(gridIndex);
const ok = this.merge.discardDraggedMapGlyph(gridIndex);
if (!ok) return false;
const next = this.merge.map.getCellKind(gridIndex);
if (this._walkabilityChanged(before, next)) {
const becameBlocked = before === MapCellKind.Walkable && next !== MapCellKind.Walkable;
this._onWalkabilityCellChanged(gridIndex, becameBlocked);
} else {
this._syncGridVisual(gridIndex);
}
GEvent.Ins.emit(GEvent.UpdateBestPlace);
return true;
}
/**
* 背包字拖到空装饰格时删掉,不退回字池。
* @param slotIndex 来源槽
*/
discardBagSlot(slotIndex: number): boolean {
const rec = this.merge.bag.getSlot(slotIndex);
if (!rec) {
console.log('[BattleBagSession] 空槽无法删除', slotIndex);
return false;
}
this.merge.bag.clearSlot(slotIndex);
GEvent.Ins.emit(GEvent.UpdateBestPlace);
return true;
}
drop(source: DragSource, target: DropTarget): PlaceRejectReason | null {
if (target.kind === 'map' && !this.validator.isAllowedCampForPlace(target.gridIndex)) {
this.merge.reject(PlaceRejectReason.WrongCampArea);
@ -672,6 +856,23 @@ export class BattleBagSession {
const unlockHit = this._dragUnlockArea(source, target.gridIndex);
if (unlockHit !== 'skip') return unlockHit;
}
if (target.kind === 'bag') {
const land = this.landStateOf?.(target.slotIndex);
if (land === TextListCellState.Locked || land === TextListCellState.Blank) {
this.merge.reject(PlaceRejectReason.WrongCellKind);
return PlaceRejectReason.WrongCellKind;
}
if (land === TextListCellState.OpenNoDrag) {
const dst = this.merge.bag.getSlot(target.slotIndex);
const src = source.kind === 'bag'
? this.merge.bag.getSlot(source.slotIndex)
: this.merge.map.getTile(source.gridIndex);
if (!dst || !src || !this._sameElementLevel(src, dst)) {
this.merge.reject(PlaceRejectReason.InvalidMerge);
return PlaceRejectReason.InvalidMerge;
}
}
}
this._snapshotHeroBindIfMapSource(source);
const before = this._snapshotWalkKinds(source, target);
const reason = this.merge.drop(source, target);

@ -5,6 +5,7 @@ import { OtherDataType } from '../../game/GameDataEnums';
import type { BattleBagSession } from './BattleBagSession';
import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider';
import { isComboHero } from '../config/BattleHeroTable';
import { TextListCellState } from '../view/TextListLandHost';
/** 通关时场上一个完整英雄单位(同 weapon1 不同局内等级视为不同单位) */
export interface KeepHeroClearUnit {
@ -357,6 +358,7 @@ export function tryGrantPendingKeepHeroToBag(
const slots = bag.merge.bag;
const emptyIndices: number[] = [];
for (let i = 0; i < slots.slotCount; i++) {
if (bag.landStateOf?.(i) === TextListCellState.Blank) continue;
if (!slots.getSlot(i)) emptyIndices.push(i);
}
if (emptyIndices.length < records.length) {

@ -5,8 +5,10 @@ import { isAttackGlyph } from '../config/BattleHeroTable';
import { logBagRefreshDebug, logBagRefreshSection } from '../debug/BagRefreshDebugLog';
import type { BagRollEntry } from './BagWeaponPool';
/** Text.type:基础字 / 组合字 / 铲子可入池 */
const POOL_TYPES = [1, 2, 4];
/** Text.type 可入池类型(1-5:金木水火土全表;tolockland 格额外要求 type 1-5) */
const POOL_TYPES = [1, 2, 3, 4, 5];
/** number 档限量规则已废弃,剩余量统一用该值表示不限量(需兼容 |0 截断,取安全量级) */
const UNLIMITED_REMAINING = 999999;
/** 文昌笔改字:仅单字 / 组合字(禁止抽到铲子落地图变永久铲) */
const REWRITE_GLYPH_TYPES = [Weapon2TileType.Single, Weapon2TileType.Combo];
@ -30,6 +32,10 @@ export interface PlayerBagTextPoolState {
textIds: number[];
/** 与 textIds 对齐的 Text.type */
types: number[];
/** 与 textIds 对齐的输出 weapon2Id(Text.id;旧快照缺失时用 textIds) */
heroIds?: number[];
/** 与 textIds 对齐的显示等级(Text.level + 段位加值;旧快照缺失时按 Text.level) */
levels?: number[];
/** 基础权重 */
weights: number[];
/** 剩余数量 */
@ -39,7 +45,7 @@ export interface PlayerBagTextPoolState {
}
/**
* 玩家局内背包字池。
* 开局按段位选 numberN 档,整局累计扣减;最终权重 = weight × remaining。
* 按 Text.weight 加权随机,POOL_TYPES 内 weight>0 的行全部入池、不限量(number 档限量规则已废弃)。
*/
export class PlayerBagTextPool {
private _numberN = 0;
@ -47,6 +53,8 @@ export class PlayerBagTextPool {
private _heroSummonOffset = 0;
private _textIds: number[] = [];
private _types: number[] = [];
private _heroIds: number[] = [];
private _levels: number[] = [];
private _weights: number[] = [];
private _remaining: number[] = [];
private _ready = false;
@ -107,26 +115,36 @@ export class PlayerBagTextPool {
const list = gg.data.table.getTableList<ITableText>(TableNames.Text) ?? [];
this._textIds = [];
this._types = [];
this._heroIds = [];
this._levels = [];
this._weights = [];
this._remaining = [];
this._numberN = n;
// 段位等级加值:Text.level + levelvalue = 显示等级;须有 Hero(herotype=type, level) 行才有编号可显示
const rid2 = rankId ?? RankDataManager.getPlayerRankId();
const levelValue = Math.max(0, Math.floor(Number(RankDataManager.getRankConfig(rid2)?.levelvalue) || 0));
const heroes = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
for (let i = 0; i < list.length; i++) {
const row = list[i];
const type = Number(row.type);
if (POOL_TYPES.indexOf(type) < 0) continue;
const weight = Number(row.weight);
if (!(weight > 0)) continue;
const cap = this._readNumberN(row, n);
if (!(cap > 0)) continue;
const level = Math.floor(Number(row.level)) + levelValue;
const hero = this._heroAt(heroes, type, level);
if (!hero) continue;
this._textIds.push(Number(row.id));
this._types.push(type);
this._heroIds.push(hero.id);
this._levels.push(level);
this._weights.push(weight);
this._remaining.push(cap);
this._remaining.push(UNLIMITED_REMAINING);
}
if (this._textIds.length === 0) {
console.log(`[PlayerBagTextPool] 候选为空 rankId=${rid} N=${n}`);
console.log(`[PlayerBagTextPool] 候选为空 rankId=${rid} N=${n} levelValue=${levelValue}`);
this._ready = false;
return;
}
@ -134,11 +152,22 @@ export class PlayerBagTextPool {
this._ready = true;
if (heroSummonActive) {
logBagRefreshSection(
`字池建池 rankId=${rid} 召神令 baseN=${this._baseNumberN} offset=${this._heroSummonOffset} N=${n} 候选=${this._textIds.length}`,
`字池建池 rankId=${rid} 召神令 baseN=${this._baseNumberN} offset=${this._heroSummonOffset} N=${n} 候选=${this._textIds.length} levelValue=${levelValue}`,
);
return;
}
logBagRefreshSection(`字池建池 rankId=${rid} N=${n} 候选=${this._textIds.length}`);
logBagRefreshSection(`字池建池 rankId=${rid} N=${n} 候选=${this._textIds.length} levelValue=${levelValue}`);
}
/** 按 herotype=Text.type、level=Text.level+段位加值 查 Hero 行 */
private _heroAt(heroes: readonly ITableHero[], herotype: number, level: number): ITableHero | null {
for (let i = 0; i < heroes.length; i++) {
const h = heroes[i];
if (Math.floor(Number(h.herotype)) !== herotype) continue;
if (Math.floor(Number(h.level)) !== level) continue;
return h;
}
return null;
}
/** 钳制 Text.number 档到 [1,20] */
@ -163,6 +192,8 @@ export class PlayerBagTextPool {
heroSummonOffset: this._heroSummonOffset,
textIds: this._textIds.slice(),
types: this._types.slice(),
heroIds: this._heroIds.slice(),
levels: this._levels.slice(),
weights: this._weights.slice(),
remaining: this._remaining.slice(),
ready: this._ready,
@ -178,11 +209,27 @@ export class PlayerBagTextPool {
this._types = state.types?.length === state.textIds.length
? state.types.slice()
: this._typesFromTextIds(state.textIds);
this._heroIds = state.heroIds?.length === state.textIds.length
? state.heroIds.slice()
: state.textIds.slice();
this._levels = state.levels?.length === state.textIds.length
? state.levels.slice()
: this._levelsFromTextIds(state.textIds);
this._weights = state.weights.slice();
this._remaining = state.remaining.slice();
this._ready = state.ready;
}
/** 兜底:按 Text 表行补 levels */
private _levelsFromTextIds(textIds: readonly number[]): number[] {
const out: number[] = [];
for (let i = 0; i < textIds.length; i++) {
const row = gg.data.table.getTableData<ITableText>(TableNames.Text, textIds[i]);
out.push(row ? Math.floor(Number(row.level)) : 1);
}
return out;
}
/**
* 文昌笔:按召唤字池加权抽取 1 个单字/组合字(扣减 remaining;不含铲子)。
* @param excludeWeapon2Id 尽量排除该字;池内无其它候选时返回 null
@ -195,7 +242,7 @@ export class PlayerBagTextPool {
const idx = this._pickGlyphIndexExcluding(excludeWeapon2Id);
if (idx == null) return null;
this._remaining[idx] -= 1;
const entry: BagRollEntry = { weapon2Id: this._textIds[idx] };
const entry: BagRollEntry = { weapon2Id: this._textIds[idx], level: this._levels[idx] };
logBagRefreshDebug('字池单抽', String(entry.weapon2Id));
return entry;
}
@ -207,8 +254,8 @@ export class PlayerBagTextPool {
}
/**
* 按 Text.id 扣减 1 次 remaining(GM/预设刷新等非 rollSlots 路径)。
* @param weapon2Id Text.id
* 按 id 扣减 1 次 remaining(GM/预设刷新等非 rollSlots 路径)。
* @param weapon2Id Text.id 或池输出的 Hero.id
* @returns 是否命中字池并成功扣减
*/
tryConsumeWeapon2Id(weapon2Id: number): boolean {
@ -216,7 +263,7 @@ export class PlayerBagTextPool {
const id = Math.floor(Number(weapon2Id) || 0);
if (id <= 0) return false;
for (let i = 0; i < this._textIds.length; i++) {
if (this._textIds[i] !== id) continue;
if (this._textIds[i] !== id && this._heroIds[i] !== id) continue;
if (this._remaining[i] <= 0) return false;
this._remaining[i] -= 1;
return true;
@ -225,8 +272,8 @@ export class PlayerBagTextPool {
}
/**
* 按 Text.id 归还 1 次 remaining(保底替换退还原抽中字)。
* @param weapon2Id Text.id
* 按 id 归还 1 次 remaining(保底替换退还原抽中字)。
* @param weapon2Id Text.id 或池输出的 Hero.id
* @returns 是否命中字池并成功归还
*/
tryRefundWeapon2Id(weapon2Id: number): boolean {
@ -234,7 +281,7 @@ export class PlayerBagTextPool {
const id = Math.floor(Number(weapon2Id) || 0);
if (id <= 0) return false;
for (let i = 0; i < this._textIds.length; i++) {
if (this._textIds[i] !== id) continue;
if (this._textIds[i] !== id && this._heroIds[i] !== id) continue;
this._remaining[i] += 1;
return true;
}
@ -295,7 +342,7 @@ export class PlayerBagTextPool {
}
const result: BagRollEntry[] = [];
for (let i = 0; i < pickedIdx.length; i++) {
result.push({ weapon2Id: this._textIds[pickedIdx[i]] });
result.push({ weapon2Id: this._textIds[pickedIdx[i]], level: this._levels[pickedIdx[i]] });
}
if (shuffle && ensureAttacker) this._shuffleEntries(result);
logBagRefreshDebug('字池抽取', result.map((e) => e.weapon2Id).join(','));
@ -307,6 +354,19 @@ export class PlayerBagTextPool {
return this._pickIndexExcluding(undefined);
}
/**
* 开格随机:normalland 用全池,tolockland 只收 Text.type 1-5。
* @param gate true 为 tolockland 候选
* @returns weapon2Id(Text.id)与显示等级;无候选返回 null
*/
rollOneForLand(gate: boolean): BagRollEntry | null {
if (!this._ready) return null;
const idx = this._pickIndexFiltered(undefined, false, gate ? 5 : 0);
if (idx == null) return null;
this._remaining[idx] -= 1;
return { weapon2Id: this._textIds[idx], level: this._levels[idx] };
}
/** 只从可攻击单字里加权选 */
private _pickAttackerIndex(): number | null {
let total = 0;
@ -342,45 +402,48 @@ export class PlayerBagTextPool {
/** 加权选下标,可排除指定 weapon2Id */
private _pickIndexExcluding(excludeWeapon2Id?: number): number | null {
return this._pickIndexFiltered(excludeWeapon2Id, false);
return this._pickIndexFiltered(excludeWeapon2Id, false, 0);
}
/** 文昌笔:仅单字/组合字加权选下标 */
private _pickGlyphIndexExcluding(excludeWeapon2Id?: number): number | null {
return this._pickIndexFiltered(excludeWeapon2Id, true);
return this._pickIndexFiltered(excludeWeapon2Id, true, 0);
}
/**
* 加权选下标。
* @param excludeWeapon2Id 排除 id
* @param excludeWeapon2Id 排除 id(匹配 Text.id 或输出 Hero.id)
* @param glyphsOnly true 时跳过铲子等非改字类型
* @param maxType Text.type 上限;0 不限(tolockland 候选传 5)
*/
private _pickIndexFiltered(excludeWeapon2Id: number | undefined, glyphsOnly: boolean): number | null {
private _pickIndexFiltered(excludeWeapon2Id: number | undefined, glyphsOnly: boolean, maxType: number): number | null {
let total = 0;
for (let i = 0; i < this._textIds.length; i++) {
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly)) continue;
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
total += this._weights[i] * this._remaining[i];
}
if (!(total > 0)) return null;
let r = Math.random() * total;
for (let i = 0; i < this._textIds.length; i++) {
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly)) continue;
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
r -= this._weights[i] * this._remaining[i];
if (r < 0) return i;
}
for (let i = this._textIds.length - 1; i >= 0; i--) {
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly)) continue;
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
return i;
}
return null;
}
/** 候选是否可参与本次加权抽取 */
private _isPickable(index: number, excludeWeapon2Id: number | undefined, glyphsOnly: boolean): boolean {
private _isPickable(index: number, excludeWeapon2Id: number | undefined, glyphsOnly: boolean, maxType: number): boolean {
if (this._remaining[index] <= 0) return false;
if (excludeWeapon2Id != null && this._textIds[index] === excludeWeapon2Id) return false;
if (excludeWeapon2Id != null && this._heroIds[index] === excludeWeapon2Id) return false;
if (glyphsOnly && REWRITE_GLYPH_TYPES.indexOf(this._types[index]) < 0) return false;
if (maxType > 0 && this._types[index] > maxType) return false;
return true;
}
@ -393,12 +456,4 @@ export class PlayerBagTextPool {
}
return out;
}
/** 读取 Text.numberN */
private _readNumberN(row: ITableText, n: number): number {
const key = `number${n}` as keyof ITableText;
const raw = row[key];
const v = Number(raw);
return Number.isFinite(v) ? v : 0;
}
}

@ -184,6 +184,27 @@ export class WeaponComposeConfigProvider implements IComposeConfigProvider {
return Number(weapon2Id) === PEACH_TREE_WEAPON2_ID;
}
/**
* 五行显示等级上限:同 herotype 的 Hero.level 最大值。
* @param textType Text.type(1–5 金木水火土)
*/
getElementMaxLevel(textType: number): number {
this.load();
const type = Math.floor(Number(textType));
let max = 0;
for (let i = 0; i < this._weapon1List.length; i++) {
const hero = this._weapon1List[i];
if (Math.floor(Number(hero.herotype)) !== type) continue;
const level = Math.floor(Number(hero.level));
if (level > max) max = level;
}
if (!(max > 0)) {
console.log('[WeaponCompose] 五行没有等级行', type);
return 1;
}
return max;
}
/** 蟠桃树合成等级上限(读 Hero.level_limit);其它 weapon2 返回 1 */
getKeepDigTileMaxLevel(weapon2Id: number): number {
this.load();

@ -12,9 +12,6 @@ import { calcSpecialReplicaFinalHpScale, getWeaponParaFromMaxChapter, resolveHpM
import { resetMonsterHpDebugLog } from '../debug/MonsterHpDebugLog';
import { Weapon2TileType } from 'db://assets/mx/module/merge/types/MergeTypes';
/** 战斗地图固定读关卡表这一行(Level.map 与 unlockland) */
export const FIXED_MAP_LEVEL_ID = 11;
/** atk_after 段 1 类 debuff 类型 → 状态特效预制体 */
const DEBUFF_TYPE_TO_STATUS_FX: Readonly<Record<number, string>> = {
1: '怪_火',
@ -63,6 +60,8 @@ export interface WaveIntervalConfig {
/** 双方基地默认血量(后续养成覆盖) */
export const DEFAULT_BATTLE_WALL_HP = 3;
/** 开战初始桃子(写死,不读 Level.startnumber) */
const START_PEACH = 1000;
/** 单怪出怪间隔默认秒数(常量表无对应行) */
const DEFAULT_MONSTER_SPAWN_INTERVAL_SEC = 1;
/** 对齐 Const 表 id=10(ConstNumType.WaveInterval),避免从 ConfigProjectData 拉环 */
@ -79,7 +78,7 @@ export class ChapterLevelBuilder {
}
const difficultyRow = Math.max(1, options.rankDifficultyBonus ?? (Number(levelRow.diffcult) || 1));
const startGold = Number(levelRow.startnumber) > 0 ? Number(levelRow.startnumber) : 200;
const startGold = START_PEACH;
const spawnInterval = this.resolveMonsterSpawnIntervalSec(options.spawnInterval);
const waveTiming = this.resolveWaveIntervalConfig();
const firstWaveDelay = options.firstWaveDelay ?? waveTiming.firstWaveDelaySec;
@ -171,17 +170,17 @@ export class ChapterLevelBuilder {
}
/**
* 地图预制体名。固定读关卡表 {@link FIXED_MAP_LEVEL_ID} 的 Level.map。
* @param _levelId 保留签名;地图不跟当前关卡走
* 地图预制体名。读关卡表当前关卡 Level.map。
* @param levelId 当前关卡 id
*/
static resolveLevelMapPrefabName(_levelId: number): string {
const level = gg.data.table.getTableData<ITableLevel>(TableNames.Level, FIXED_MAP_LEVEL_ID);
static resolveLevelMapPrefabName(levelId: number): string {
const level = gg.data.table.getTableData<ITableLevel>(TableNames.Level, levelId);
const raw = level?.map;
if (typeof raw === 'string') {
const name = raw.trim();
if (name) return name;
}
console.warn('[ChapterLevelBuilder] 固定地图关卡缺少 Level.map', FIXED_MAP_LEVEL_ID);
console.warn('[ChapterLevelBuilder] 关卡缺少 Level.map', levelId);
return '';
}

@ -45,7 +45,6 @@ import { TableNames } from '../../game/ConfigTableData';
import type { BattleWeaponDesc1PanelData } from '../../ui/BattleWeaponDesc1/UIBattleWeaponDesc1';
import type { BattleWeaponDescHeroPanelData } from '../../ui/BattleWeaponDescHero/UIBattleWeaponDescHero';
import { BattleBagSession } from '../bag/BattleBagSession';
import { FIXED_MAP_LEVEL_ID } from '../config/ChapterLevelBuilder';
import { DeploySync } from '../runtime/DeploySync';
import { composeKey } from '../runtime/ComposeUnitKey';
import { findEntityIdByGridIndex } from '../runtime/DeployEntityQuery';
@ -141,6 +140,8 @@ export class CocosBagBoardView {
private _bagList: Node | null = null;
/** ui_textList 开启 / 待解锁 / 未解锁 */
private readonly _land = new TextListLandHost();
/** 开格展示字块补显重试帧计数(textGrid 预制体异步加载未就绪时留空格) */
private _landGlyphRetryFrame = 0;
/** AI 神偷落背包时飞行目标(BattleGame ui_otherBag) */
private _otherBagAnchor: Node | null = null;
private _mapRoot: Node | null = null;
@ -268,10 +269,9 @@ export class CocosBagBoardView {
private _fillMissingBagSlotGrids(usedTextGrids?: MembershipSet<TextGrid>): void {
if (!this._bagSession || !this._bagList) return;
const slots = this._bagSession.getBagSlots();
const children = this._bagList.children;
for (let i = 0; i < slots.length; i++) {
const record = slots[i];
const slotNode = children[i];
const slotNode = this._bagSlotNodeAt(i);
if (!record) {
if (slotNode?.isValid && !this._findFlightGridForBag(i)) this._purgeTextGridsOnNode(slotNode);
this._slotGrids.delete(i);
@ -302,6 +302,16 @@ export class CocosBagBoardView {
return false;
}
/** 槽位下标对应的 ui_textList 格子节点(gridN → N;temp 与无名节点不算) */
private _bagSlotNodeAt(slotIndex: number): Node | null {
if (!this._bagList?.isValid) return null;
const children = this._bagList.children;
for (let i = 0; i < children.length; i++) {
if (TextListLandHost.indexOfNode(children[i]) === slotIndex) return children[i];
}
return null;
}
/** 节点是否挂在地图格 MapGrid 下 */
private _isNodeUnderMapCell(node: Node | null | undefined): boolean {
if (!node?.isValid || !this._map?.mapGrids?.length) return false;
@ -401,7 +411,7 @@ export class CocosBagBoardView {
if (!this._bagSession || !this._bagList || !grid?.isValid || !grid.node?.isValid) return;
const record = this._bagSession.getBagSlots()[slotIndex];
if (!record) return;
const slotNode = this._bagList.children[slotIndex];
const slotNode = this._bagSlotNodeAt(slotIndex);
if (!slotNode?.isValid) return;
const entity = this._buildBagSlotEntity(slotIndex);
if (!entity) return;
@ -444,7 +454,7 @@ export class CocosBagBoardView {
const parent = this._map?.getMapGridByIndex(target.gridIndex)?.node;
this._commitMapTextGridView(target.gridIndex, swap ? grid : this._keeperAfterDrop(grid, parent));
} else {
const slotNode = this._bagList?.children[target.slotIndex];
const slotNode = this._bagSlotNodeAt(target.slotIndex);
const incoming = swap && grid.weapon2Id !== 0 ? grid : this._keeperAfterDrop(grid, slotNode);
this._commitBagTextGridView(target.slotIndex, incoming);
}
@ -679,7 +689,7 @@ export class CocosBagBoardView {
/** 保证背包槽有 TextGrid(逻辑有 record、视图缺失或节点已回池时重建) */
ensureBagSlotGrid(slotIndex: number): TextGrid | null {
if (!this._bagSession || !this._bagList) return null;
const slotNode = this._bagList.children[slotIndex];
const slotNode = this._bagSlotNodeAt(slotIndex);
const record = this._bagSession.getBagSlots()[slotIndex];
if (!record || !slotNode?.isValid) return null;
const entity = buildTextGridEntityData(
@ -820,7 +830,7 @@ export class CocosBagBoardView {
this._fillMissingBagSlotGrids();
for (let i = 0; i < slots.length; i++) {
if (!slots[i]) continue;
const slotNode = this._bagList.children[i];
const slotNode = this._bagSlotNodeAt(i);
if (!slotNode?.isValid) continue;
const live = this._resolveBagSlotLiveGrid(i, slotNode, slots[i].weapon2Id) ?? this.ensureBagSlotGrid(i);
if (!live?.isValid) continue;
@ -869,7 +879,7 @@ export class CocosBagBoardView {
const bagSlot = this._deferredBagUpgradeSlot;
if (bagSlot != null) {
this._deferredBagUpgradeSlot = null;
const slotNode = this._bagList?.children[bagSlot];
const slotNode = this._bagSlotNodeAt(bagSlot);
if (this._bagSession?.getBagSlots()[bagSlot] && slotNode?.isValid) {
const grid = this._resolveBagTextGridForUpgrade(bagSlot, slotNode);
const record = this._bagSession.getBagSlots()[bagSlot];
@ -886,7 +896,7 @@ export class CocosBagBoardView {
private _vacateBagSlotView(slotIndex: number): void {
this._grids.deleteBagSlot(slotIndex);
const slotNode = this._bagList?.children[slotIndex];
const slotNode = this._bagSlotNodeAt(slotIndex);
if (!slotNode?.isValid) return;
for (const child of [...slotNode.children]) {
const tg = child.getComponent(TextGrid);
@ -1136,7 +1146,7 @@ export class CocosBagBoardView {
/** 技能产桃:本机格心飞向 ui_money;敌方格心 coinFly 原地弹跳 */
private _skillPeachFlyHandler: ((gridIndex: number, peachCount: number, ownerPlayerId: number) => void) | null = null;
/** 砚台(btnIllustrated)世界坐标,供背包刷新从砚台出字 */
/** 出字起点(btnRefresh)世界坐标 */
setInkstoneWorldPosProvider(provider: (() => Readonly<Vec3> | null) | null): void {
this._inkstoneWorldPosProvider = provider;
}
@ -1183,6 +1193,7 @@ export class CocosBagBoardView {
/** 帧更新:己方/敌方地图蟠桃树按技能 CD 切换桃字1/桃字2 循环 */
tickPeachTreePresentations(): void {
this._tickLandGlyphRetry();
for (const grid of this._mapGrids.values()) {
if (grid?.isValid) grid.tickPeachTreeCdPresentation();
}
@ -1194,6 +1205,15 @@ export class CocosBagBoardView {
});
}
/** 空开启槽补显:每 10 帧检查一次,textGrid 预制体就绪后自动补齐;前 30 秒后停轮询 */
private _tickLandGlyphRetry(): void {
this._landGlyphRetryFrame++;
if (this._landGlyphRetryFrame >= 1800) return; // 30 秒后停
if (this._landGlyphRetryFrame % 10 !== 0) return;
if (!this._bagSession) return;
this._fillMissingBagSlotGrids();
}
getInkstoneWorldPosition(): Readonly<Vec3> | null {
return this._inkstoneWorldPosProvider?.() ?? null;
}
@ -1229,7 +1249,7 @@ export class CocosBagBoardView {
this._battle = battle;
this._map = map;
this._bagList = bagList;
this._land.bind(FIXED_MAP_LEVEL_ID, bagList);
this._land.bind(battle.LevelConfigId, bagList);
this._mapRoot = mapRoot;
this._dragLayer = dragLayer ?? mapRoot;
if (this._dragLayer?.isValid) {
@ -1240,8 +1260,16 @@ export class CocosBagBoardView {
this._linePresenter.bind(this._lineNode);
this._rangePresenter.bind(rangeNode ?? null, mapRoot?.parent ?? null);
this._bagSession = new BattleBagSession(map, battle.LevelConfigId, battle.session);
// 槽位下标与格子下标(gridN → N)一致,槽数按最大格子下标 + 1 分配(slot0 对应不到格子,保持空)
let maxLandIndex = 0;
const bagChildren = bagList.children;
for (let i = 0; i < bagChildren.length; i++) {
const idx = TextListLandHost.indexOfNode(bagChildren[i]);
if (idx != null && idx > maxLandIndex) maxLandIndex = idx;
}
this._bagSession = new BattleBagSession(map, battle.LevelConfigId, battle.session, maxLandIndex + 1);
battle.bagSession = this._bagSession;
this._bagSession.landStateOf = (slotIndex) => this._land.stateOf(slotIndex);
battle.charmController.bindBagView(this);
battle.charmController.bindMergeSession(this._bagSession.merge);
getTowerLevelDownHost().bindBagView(this);
@ -1258,9 +1286,12 @@ export class CocosBagBoardView {
this._bagSession.on(MERGE_EVENT_ALL, this._mergeListener);
this._bindMapGridClicks();
this._rebuildBagFromSession(true);
this._warmTextGridPool();
// 开局只给 tolockland 放字;normalland 留空;grid17 不出字
this._bagSession.autoFillOpenSlots();
this._rebuildBagFromSession(true);
this._refreshDragEnabled();
this._fillMissingBagSlotGrids();
CocosBagBoardView._bound = this;
}
@ -1681,7 +1712,7 @@ export class CocosBagBoardView {
if (placement.kind === 'map') {
return this._map?.getMapGridByIndex(placement.gridIndex)?.node ?? null;
}
return this._bagList?.children[placement.slotIndex] ?? null;
return this._bagSlotNodeAt(placement.slotIndex) ?? null;
}
/** 预览妙手神偷落点(敌方字块绿/红 select) */
@ -2975,7 +3006,7 @@ export class CocosBagBoardView {
/** 背包槽世界坐标(拉线终点) */
getBagSlotWorldPosition(slotIndex: number): Vec3 | null {
const slot = this._bagList?.children[slotIndex];
const slot = this._bagSlotNodeAt(slotIndex);
if (!slot?.isValid) return null;
return slot.worldPosition.clone();
}
@ -3091,6 +3122,35 @@ export class CocosBagBoardView {
}
}
/**
* 只让新补上的空格字从砚台飞入,已有字的格子保持不动。
* @param slotIndices 本次写入的槽
* @param fromWorld 起点世界坐标
*/
playNewSlotEntrance(slotIndices: readonly number[], fromWorld?: Readonly<Vec3> | null): void {
this._fillMissingBagSlotGrids();
const flying = new Map<number, TextGrid>();
for (let i = 0; i < slotIndices.length; i++) {
const grid = this._slotGrids.get(slotIndices[i]);
if (!grid?.isValid) continue;
grid.enableDrag(false);
flying.set(slotIndices[i], grid);
}
if (flying.size === 0) return;
this._battle?.forceEndBagDragSlowMo();
const layout = this._bagList?.getComponent(Layout) ?? null;
if (layout) layout.enabled = false;
this._bagEntrancePlaying = true;
this._bagEntranceStartHandler?.();
BagSlotEntranceAnimator.play(
flying,
this._bagList,
this._onBagEntranceFinished,
fromWorld,
this._onBagEntranceSlotLanded,
);
}
private _playBagEntrance(fromWorld?: Readonly<Vec3> | null): void {
this._battle?.forceEndBagDragSlowMo();
this._fillMissingBagSlotGrids();
@ -3142,11 +3202,10 @@ export class CocosBagBoardView {
private _snapBagSlotsAfterEntrance(): void {
if (!this._bagSession || !this._bagList) return;
const slots = this._bagSession.getBagSlots();
const children = this._bagList.children;
for (let i = 0; i < slots.length; i++) {
const record = slots[i];
if (!record) continue;
const slotNode = children[i];
const slotNode = this._bagSlotNodeAt(i);
let grid = this._slotGrids.get(i);
if (!grid?.node?.isValid || (slotNode?.isValid && grid.node.parent !== slotNode)) {
grid = this.ensureBagSlotGrid(i) ?? undefined;
@ -3197,7 +3256,10 @@ export class CocosBagBoardView {
const comp = node.getComponent(TextGrid) ?? node.addComponent(TextGrid);
this._writeGridData(comp, entity);
comp.rememberOrigin();
comp.enableDrag(enableDragNow && this._canDrag());
const dragOn = entity.location.kind === 'bag'
? this._canEnableBagGridDrag(comp)
: this._canDrag();
comp.enableDrag(enableDragNow && dragOn);
comp.setMapClickHandler((g) => this._onTextGridClick(g));
this._bindDrag(comp);
return comp;
@ -3378,7 +3440,7 @@ export class CocosBagBoardView {
this._showMergeTargetShake(this._activeDrag);
if (lineDragMode) {
if (source.kind === 'bag') {
BagTextDragDebugLog.slotChildren(source.slotIndex, this._bagList?.children[source.slotIndex], 'beforeGhost');
BagTextDragDebugLog.slotChildren(source.slotIndex, this._bagSlotNodeAt(source.slotIndex), 'beforeGhost');
}
this._ensureDragGhost(grid);
BagTextDragDebugLog.snap(debugSeq, 'ghostReady', this._dragGhost, 'ghost');
@ -3433,7 +3495,7 @@ export class CocosBagBoardView {
const bagSwap = fromSlot >= 0 && bagSlotIndex !== fromSlot;
const mapToBag = fromMapIndex >= 0;
if (bagSwap || mapToBag) {
const bagSlotNode = this._bagList?.children[bagSlotIndex];
const bagSlotNode = this._bagSlotNodeAt(bagSlotIndex);
if (bagSlotNode?.isValid) {
this._setDragSelectTarget({ kind: 'bag', index: bagSlotIndex });
if (lineFrom) {
@ -3539,6 +3601,10 @@ export class CocosBagBoardView {
}
const target = this._resolveDropTarget(event, drag.source);
if (target.kind === 'bag' && this._land.isBlank(target.slotIndex)) {
this._discardGlyphToBlank(grid, drag);
return;
}
if (target.kind === 'bag' && this._interceptLandDrop(target.slotIndex)) {
if (drag.type3Backup) {
this._bagSession.revertType3Pickup(drag.type3Backup.gridIndex, drag.type3Backup.tile);
@ -3625,6 +3691,10 @@ export class CocosBagBoardView {
}
const bagSlotIndex = this._resolveBagSlotAt(uiPos);
if (bagSlotIndex != null && this._land.isBlank(bagSlotIndex)) {
this._discardGlyphToBlank(grid, drag);
return;
}
if (bagSlotIndex != null && this._interceptLandDrop(bagSlotIndex)) {
this._lineDragCancel(grid, drag);
return;
@ -3721,7 +3791,7 @@ export class CocosBagBoardView {
BagTextDragDebugLog.snap(seqMap, 'bagToMap', grid, `fromSlot=${slotIndex}→toMap=${mapIndex}`);
}
const targetWorld = mapGrid.node.worldPosition.clone();
const bagSlotNode = slotIndex >= 0 ? this._bagList?.children[slotIndex] : null;
const bagSlotNode = slotIndex >= 0 ? this._bagSlotNodeAt(slotIndex) : null;
const displacedGrid = this._mapGrids.get(mapIndex) ?? null;
const swapToBag = displacedGrid?.node?.isValid
&& bagSlotNode?.isValid
@ -3801,8 +3871,8 @@ export class CocosBagBoardView {
toSlot: number,
event: EventTouch,
): void {
const targetBagNode = this._bagList?.children[toSlot];
const sourceBagNode = this._bagList?.children[fromSlot];
const targetBagNode = this._bagSlotNodeAt(toSlot);
const sourceBagNode = this._bagSlotNodeAt(fromSlot);
if (!targetBagNode?.isValid || !sourceBagNode?.isValid || !this._dragLayer?.isValid) {
grid.restoreOrigin();
return;
@ -3872,7 +3942,7 @@ export class CocosBagBoardView {
toSlot: number,
event: EventTouch,
): void {
const targetBagNode = this._bagList?.children[toSlot];
const targetBagNode = this._bagSlotNodeAt(toSlot);
const sourceMapGrid = this._map?.getMapGridByIndex(fromMapIndex);
if (!targetBagNode?.isValid || !sourceMapGrid?.node?.isValid || !this._dragLayer?.isValid) {
this._lineDragCancel(grid, drag);
@ -4053,11 +4123,13 @@ export class CocosBagBoardView {
/** 背包槽互换:仅同级可合成时不交换;异级/满级都交换 */
private _willBagBagSwap(fromSlot: number, toSlot: number): boolean {
if (fromSlot === toSlot || !this._bagSession) return false;
if (this._land.stateOf(toSlot) !== TextListCellState.Open) return false;
const slots = this._bagSession.getBagSlots();
const src = slots[fromSlot];
const dst = slots[toSlot];
if (!src || !dst) return false;
if (this._willSingleMergeUpgrade(src, dst)) return false;
if (this._willElementMerge(src, dst)) return false;
return true;
}
@ -4092,6 +4164,10 @@ export class CocosBagBoardView {
/** 地图字块落子到已占背包槽时,是否会与背包字块交换到地图 */
private _willMapBagSwap(fromMapIndex: number, toSlot: number): boolean {
if (this._land.stateOf(toSlot) !== TextListCellState.Open) return false;
const tile = this._bagSession?.merge.map.getTile(fromMapIndex);
const slot = this._bagSession?.getBagSlots()[toSlot];
if (tile && slot && this._willElementMerge(tile, slot)) return false;
return this._willBagMapSwap(toSlot, fromMapIndex);
}
@ -4292,6 +4368,20 @@ export class CocosBagBoardView {
return this._bagSession?.isMergeUpgradeBlocked(gridIndex) === true;
}
/**
* 五行同类型同等级会在背包合成(含满级拒绝),不走交换。
* @param src 拖来的字
* @param dst 目标格上的字
*/
private _willElementMerge(src: { weapon2Id: number; level?: number }, dst: { weapon2Id: number; level?: number }): boolean {
WeaponComposeConfigProvider.instance.load();
const provider = WeaponComposeConfigProvider.instance;
const typeA = Math.floor(provider.getWeapon2Type(src.weapon2Id));
const typeB = Math.floor(provider.getWeapon2Type(dst.weapon2Id));
if (typeA < 1 || typeA > 5 || typeA !== typeB) return false;
return Math.floor(src.level ?? 1) === Math.floor(dst.level ?? 1);
}
/** type1 同 Weapon1 同等级可合成升级 */
private _willSingleMergeUpgrade(
src: { weapon2Id: number; weapon1Id?: number; level?: number },
@ -4331,12 +4421,97 @@ export class CocosBagBoardView {
}
/**
* 待解锁格放行拖入走正常合成(BagMerged 后解锁下标+1),其余吃掉拖放。
* 背包或地图上的字拖到空装饰格:飞到 grid17 后消失,不留在格子上。
* @param grid 被拖的字
* @param drag 本次拖拽
*/
private _discardGlyphToBlank(grid: TextGrid, drag: NonNullable<typeof this._activeDrag>): void {
const session = this._bagSession;
const blank = this._blankLandNode();
const layer = this._dragLayer;
if (!session || !blank?.isValid || !layer?.isValid || !grid.node?.isValid) {
this._discardGlyphData(drag);
if (grid.node?.isValid) this._forceRecycleTextGrid(grid);
return;
}
const from: GridDetachSource = drag.source.kind === 'bag'
? { kind: 'bag', slotIndex: drag.source.slotIndex }
: { kind: 'map', gridIndex: drag.mapDrag?.fromGridIndex ?? drag.source.gridIndex };
const home = from.kind === 'bag'
? this._bagSlotNodeAt(from.slotIndex)
: this._map?.getMapGridByIndex(from.gridIndex)?.node ?? null;
const startWorld = grid.node.worldPosition.clone();
this._mapDropAnimInflight++;
this._detachGridForFlight(grid, from);
grid.node.parent = layer;
grid.node.setWorldPosition(startWorld);
grid.preparePlaceFlightVisual();
grid.enableDrag(false);
if (!this._discardGlyphData(drag)) {
if (this._mapDropAnimInflight > 0) this._mapDropAnimInflight--;
this._grids.removeFromFlight(grid);
if (home?.isValid) {
grid.node.parent = home;
grid.node.setPosition(0, 0, 0);
}
this._lineDragCancel(grid, drag);
return;
}
const target = blank.worldPosition.clone();
BagToMapPlaceAnimator.play(grid, target, layer, () => {
this._finishDiscardFlight(grid);
}, { showTrail: DeviceProfile.enableBoardPlaceTrail });
const seq = drag.debugSeq ?? this._textDragDebugSeq;
if (seq) BagTextDragDebugLog.end(seq, grid, 'discardBlank');
this._textDragDebugSeq = 0;
}
/**
* 删掉拖来的背包槽或地图字,不退回字池。
* @param drag 本次拖拽
*/
private _discardGlyphData(drag: NonNullable<typeof this._activeDrag>): boolean {
const session = this._bagSession;
if (!session) return false;
if (drag.source.kind === 'bag') return session.discardBagSlot(drag.source.slotIndex);
const mapIndex = drag.mapDrag?.fromGridIndex ?? drag.source.gridIndex;
return session.discardMapGlyph(mapIndex);
}
/**
* 飞到空装饰格后回收字块,并补上推迟的盘面刷新。
* @param grid 飞行中的字
*/
private _finishDiscardFlight(grid: TextGrid): void {
this._grids.removeFromFlight(grid);
if (grid?.node?.isValid) this._forceRecycleTextGrid(grid);
if (this._mapDropAnimInflight > 0) this._mapDropAnimInflight--;
this._flushAllPendingRebuilds();
this._refreshDragEnabled();
this._refreshCanUpgradeHints();
}
/** 空装饰格节点(grid17) */
private _blankLandNode(): Node | null {
const list = this._bagList;
if (!list?.isValid) return null;
const children = list.children;
for (let i = 0; i < children.length; i++) {
const index = TextListLandHost.indexOfNode(children[i]);
if (index != null && this._land.isBlank(index)) return children[i];
}
return null;
}
/**
* 未解锁格吃掉拖放。待开启格放行,走同类型同等级合成。
* 背包槽范围外的格子不落子,避免写入无效槽。
* @param slotIndex 命中的格子下标
*/
private _interceptLandDrop(slotIndex: number): boolean {
if (this._land.stateOf(slotIndex) === TextListCellState.OpenNoDrag) return false;
const state = this._land.stateOf(slotIndex);
if (state === TextListCellState.Blank || state === TextListCellState.Locked) return true;
if (state === TextListCellState.OpenNoDrag) return false;
const slotCount = this._bagSession?.merge.bag.slotCount ?? 0;
if (slotIndex >= 0 && slotIndex < slotCount) return false;
return this._land.stateOf(slotIndex) !== TextListCellState.Open;
@ -4388,11 +4563,8 @@ export class CocosBagBoardView {
private _resolveBagSlotIndexForTextGrid(grid: TextGrid): number | null {
const parent = grid.node?.parent;
if (!parent?.isValid || !this._bagList?.isValid) return null;
const children = this._bagList.children;
for (let i = 0; i < children.length; i++) {
if (children[i] === parent) return i;
}
return null;
// 槽位下标 = 格子下标(gridN → N),temp 与其他节点返回 null
return TextListLandHost.indexOfNode(parent);
}
/** 拖放源以节点实际挂接位置为准,location 仅作兜底 */
@ -4549,9 +4721,13 @@ export class CocosBagBoardView {
this._scheduleComposeFxFlush();
break;
case MergeEventType.BagMerged:
// 待解锁格吃到的合成(相同类型相同等级)解锁下标 +1 的格子
// 待开启格吃到同类型同等级:自身变开启,四周未解锁格变待开启并各生成一个字
if (this._land.stateOf(event.slotIndex) === TextListCellState.OpenNoDrag) {
this._land.unlockNext(event.slotIndex);
const born = this._land.promoteAround(event.slotIndex);
for (let i = 0; i < born.length; i++) {
this._bagSession?.fillGateUnlockSlot(born[i]);
}
this._fillMissingBagSlotGrids();
}
this._reportMergeUpgraded(event.weapon1Id, event.level);
if (this._dropSyncPhase === 'awaitingCommit') {
@ -4819,13 +4995,12 @@ export class CocosBagBoardView {
): void {
if (!this._bagSession || !this._bagList) return;
const slots = this._bagSession.getBagSlots();
const children = this._bagList.children;
const bagNeedSpawn: number[] = [];
const mutableFlight = [...flightGrids];
for (let i = 0; i < slots.length; i++) {
const record = slots[i];
const slotNode = children[i];
const slotNode = this._bagSlotNodeAt(i);
if (!record || !slotNode) {
if (record && !slotNode) {
console.log('[CocosBagBoardView] 背包槽节点缺失', 'slot=', i, 'weapon2Id=', record.weapon2Id);
@ -4905,7 +5080,7 @@ export class CocosBagBoardView {
for (let n = 0; n < bagNeedSpawn.length; n++) {
const i = bagNeedSpawn[n];
const record = slots[i];
const slotNode = children[i];
const slotNode = this._bagSlotNodeAt(i);
if (!record || !slotNode?.isValid) continue;
const entity = buildTextGridEntityData(
{ kind: 'bag', slotIndex: i },
@ -5202,7 +5377,11 @@ export class CocosBagBoardView {
this._writeGridData(existing, entity);
existing.rememberOrigin();
existing.node.name = 'textGrid';
existing.enableDrag(this._canDrag() && !BagSlotEntranceAnimator.isPlaying(existing));
existing.enableDrag(
entity.location.kind === 'bag'
? this._canEnableBagGridDrag(existing)
: this._canDrag() && !BagSlotEntranceAnimator.isPlaying(existing),
);
this._ensureMapClickHandler(existing);
return existing;
}
@ -5448,7 +5627,7 @@ export class CocosBagBoardView {
private _applyBagUpgradeInPlace(slotIndex: number, newLevel: number): boolean {
if (!this._bagSession || !this._bagList) return false;
if (!this._bagSession.getBagSlots()[slotIndex]) return false;
const slotNode = this._bagList.children[slotIndex];
const slotNode = this._bagSlotNodeAt(slotIndex);
if (!slotNode?.isValid) return false;
const grid = this._resolveBagTextGridForUpgrade(slotIndex, slotNode);
@ -5490,7 +5669,7 @@ export class CocosBagBoardView {
private _applyBagGlyphRewriteInPlace(slotIndex: number): boolean {
if (!this._bagSession || !this._bagList) return false;
if (!this._bagSession.getBagSlots()[slotIndex]) return false;
const slotNode = this._bagList.children[slotIndex];
const slotNode = this._bagSlotNodeAt(slotIndex);
if (!slotNode?.isValid) return false;
const grid = this._resolveBagTextGridForUpgrade(slotIndex, slotNode);
@ -5817,9 +5996,12 @@ export class CocosBagBoardView {
}
}
/** 背包字块:入场未落地前不可拖 */
/** 背包字块:入场未落地、待解锁格、空装饰格不可拖 */
private _canEnableBagGridDrag(grid: TextGrid): boolean {
return this._canDrag() && !BagSlotEntranceAnimator.isPlaying(grid);
if (!this._canDrag() || BagSlotEntranceAnimator.isPlaying(grid)) return false;
const loc = grid.location;
if (loc?.kind === 'bag' && this._land.isNoDrag(loc.slotIndex)) return false;
return true;
}
/** 入场未落地的字块不显示可合成箭头 */
@ -5834,7 +6016,7 @@ export class CocosBagBoardView {
const enabled = this._canDrag();
for (const g of this._slotGrids.values()) {
if (!g?.isValid) continue;
g.enableDrag(enabled && !BagSlotEntranceAnimator.isPlaying(g));
g.enableDrag(this._canEnableBagGridDrag(g));
this._ensureMapClickHandler(g);
}
for (const g of this._mapGrids.values()) {
@ -6017,7 +6199,7 @@ export class CocosBagBoardView {
/** ghost 创建后:确认源字仍挂在背包槽,被偷走则立刻挂回并补齐 */
private _assertBagSourceStillInSlot(grid: TextGrid, source: DragSource): void {
if (source.kind !== 'bag') return;
const slotNode = this._bagList?.children[source.slotIndex];
const slotNode = this._bagSlotNodeAt(source.slotIndex);
BagTextDragDebugLog.slotChildren(source.slotIndex, slotNode, 'afterGhost');
if (!slotNode?.isValid) return;
if (grid.node?.isValid && grid.node.parent === slotNode) {
@ -6095,7 +6277,7 @@ export class CocosBagBoardView {
const anchor = this._lineDragAnchorWorld(grid, drag);
if (anchor) return anchor.clone();
if (drag.source.kind === 'bag') {
const slotNode = this._bagList?.children[drag.source.slotIndex];
const slotNode = this._bagSlotNodeAt(drag.source.slotIndex);
if (slotNode?.isValid) return slotNode.worldPosition.clone();
} else {
const mapIndex = drag.mapDrag?.fromGridIndex ?? drag.source.gridIndex;
@ -6296,7 +6478,7 @@ export class CocosBagBoardView {
/** 起飞后清原格残留 TextGrid(飞走的节点已挂到 dragLayer) */
private _purgeVacatedCell(from: GridDetachSource): void {
const parent = from.kind === 'bag'
? this._bagList?.children[from.slotIndex]
? this._bagSlotNodeAt(from.slotIndex)
: this._map?.getMapGridByIndex(from.gridIndex)?.node;
if (!parent?.isValid) return;
for (const child of [...parent.children]) {
@ -6798,7 +6980,7 @@ export class CocosBagBoardView {
return mg?.node?.isValid ? mg.node.worldPosition.clone() : null;
}
const slotNode = this._bagList?.children[target.index];
const slotNode = this._bagSlotNodeAt(target.index);
const centerNode = grid.node?.isValid ? grid.node : slotNode;
return centerNode?.isValid ? centerNode.worldPosition.clone() : null;
}
@ -6929,7 +7111,7 @@ export class CocosBagBoardView {
/** 背包槽 select:有 textGrid 时高亮字块,空槽时高亮槽位上的 select 节点 */
private _setBagSlotSelect(slotIndex: number, selected: boolean): void {
const slotNode = this._bagList?.children[slotIndex];
const slotNode = this._bagSlotNodeAt(slotIndex);
const slotSelect = slotNode?.getChildByName('select') ?? null;
const tg = this._slotGrids.get(slotIndex);
@ -7261,7 +7443,7 @@ export class CocosBagBoardView {
if (claimed.target === 'bag') {
const slotIndex = claimed.slotIndex;
const slotNode = this._bagList?.children[slotIndex];
const slotNode = this._bagSlotNodeAt(slotIndex);
if (!slotNode?.isValid) {
settle(false);
return false;
@ -7553,7 +7735,7 @@ export class CocosBagBoardView {
return false;
}
const slotNode = this._bagList?.children[slotIndex];
const slotNode = this._bagSlotNodeAt(slotIndex);
if (!slotNode?.isValid) {
onComplete(false);
return false;
@ -7668,7 +7850,7 @@ export class CocosBagBoardView {
grid.setDragFollow(true);
if (source.kind === 'bag') {
const slotNode = this._bagList?.children[source.slotIndex];
const slotNode = this._bagSlotNodeAt(source.slotIndex);
if (!slotNode?.isValid) return false;
if (grid.node.parent !== slotNode) {
grid.node.parent = slotNode;
@ -7709,7 +7891,7 @@ export class CocosBagBoardView {
const target: DropTarget = { kind: 'map', gridIndex: mapIndex };
const targetWorld = mapGrid.node.worldPosition.clone();
const bagSlotNode = this._bagList?.children[slotIndex];
const bagSlotNode = this._bagSlotNodeAt(slotIndex);
const displacedGrid = this._mapGrids.get(mapIndex) ?? null;
const swapToBag = displacedGrid?.node?.isValid
&& bagSlotNode?.isValid
@ -7785,8 +7967,8 @@ export class CocosBagBoardView {
toSlot: number,
finish: (ok: boolean) => void,
): void {
const targetBagNode = this._bagList?.children[toSlot];
const sourceBagNode = this._bagList?.children[fromSlot];
const targetBagNode = this._bagSlotNodeAt(toSlot);
const sourceBagNode = this._bagSlotNodeAt(fromSlot);
if (!targetBagNode?.isValid || !sourceBagNode?.isValid || !this._bagSession) {
finish(false);
return;

@ -4,6 +4,7 @@ import type { ComposedUnit } from 'db://assets/mx/module/merge';
import { Weapon2TileType } from 'db://assets/mx/module/merge';
import { GEvent } from 'db://assets/mx/module/event/GEvent';
import { GBundle, GPath } from '../../game/ConfigRes';
import { TableNames } from '../../game/ConfigTableData';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { PEACH_TREE_WEAPON2_ID, SHOVEL_WEAPON2_ID } from '../bag/BagPrepConfig';
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
@ -27,6 +28,7 @@ import {
GLYPH_ACTIVE_SKIN_BUCKETS,
GLYPH_SKIN_CAMP_ALLY,
GLYPH_SKIN_CAMP_ENEMY,
glyphSpineHasSkin,
resolveActiveGlyphSkin,
} from './GlyphSpineSkin';
import 'db://assets/mx/extension/NodeDragExt';
@ -154,8 +156,8 @@ export interface TextGridData extends TileEntitySnapshot {
/**
* 背包/地图字块视图(textGrid 预制体)。
* 普通字:skin 节点按 Text.icon 切 Spine;铲:隐藏 skin,显示「铲」节点;
* 蟠桃树(102):显示「桃/桃树」Spine,按当前等级 Skill.cd 前半/后半循环桃字1/2,产桃播桃字3。
* 五行字:不显示 lv;按 Hero(herotype=Text.type, level=当前显示等级).number 打开同名节点,并切金/木/水/火/土皮。
* 铲:显示「铲」节点;蟠桃树(102):显示「桃/桃树」Spine。
*/
@ccclass('TextGrid')
export class TextGrid extends Component {
@ -197,6 +199,8 @@ export class TextGrid extends Component {
private _originParent: Node | null = null;
private _originPos = { x: 0, y: 0, z: 0 };
private _labelLv: Label | null = null;
/** 当前五行数字皮节点(子节点名 = Hero.number) */
private _numberNode: Node | null = null;
private _clickStart: Vec2 | null = null;
private _entity: TileEntitySnapshot | null = null;
private _dragFollow = true;
@ -253,6 +257,7 @@ export class TextGrid extends Component {
this._peachNode = this.node.getChildByName(PEACH_TREE_NODE);
if (this._peachNode?.isValid) this._peachNode.active = false;
this._labelLv = this.node.getChildByName('lv')?.getComponent(Label) ?? null;
if (this._labelLv?.node?.isValid) this._labelLv.node.active = false;
this.selectNode = this.node.getChildByName('select');
this.redSelectNode = this.node.getChildByName('redSelect');
this._updateSelect();
@ -774,6 +779,7 @@ export class TextGrid extends Component {
private _syncYingCampState(active: boolean): void {
if (!active) {
this._releaseYingCampDisplay();
this._updateLevelLabel();
if (this._skinNode?.isValid) {
this._skinNode.active = true;
this.resetSkinPresentation();
@ -784,7 +790,7 @@ export class TextGrid extends Component {
this._ensureYingCampDisplay();
if (!this._yingCampDisplay?.isValid) return;
this._yingCampDisplay.active = true;
if (this._skinNode?.isValid) this._skinNode.active = false;
this._setGlyphFaceActive(false);
const anim = this._getYingCampAnim();
if (!anim?.isValid) {
console.log('[TextGrid] 营展示 Spine 未就绪', this.weapon2Id);
@ -864,15 +870,21 @@ export class TextGrid extends Component {
HeroActorHost.instance.destroyAtGrid(prevLoc.gridIndex);
}
if (this._isShovelTile()) {
if (this._isElementNumberTile()) {
this._hideShovelPresentation();
this._hidePeachTreePresentation();
} else if (this._isShovelTile()) {
this._hidePeachTreePresentation();
this._hideNumberNodes();
this._applyShovelPresentation();
} else if (this._isPeachTreeTile()) {
this._hideShovelPresentation();
this._hideNumberNodes();
this._applyPeachTreePresentation();
} else {
this._hideShovelPresentation();
this._hidePeachTreePresentation();
this._hideNumberNodes();
this._ensureSkinNode();
const iconName = textIconName(data.tile);
this._skinIconName = null;
@ -948,10 +960,18 @@ export class TextGrid extends Component {
this.resetNodeTransform();
}
/** 入场落地后恢复可见性:铲只显铲,字按 HeroActor 态同步 */
/** 入场落地后恢复可见性:五行数字皮优先,铲只显铲,字按 HeroActor 态同步 */
syncPresentationVisibility(): void {
if (!this.isValid) return;
if (this._isElementNumberTile()) {
this._hideShovelPresentation();
this._hidePeachTreePresentation();
this._updateLevelLabel();
this._syncSkillActiveState();
return;
}
if (this._isShovelTile()) {
this._hideNumberNodes();
this._applyShovelPresentation();
return;
}
@ -988,7 +1008,18 @@ export class TextGrid extends Component {
this.resetNodeTransform();
this.resetSkinPresentation();
if (this._isShovelTile()) {
if (this._isElementNumberTile()) {
this._hideShovelPresentation();
this._hidePeachTreePresentation();
this._updateLevelLabel();
const sk = this._numberNode?.getComponent(sp.Skeleton) ?? null;
if (sk?.isValid && !sk.enabled) {
sk.enabled = true;
repaired = true;
}
this._syncShadow();
} else if (this._isShovelTile()) {
this._hideNumberNodes();
this._applyShovelPresentation();
} else if (this._isPeachTreeTile()) {
this._applyPeachTreePresentation();
@ -1037,7 +1068,14 @@ export class TextGrid extends Component {
/** 松手飞入目标格前:组合/英雄皮可能被 HeroActor 隐藏,先恢复可见再播弧线 */
preparePlaceFlightVisual(): void {
if (this._isElementNumberTile()) {
this._hideShovelPresentation();
this._hidePeachTreePresentation();
this._updateLevelLabel();
return;
}
if (this._isShovelTile()) {
this._hideNumberNodes();
if (this._shovelNode?.isValid) this._shovelNode.active = true;
return;
}
@ -1074,6 +1112,8 @@ export class TextGrid extends Component {
this.resetNodeTransform();
this.resetSkinPresentation();
this._skinIconName = null;
this._hideNumberNodes();
this._hideLevelLabel();
hideGridUpgradeSpine(this.node);
this._hideShovelDigAnim();
this._hidePeachTreePresentation();
@ -1810,8 +1850,9 @@ export class TextGrid extends Component {
return this.weapon2Id === PEACH_TREE_WEAPON2_ID;
}
/** 抖动/复位用的表现根:铲用「铲」、蟠桃树用「桃」、其余用 skin */
/** 抖动/复位用的表现根:五行数字皮、铲、蟠桃树,其余用 skin */
private _presentationRoot(): Node {
if (this._isElementNumberTile() && this._numberNode?.isValid) return this._numberNode;
if (this._isShovelTile() && this._shovelNode?.isValid) return this._shovelNode;
if (this._isPeachTreeTile() && this._peachNode?.isValid) return this._peachNode;
if (this._skinNode?.isValid) return this._skinNode;
@ -1827,7 +1868,8 @@ export class TextGrid extends Component {
}
const node = this._shadowNode;
if (!node?.isValid) return;
node.active = !this._isShovelTile()
const shovelFace = this._isShovelTile() && !this._isElementNumberTile();
node.active = !shovelFace
&& !this._isPeachTreeTile()
&& !this._shouldHideSkinForHeroActor();
}
@ -1837,6 +1879,7 @@ export class TextGrid extends Component {
this._ensureShovelNode();
this._hideShovelDigAnim();
this._skinIconName = null;
this._hideNumberNodes();
if (this._skinNode?.isValid) {
Tween.stopAllByTarget(this._skinNode);
this._skinNode.setPosition(0, 0, 0);
@ -1889,6 +1932,7 @@ export class TextGrid extends Component {
private _applyPeachTreePresentation(): void {
this._ensurePeachNode();
this._skinIconName = null;
this._hideNumberNodes();
if (this._skinNode?.isValid) {
Tween.stopAllByTarget(this._skinNode);
this._skinNode.setPosition(0, 0, 0);
@ -2135,49 +2179,160 @@ export class TextGrid extends Component {
if (trail3?.isValid) trail3.active = use3;
}
private _updateLevelLabel(): void {
/** 关掉 lv。五行字改由数字节点显示 Hero.number */
private _hideLevelLabel(): void {
if (!this._labelLv?.isValid) {
this._labelLv = this.node.getChildByName('lv')?.getComponent(Label) ?? null;
}
if (!this._labelLv) return;
WeaponComposeConfigProvider.instance.load();
const provider = WeaponComposeConfigProvider.instance;
const w2Type = provider.getWeapon2Type(this.weapon2Id) as Weapon2TileType;
if (w2Type === Weapon2TileType.Grid || w2Type === Weapon2TileType.Shovel) {
if (!this._isPeachTreeTile()) {
this._labelLv.node.active = false;
return;
}
if (this._labelLv?.node?.isValid) this._labelLv.node.active = false;
}
/**
* 五行字:Text.type 为 1–5(金木水火土)。
* 与旧 Weapon2 类型(单字/组合/格子/铲)不是同一套编号。
*/
private _isElementNumberTile(): boolean {
const type = Math.floor(Number(this._entity?.tile?.type));
return type >= 1 && type <= 5;
}
/** 子节点名是否为纯数字(对应 Hero.number) */
private _isNumberFaceName(name: string): boolean {
if (!name) return false;
let n = 0;
for (let i = 0; i < name.length; i++) {
const c = name.charCodeAt(i);
if (c < 48 || c > 57) return false;
n = n * 10 + (c - 48);
}
return n > 0;
}
/** Hero.herotype → 数字 Spine 皮肤名 */
private _elementSkinName(herotype: number): string {
switch (herotype) {
case 1: return '金';
case 2: return '木';
case 3: return '水';
case 4: return '火';
case 5: return '土';
default: return '';
}
}
/** 按 herotype 与显示等级查 Hero。显示等级含段位加值与合成升级,不是 Text.level */
private _heroByTypeLevel(herotype: number, level: number): ITableHero | null {
const heroes = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
for (let i = 0; i < heroes.length; i++) {
const hero = heroes[i];
if (Math.floor(Number(hero.herotype)) !== herotype) continue;
if (Math.floor(Number(hero.level)) !== level) continue;
return hero;
}
return null;
}
/** 关掉全部数字皮节点 */
private _hideNumberNodes(): void {
if (!this.node?.isValid) return;
const children = this.node.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (!child?.isValid || !this._isNumberFaceName(child.name)) continue;
child.active = false;
}
this._numberNode = null;
}
/** 旧 skin 与当前数字皮一起显隐 */
private _setGlyphFaceActive(active: boolean): void {
if (this._skinNode?.isValid) this._skinNode.active = active;
if (!this._numberNode?.isValid) return;
this._numberNode.active = active;
if (!active) return;
const sk = this._numberNode.getComponent(sp.Skeleton);
if (sk?.isValid) sk.enabled = true;
}
/**
* 打开 Hero.number 同名节点并切五行皮。
* @param show 是否可见(成将/营盖住时仍选中节点但保持关闭)
*/
private _applyNumberGlyph(show: boolean): void {
const tile = this._entity?.tile;
if (!tile) {
this._hideNumberNodes();
return;
}
const herotype = Math.floor(Number(tile.type));
const level = this._resolveDisplayLevel();
if (w2Type === Weapon2TileType.Single || this._isPeachTreeTile()) {
this._labelLv.node.active = true;
this._labelLv.string = `${level}`;
if (this._levelDown) this._applyLevelDownLvColor();
const hero = this._heroByTypeLevel(herotype, level);
if (!hero) {
console.log('[TextGrid] 无英雄编号', this.weapon2Id, 'type=', herotype, 'level=', level);
this._hideNumberNodes();
return;
}
// type2 组合字:多格组合时仅最后一字显示等级
const composition = this._getComposition();
if (composition && composition.gridIndices.length >= 2
&& !provider.isSingleCellWeapon1(composition.weapon1Id)) {
const show = this._isLastTileInComposition(composition);
this._labelLv.node.active = show;
if (show) this._labelLv.string = `${level}`;
if (this._levelDown) this._applyLevelDownLvColor();
const num = Math.floor(Number(hero.number));
if (!(num > 0)) {
console.log('[TextGrid] Hero.number 无效', hero.id, hero.number);
this._hideNumberNodes();
return;
}
const show = level > 1;
this._labelLv.node.active = show;
if (show) this._labelLv.string = `${level}`;
if (this._levelDown) this._applyLevelDownLvColor();
const node = this.node.getChildByName(String(num));
if (!node?.isValid || !this._isNumberFaceName(node.name)) {
console.log('[TextGrid] 数字节点缺失', num, 'hero=', hero.id);
this._hideNumberNodes();
return;
}
const skinName = this._elementSkinName(herotype);
if (!skinName) {
console.log('[TextGrid] 五行皮肤未定义', herotype, 'hero=', hero.id);
this._hideNumberNodes();
return;
}
this._hideNumberNodes();
this._numberNode = node;
node.active = true;
this._playElementSkin(node, skinName);
node.active = show;
}
/** 组合方向上的最后一格(横→最右,竖→最下) */
private _isLastTileInComposition(composition: ComposedUnit): boolean {
const loc = this.location;
if (loc?.kind !== 'map') return false;
const lastIdx = composition.gridIndices[composition.gridIndices.length - 1];
return loc.gridIndex === lastIdx;
/** 数字节点切五行皮并播待机 */
private _playElementSkin(node: Node, skinName: string): void {
const sk = node.getComponent(sp.Skeleton);
if (!sk?.isValid) {
console.log('[TextGrid] 数字节点缺少 Skeleton', node.name, this.weapon2Id);
return;
}
sk.enabled = true;
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
const sd = sk.skeletonData;
if (!sd?.isValid) {
console.log('[TextGrid] 数字节点 skeletonData 无效', node.name, this.weapon2Id);
return;
}
ensureSpineSkeletonDataRuntime(sd);
if (!glyphSpineHasSkin(sk, skinName)) {
console.log('[TextGrid] 数字皮不存在', skinName, node.name, this.weapon2Id);
return;
}
sk.setSkin(skinName);
sk.setSlotsToSetupPose();
const runtime = sd.getRuntimeData?.(true);
if (runtime?.findAnimation?.('待机')) {
sk.setAnimation(0, '待机', true);
}
}
/** 隐藏 lv,并按当前显示等级刷新五行数字皮 */
private _updateLevelLabel(): void {
this._hideLevelLabel();
if (!this._isElementNumberTile()) {
this._hideNumberNodes();
return;
}
const yingCover = !!this._yingCampDisplay?.isValid && this._yingCampDisplay.active;
this._applyNumberGlyph(!this._shouldHideGridSkin() && !yingCover);
}
/** 显示等级:以 setData 写入的 level 为准(与 tile / 组合数据同步) */
@ -2185,7 +2340,7 @@ export class TextGrid extends Component {
return this.level;
}
/** 地图字块等级变更后刷新 lv 与等级皮(免全量 setData) */
/** 地图字块等级变更后刷新数字皮(免全量 setData) */
applyMapLevel(level: number): void {
if (!this.isValid) return;
const lv = Math.max(1, Math.floor(level) || 1);
@ -2355,7 +2510,8 @@ export class TextGrid extends Component {
/** 地图字块已组合为攻击单位 → 激活;拆开或未组合 → 关闭;组合字切换 Spine 皮肤 */
private _syncSkillActiveState(): void {
this._syncShadow();
if (this._isShovelTile() || this._isPeachTreeTile()) return;
if (this._isPeachTreeTile()) return;
if (this._isShovelTile() && !this._isElementNumberTile()) return;
const composition = this._getComposition();
const active = this._isMapComposedAttackUnit();
if (this._aiMapLite) {
@ -2374,10 +2530,9 @@ export class TextGrid extends Component {
return;
}
const skin = this._skinNode?.isValid ? this._skinNode : null;
if (!skin) return;
const skill = skin.getComponent(Skill) ?? skin.getComponentInChildren(Skill);
const skill = skin?.getComponent(Skill) ?? skin?.getComponentInChildren(Skill) ?? null;
if (!skill) {
skin.active = !this._shouldHideSkinForHeroActor();
this._setGlyphFaceActive(!this._shouldHideSkinForHeroActor());
return;
}
const w2Type = WeaponComposeConfigProvider.instance.getWeapon2Type(this.weapon2Id) as Weapon2TileType;
@ -2456,7 +2611,8 @@ export class TextGrid extends Component {
}
private _syncHeroActorState(composition: ComposedUnit | null, active: boolean): void {
if (this._isShovelTile() || this._isPeachTreeTile()) return;
if (this._isPeachTreeTile()) return;
if (this._isShovelTile() && !this._isElementNumberTile()) return;
const loc = this.location;
if (active && composition) {
const map = gg.game.CurentBattle?.Map;
@ -2470,9 +2626,7 @@ export class TextGrid extends Component {
}
const useHeroActor = isHeroActorWeapon1(composition.weapon1Id);
const displayReady = this._isHeroDisplayOnThisCell(composition);
if (this._skinNode?.isValid) {
this._skinNode.active = !this._shouldHideSkinForHeroActor();
}
this._setGlyphFaceActive(!this._shouldHideSkinForHeroActor());
// 皮可见 ≠ 已挂 Skill:entityId 晚到时必须继续对账,否则有皮无武器、不攻击
const skillReady = !useHeroActor
|| loc?.kind !== 'map'
@ -2484,7 +2638,8 @@ export class TextGrid extends Component {
const token = this._heroActorRetryToken;
this.scheduleOnce(() => {
if (!this.isValid || token !== this._heroActorRetryToken) return;
if (this._isShovelTile() || this._isPeachTreeTile()) return;
if (this._isPeachTreeTile()) return;
if (this._isShovelTile() && !this._isElementNumberTile()) return;
this._syncHeroActorState(composition, active);
}, delay);
} else if (useHeroActor) {
@ -2500,11 +2655,9 @@ export class TextGrid extends Component {
return;
}
this._heroActorRetry = 0;
if (this._skinNode?.isValid) {
this._skinNode.active = true;
this.resetSkinPresentation();
this._resetGlyphSkinToBagIdle();
}
this._setGlyphFaceActive(true);
this.resetSkinPresentation();
this._resetGlyphSkinToBagIdle();
// 仅当地图格曾属于已激活组合时才拆 HeroActor;散落组合字(如单独「空」)勿误伤邻格
if (loc?.kind === 'map' && this._shouldDestroyHeroAtDeactivate(loc.gridIndex)) {
HeroActorHost.instance.destroyAtGrid(loc.gridIndex);

@ -3,24 +3,35 @@ import { TableNames } from '../../game/ConfigTableData';
/** ui_textList 格子状态 */
export enum TextListCellState {
/** 开启,可按背包规则拖动 */
/** 开启:可拖动、可合成;开局不放字 */
Open = 1,
/** 开启但不能拖;其他字拖入后解锁下标 +1 */
/** 待开启:不可拖动、可合成;开局或被邻格解锁时放字 */
OpenNoDrag = 2,
/** 未解锁 */
/** 未解锁:不可拖、不可落字 */
Locked = 3,
/** 空装饰格:不刷底、不出字、不可拖放 */
Blank = 4,
}
/** 格子节点上的开启底 */
const OPEN_NODE = '背包格开启';
/** 格子节点上的未开启底 */
const LOCKED_NODE = '背包格未开启';
/** 格子节点上的待开启底 */
const GATE_NODE = '待开启';
/** 模板节点,不参与状态 */
const TEMP_NODE = 'temp';
/** 空装饰格下标(grid17):预制体底图保持关闭,逻辑不处理 */
const BLANK_INDEX = 17;
/**
* 按关卡 normalland / tolockland 显示 ui_textList 格子,并处理待解锁格的拖入解锁。
* normalland 为开启;tolockland 为开启但不能拖;其余为未解锁。名为 temp 的节点跳过。
* 按关卡 normalland / tolockland 管理 ui_textList 格子状态与拖入解锁。
* normalland 为开启且开局不放字;tolockland 为待开启(有字、不能拖、靠同类型同等级合成打开)。
* 待开启被合成后自身变为开启,上下左右的未解锁格变为待开启。其余为未解锁。
* grid17 是空装饰格,不刷底、不出字、不可拖放。名为 temp 的节点跳过。
* 格子上的字块是背包槽位数据(BattleBagSession),由视图按槽位记录生成标准 TextGrid;
* 拖拽预览、合成解锁走既有背包管线,本类只负责格子底图状态与解锁规则。
*/
export class TextListLandHost {
/** ui_textList */
@ -60,6 +71,8 @@ export class TextListLandHost {
this._useTable = true;
this._fill(this._open, normal, 'normalland');
this._fill(this._gate, gate, 'tolockland');
this._stripBlank(this._open, 'normalland');
this._stripBlank(this._gate, 'tolockland');
for (let i = this._gate.length - 1; i >= 0; i--) {
if (!this._has(this._open, this._gate[i])) continue;
console.log('[TextListLand] 下标同时在开启与待解锁,按开启', this._gate[i]);
@ -77,20 +90,19 @@ export class TextListLandHost {
}
/**
* 节点对应的格子下标。temp 与无法解析的名字返回 null。
* 节点对应的格子下标。命名 gridN → N(grid1-23),temp 跳过,其余名字打日志返回 null。
* @param node ui_textList 子节点
*/
static indexOfNode(node: Node | null | undefined): number | null {
if (!node?.isValid) return null;
if (node.name === TEMP_NODE) return null;
if (node.name === 'uigrid') return 0;
const matched = /^uigrid-(\d+)$/.exec(node.name);
const matched = /^grid(\d+)$/.exec(node.name);
if (!matched) {
console.log('[TextListLand] 无法解析格子下标', node.name);
return null;
}
const index = parseInt(matched[1], 10);
if (!Number.isFinite(index) || index < 0) {
if (!Number.isFinite(index) || index <= 0) {
console.log('[TextListLand] 格子下标非法', node.name);
return null;
}
@ -102,6 +114,7 @@ export class TextListLandHost {
* @param index 格子下标
*/
stateOf(index: number): TextListCellState {
if (this.isBlank(index)) return TextListCellState.Blank;
if (!this._useTable) return TextListCellState.Open;
if (this._has(this._open, index)) return TextListCellState.Open;
if (this._has(this._gate, index)) return TextListCellState.OpenNoDrag;
@ -109,35 +122,73 @@ export class TextListLandHost {
}
/**
* 待解锁格禁止把字拖走。
* 是否空装饰格(grid17)。
* @param index 格子下标
*/
isBlank(index: number): boolean {
return index === BLANK_INDEX;
}
/**
* 待解锁格与空装饰格禁止把字拖走。
* @param index 格子下标
*/
isNoDrag(index: number): boolean {
return this.stateOf(index) === TextListCellState.OpenNoDrag;
const state = this.stateOf(index);
return state === TextListCellState.OpenNoDrag || state === TextListCellState.Blank;
}
/**
* 其他字拖到待解锁格:把下标 +1 的未解锁格改为开启。
* @param gateIndex 待解锁格下标
* 待开启格被同类型同等级合成:自身改为开启,上下左右的未解锁格改为待开启。
* @param gateIndex 待开启格下标
* @returns 新变成待开启、需要生成文字的格子
*/
unlockNext(gateIndex: number): void {
if (!this.isNoDrag(gateIndex)) {
console.log('[TextListLand] 非待解锁格', gateIndex);
return;
promoteAround(gateIndex: number): number[] {
if (this.isBlank(gateIndex) || this.stateOf(gateIndex) !== TextListCellState.OpenNoDrag) {
console.log('[TextListLand] 非待开启格', gateIndex);
return [];
}
const next = gateIndex + 1;
const node = this._nodeAt(next);
if (!node) {
console.log('[TextListLand] 下标+1 没有可解锁格子', gateIndex, next);
return;
const gateAt = this._gate.indexOf(gateIndex);
if (gateAt >= 0) this._gate.splice(gateAt, 1);
if (!this._has(this._open, gateIndex)) this._open.push(gateIndex);
const self = this._nodeAt(gateIndex);
if (self) this._applyNode(self, TextListCellState.Open);
const born: number[] = [];
const neighbors = this._orthogonalNeighbors(gateIndex);
for (let i = 0; i < neighbors.length; i++) {
const index = neighbors[i];
if (this.stateOf(index) !== TextListCellState.Locked) continue;
this._gate.push(index);
const node = this._nodeAt(index);
if (node) this._applyNode(node, TextListCellState.OpenNoDrag);
born.push(index);
}
if (this.stateOf(next) === TextListCellState.Open) return;
if (this.stateOf(next) !== TextListCellState.Locked) {
console.log('[TextListLand] 下标+1 已不是未解锁', next, this.stateOf(next));
return;
return born;
}
/**
* 上下左右相邻格(按节点坐标,间距约一格)。空装饰格不算。
* @param index 中心格下标
*/
private _orthogonalNeighbors(index: number): number[] {
const origin = this._nodeAt(index);
const list = this._list;
if (!origin?.isValid || !list?.isValid) return [];
const ox = origin.position.x;
const oy = origin.position.y;
const out: number[] = [];
const children = list.children;
for (let i = 0; i < children.length; i++) {
const other = TextListLandHost.indexOfNode(children[i]);
if (other == null || other === index || this.isBlank(other)) continue;
const dx = Math.abs(children[i].position.x - ox);
const dy = Math.abs(children[i].position.y - oy);
const horizontal = dy < 30 && dx > 50 && dx < 120;
const vertical = dx < 30 && dy > 50 && dy < 120;
if (horizontal || vertical) out.push(other);
}
this._open.push(next);
this._applyNode(node, TextListCellState.Open);
return out;
}
/**
@ -159,45 +210,50 @@ export class TextListLandHost {
}
}
/** 按当前开启/待解锁刷新全部格子 */
private _applyFromTable(): void {
/** 全部格子按状态刷底图 */
private _applyAll(state: TextListCellState): void {
const list = this._list;
if (!list?.isValid) return;
const children = list.children;
for (let i = 0; i < children.length; i++) {
const node = children[i];
const index = TextListLandHost.indexOfNode(node);
if (index == null) continue;
this._applyNode(node, this.stateOf(index));
const index = TextListLandHost.indexOfNode(children[i]);
if (index == null || this.isBlank(index)) continue;
this._applyNode(children[i], state);
}
this._warnMissing(this._open);
this._warnMissing(this._gate);
}
/**
* 配表下标在列表里没有对应格子时打日志。
* @param indices 下标
*/
private _warnMissing(indices: readonly number[]): void {
for (let i = 0; i < indices.length; i++) {
if (this._nodeAt(indices[i])) continue;
console.log('[TextListLand] 下标没有可显示格子', indices[i]);
/** 按配表状态刷底图 */
private _applyFromTable(): void {
const list = this._list;
if (!list?.isValid) return;
const children = list.children;
for (let i = 0; i < children.length; i++) {
const cell = children[i];
const index = TextListLandHost.indexOfNode(cell);
if (index == null || this.isBlank(index)) continue;
this._applyNode(cell, this.stateOf(index));
}
}
/**
* 未配置地块时统一刷一种状态。
* @param state 状态
* 切换单格底图:开启显示「背包格开启」,待开启显示「待开启」,未解锁显示「背包格未开启」。
* @param cell 格子节点
* @param state 目标状态
*/
private _applyAll(state: TextListCellState): void {
const list = this._list;
if (!list?.isValid) return;
const children = list.children;
for (let i = 0; i < children.length; i++) {
const node = children[i];
if (TextListLandHost.indexOfNode(node) == null) continue;
this._applyNode(node, state);
private _applyNode(cell: Node, state: TextListCellState): void {
const openNode = cell.getChildByName(OPEN_NODE);
const lockedNode = cell.getChildByName(LOCKED_NODE);
const gateNode = cell.getChildByName(GATE_NODE);
if (!openNode && !lockedNode && !gateNode) {
console.log('[TextListLand] 格子缺少底图节点', cell.name);
return;
}
if (state === TextListCellState.OpenNoDrag && !gateNode) {
console.log('[TextListLand] 格子缺少待开启节点', cell.name);
}
if (openNode) openNode.active = state === TextListCellState.Open;
if (lockedNode) lockedNode.active = state === TextListCellState.Locked;
if (gateNode) gateNode.active = state === TextListCellState.OpenNoDrag;
}
/**
@ -215,31 +271,19 @@ export class TextListLandHost {
}
/**
* 开启显示背包格开启,未解锁显示背包格未开启。
* @param node 格子节点
* @param state 状态
* 配表若写了空装饰格,去掉并打日志。
* @param arr 下标数组
* @param label 字段名
*/
private _applyNode(node: Node, state: TextListCellState): void {
const openNode = node.getChildByName(OPEN_NODE);
const lockedNode = node.getChildByName(LOCKED_NODE);
if (!openNode?.isValid || !lockedNode?.isValid) {
console.log('[TextListLand] 缺少开关节点', node.name);
return;
}
const opened = state !== TextListCellState.Locked;
openNode.active = opened;
lockedNode.active = !opened;
private _stripBlank(arr: number[], label: string): void {
const idx = arr.indexOf(BLANK_INDEX);
if (idx < 0) return;
console.log('[TextListLand] 空装饰格不参与地块配置', label, BLANK_INDEX);
arr.splice(idx, 1);
}
/**
* 数组是否包含下标。
* @param list 下标列表
* @param index 下标
*/
private _has(list: readonly number[], index: number): boolean {
for (let i = 0; i < list.length; i++) {
if (list[i] === index) return true;
}
return false;
/** 数组是否包含 */
private _has(arr: readonly number[], value: number): boolean {
return arr.indexOf(value) >= 0;
}
}

@ -402,7 +402,9 @@ declare global {
15、目标圆形范围
16、扇形多段
17、不穿透散射箭
18、目标圆形范围多段 */
18、目标圆形范围多段
19、全屏直线弹道
20、目标圆形范围多段 */
readonly atk_type: string;
/** 普攻投射物 */
readonly fly: string;
@ -424,7 +426,9 @@ declare global {
15、半径,伤害系数
16、伤害段数,伤害间隔,角度,伤害系数
17、散射箭数量,角度,伤害系数
18、伤害段数,伤害间隔,半径,伤害系数 */
18、伤害段数,伤害间隔,半径,伤害系数
19、数量,伤害系数
20、伤害段数,伤害间隔,半径,伤害系数 */
readonly atk_para: string;
/** 命中后效果:buff类型,参数
1、眩晕,参数:概率,眩晕时长
@ -438,7 +442,9 @@ declare global {
9、改变地块状态,参数:无
10、恢复血量,参数:回血比例(最大满血)
11、攻击距离增加,参数:距离加值,持续时间(-1为本场永久生效)
12、损毁基础字,参数:概率 */
12、损毁基础字,参数:概率
13、冰冻,参数:概率,冰冻时间
14、点燃,参数:伤害间隔,伤害系数,持续时间 */
readonly atk_buff1: string;
/** 命中后效果:buff类型,参数
1、眩晕,参数:概率,眩晕时长
@ -487,7 +493,9 @@ declare global {
类型9:一道直线火龙伤害,参数:飞行速度,伤害系数,持续时间
类型10:修改普通攻击方式,参数:攻击类型atk_type字段,对应参数atk_para字段
类型11:释放直线剑气,参数:剑气数量,间隔,伤害系数
类型12:从目标身上开始产生闪电链,蔓延多个目标(不会重复蔓延),参数:蔓延范围,蔓延数量,伤害系数 */
类型12:从目标身上开始产生闪电链,蔓延多个目标(不会重复蔓延),参数:蔓延范围,蔓延数量,伤害系数
13:从目标身上分裂冰锥,参数:索敌范围(半径),分裂数量,伤害系数
14:产生爆炸:参数:爆炸圆形半径,伤害系数 */
readonly result: string;
}
/**

@ -190,9 +190,9 @@ export class UIBattleGame extends Auto_BattleGame {
/** AI 技能条降频 */
private _aiSkillUiFrame = 0;
/** 砚台世界坐标,供背包出字抛物线起点 */
/** 刷新按钮世界坐标,供背包出字抛物线起点 */
private readonly _inkstoneWorldPos = (): Readonly<Vec3> | null => {
const n = this.btnIllustrated;
const n = this.btnRefresh;
return n?.isValid ? n.worldPosition : null;
};
@ -1207,7 +1207,7 @@ export class UIBattleGame extends Auto_BattleGame {
gg.ui.popPanel(UIID.BattlePause);
}
/** 召唤:扣桃刷新背包 5 槽 */
/** 召唤:按当前空开启格补字,最多 5 个 */
click_btnRefresh(): void {
const battle = this._battle;
const bag = battle?.bagSession;
@ -1220,27 +1220,28 @@ export class UIBattleGame extends Auto_BattleGame {
this._bagView.clearBagEntranceBusy();
}
// 与老项目一致:字从砚台(btnIllustrated)抛物线落入槽位,非召唤按钮
// 字从刷新按钮抛物线落入本次新补的空格;已有字的格子不重刷
const ink = this._bagView?.getInkstoneWorldPosition();
const fromWorld = ink
? new Vec3(ink.x, ink.y, ink.z)
: (this.btnIllustrated?.isValid ? this.btnIllustrated.worldPosition.clone() : null);
this._bagView?.armBagEntrance(fromWorld);
: (this.btnRefresh?.isValid ? this.btnRefresh.worldPosition.clone() : null);
const result = bag.tryNormalRefresh(battle.LevelConfigId, battle);
const result = bag.tryFillEmptyOpenSlots(battle);
if (result === 'ok') {
this._refreshPeachUi();
this._firstGuide.onSummonOk();
this._bagView?.playNewSlotEntrance(bag.lastFilledSlotIndices(), fromWorld);
return;
}
this._bagView?.disarmBagEntrance();
this._bagView?.clearBagEntranceBusy();
if (result === 'not_enough') {
if (result === 'no_slot') {
gg.ui.showToast('暂无空格');
} else if (result === 'not_enough') {
gg.ui.showToast('桃子不足');
} else {
gg.ui.showToast('刷新失败');
console.log('[UIBattleGame] tryNormalRefresh failed');
console.log('[UIBattleGame] tryFillEmptyOpenSlots failed', result);
}
this._refreshPeachUi();
}

@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "4eceeca5-303a-4dc2-a7e5-b29caa2c391e",
"files": [],
"subMetas": {},
"userData": {}
}

@ -0,0 +1,33 @@
//*********************
// create by 流云
// 插件自动生成的节点脚本
// 请勿修改(自动生成会覆盖)
// 请通过子类继承使用
// 如何生成可直接调用的UI节点和组件?
// 1.在场景节点树中选中要生成的UI根节点
// 2.点击菜单"tool"->“生成UI脚本” 或者使用快捷键(快捷键:Ctrl+Shift+C)
// 3.点击后会在script/ui/目录中生成一个以选中节点命名的文件夹,文件夹中会生成两个脚本文件,“Auto_”开头的就是本文件,不可修改,另一个是“UI”+选中节点名称命名的脚本文件,可修改,是需要挂载到选中节点的脚本文件,给用户使用的写逻辑的脚本,只会生成一次
// 4.找到第3步描述的“UI”+选中节点名称命名的脚本文件,将其挂载到选中节点上(第一生成需要挂载,后续修改场景,重复生成的时候,该脚本不会再生成,不用担心会覆盖逻辑脚本)
// 下面是类型示例
// 1.按钮,节点名以“btn_”或者“Btn_”开头或者节点添加了Button组件, 例:btn_Close,会生成btn_Close节点和click_btn_Close()方法
// 2.标签,节点名以“lb_”开头, 例:lb_title,会生成lb_title的Label组件
// 3.输入框,节点名以“eb_”开头, 例:eb_input,会生成eb_input的EditBox组件
// 4.滚动视图,节点名以“sv_”开头, 例:sv_list,会生成sv_list的ScrollView组件
// 5.图片,节点名以“img_”或者sp_“”开头, 例:img_Icon,会生成img_Icon的Sprite组件
// 6.普通节点,节点名以“ui_”或者“Ui_”或者“UI_”开头, 例:ui_Content,会生成ui_Content的Node节点
//*********************
import { _decorator, Component, Node, find, Sprite, Label, Button, ScrollView, EditBox, RichText } from 'cc';
import { UIBase } from '../../../mx/module/ui/UIBase';
const { ccclass } = _decorator;
@ccclass('Auto_grid17')
export class Auto_grid17 extends UIBase {
onLoad(){
super.onLoad();
}
onEnable() {
}
onDisable() {
}
}

@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "12ede90d-097b-4e13-bd2d-103c9cf3f454",
"files": [],
"subMetas": {},
"userData": {}
}

@ -0,0 +1,30 @@
//*********************
// create by 流云
// time: Fri Oct 02 2026
// desc:
//*********************
import { _decorator, Component, Node , find} from 'cc';
import { Auto_grid17 } from './Auto_grid17';
const { ccclass, property } = _decorator;
@ccclass('UIgrid17')
export class UIgrid17 extends Auto_grid17 {
onLoad(): void {
super.onLoad();
}
onEnable(): void {
super.onEnable();
}
onDisable(): void {
super.onDisable();
}
onInit(): void {
}
}

@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "fde65b23-ed5c-42b5-8c75-2439604acb46",
"files": [],
"subMetas": {},
"userData": {}
}
Loading…
Cancel
Save