chenanghui 2 weeks ago
parent 8ca000ffef
commit 4e31e5d635
  1. 239
      assets/bundles/battleHero/prefab/skill/棍.prefab
  2. 13
      assets/bundles/battleHero/prefab/skill/棍.prefab.meta
  3. 1418
      assets/bundles/first/config/Hero.json
  4. 880
      assets/bundles/first/config/Skill.json
  5. 37
      assets/script/battle/combat/BowAtkRuntime.ts
  6. 57
      assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts
  7. 149
      assets/script/battle/heros/Gong.ts
  8. 249
      assets/script/battle/heros/Gun.ts
  9. 7
      assets/script/battle/presentation/AtkTypePresenters.ts
  10. 3
      assets/script/battle/presentation/SkillPresenterRouter.ts
  11. 9
      assets/script/battle/presentation/SkillVisualChannel.ts
  12. 53
      assets/script/battle/presentation/SpecialSkillPresenters.ts
  13. 40
      assets/script/battle/skill/Skill.ts
  14. 2
      assets/script/game/TableAnomaly.ts
  15. 10
      assets/script/interface/AutoTable.ts
  16. 22
      docs/方案-atk_type12与13.md
  17. 5
      docs/方案-战斗技能逻辑层.md
  18. 15
      docs/方案-战斗表现数据驱动.md
  19. 2
      package.json

@ -0,0 +1,239 @@
[
{
"__type__": "cc.Prefab",
"_name": "棍",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "棍",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 8
}
],
"_prefab": {
"__id__": 10
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 9.856,
"y": 3.066,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.5,
"y": 0.5,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "剑气",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
}
],
"_prefab": {
"__id__": 7
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -20.568,
"y": 10.989,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 1,
"w": 6.123233995736766e-17
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 180
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 173.42999267578125,
"height": 148.72000122070312
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5006054509679297,
"y": 0.4103012369345594
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "faftZacVdAPpb77M0x0iJN"
},
{
"__type__": "sp.Skeleton",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_skeletonData": {
"__uuid__": "55439550-a81d-4ae0-8b2b-55a541ba162b",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "default",
"defaultAnimation": "剑气红",
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 0,
"_cacheMode": 0,
"_sockets": [],
"_useTint": false,
"_debugMesh": false,
"_debugBones": false,
"_debugSlots": false,
"_enableBatch": false,
"loop": true,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "06gh4gzJ9GA710GvBrStt7"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "b33ITW0vlFLanQ1WRCfGIb",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 9
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "9fu4lzkKZH2JAjsMyZFCqu"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "c6bl+833JG5oWC08q0+HyC",
"instance": null,
"targetOverrides": null
}
]

@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "e23a1a9e-43ed-4ba1-8803-6bb72deab1c1",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "棍"
}
}

File diff suppressed because it is too large Load Diff

@ -3716,5 +3716,885 @@
"skill": "",
"condition": "",
"result": ""
},
{
"id": 100001,
"name": "",
"desc": "剑劈砍",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "1",
"fly": "",
"atk_para": "10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 100002,
"name": "",
"desc": "扇形攻击",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "16",
"fly": "",
"atk_para": "1,0,60,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 100003,
"name": "",
"desc": "扇形二连击",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "16",
"fly": "",
"atk_para": "2,0.3,60,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 100004,
"name": "",
"desc": "连击带击退",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "16",
"fly": "",
"atk_para": "2,0.3,60,10000",
"atk_buff1": "2,5000,50",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 100005,
"name": "",
"desc": "扇形3连击",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "16",
"fly": "",
"atk_para": "3,0.2,60,10000",
"atk_buff1": "2,5000,50",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 200001,
"name": "",
"desc": "抡一圈",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "3",
"fly": "",
"atk_para": "10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 200002,
"name": "",
"desc": "抡2圈",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 260,
"atk_type": "14",
"fly": "",
"atk_para": "2,0.3,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 200003,
"name": "",
"desc": "抡2圈,概率眩晕",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 260,
"atk_type": "14",
"fly": "",
"atk_para": "2,0.3,10000",
"atk_buff1": "1,5000,3",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 200004,
"name": "",
"desc": "携带青龙影子,扩大攻击范围",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 300,
"atk_type": "14",
"fly": "",
"atk_para": "2,0.3,10000",
"atk_buff1": "1,5000,3",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 200005,
"name": "",
"desc": "额外飞出龙影攻击",
"icon": "",
"type": 1,
"level": 0,
"cd": 5,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 300,
"atk_type": "14",
"fly": "",
"atk_para": "2,0.3,10000",
"atk_buff1": "1,5000,3",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 300001,
"name": "",
"desc": "水球术",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "13",
"fly": "",
"atk_para": "1,0,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 300002,
"name": "",
"desc": "连发",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "13",
"fly": "",
"atk_para": "2,0.3,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 300003,
"name": "",
"desc": "三连发",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "13",
"fly": "",
"atk_para": "3,0.2,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 300004,
"name": "",
"desc": "4连发",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "13",
"fly": "",
"atk_para": "4,0.16,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 300005,
"name": "",
"desc": "5连发",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "13",
"fly": "",
"atk_para": "5,0.13,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 400001,
"name": "",
"desc": "单体抛物线",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "4",
"fly": "",
"atk_para": "10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 400002,
"name": "",
"desc": "2散射",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "17",
"fly": "",
"atk_para": "2,12,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 400003,
"name": "",
"desc": "3散射",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "17",
"fly": "",
"atk_para": "3,12,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 400004,
"name": "",
"desc": "穿透效果",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "12",
"fly": "",
"atk_para": "3,12,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 400005,
"name": "",
"desc": "5散射",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "12",
"fly": "",
"atk_para": "5,12,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 500001,
"name": "",
"desc": "直线戳攻击",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "5",
"fly": "",
"atk_para": "40,0,1,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 500002,
"name": "",
"desc": "2段连击",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "5",
"fly": "",
"atk_para": "40,0.3,2,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 500003,
"name": "",
"desc": "三连击",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "5",
"fly": "",
"atk_para": "40,0.21,3,10000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 500004,
"name": "",
"desc": "三连击加眩晕效果",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "5",
"fly": "",
"atk_para": "40,0.21,3,10000",
"atk_buff1": "1,5000,3",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 500005,
"name": "",
"desc": "三连击加眩晕效果后添加一段横扫,并击退",
"icon": "",
"type": 1,
"level": 0,
"cd": 0.8,
"priority": 0,
"animation": "攻击",
"showeffect": "",
"target_stage": 1,
"range": 0,
"atk_type": "5",
"fly": "",
"atk_para": "40,0.21,3,10000",
"atk_buff1": "1,5000,3",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600001,
"name": "",
"desc": "每攻击10次触发直线剑气",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600002,
"name": "",
"desc": "造成伤害概率剑阵",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600003,
"name": "",
"desc": "自动发射飞剑攻击",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600004,
"name": "",
"desc": "悟空专属技能效果",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600005,
"name": "",
"desc": "原地旋转大风车",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600006,
"name": "",
"desc": "丢出旋转的棍子攻击",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600007,
"name": "",
"desc": "暴风雪,范围持续伤害",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600008,
"name": "",
"desc": "分裂攻击",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600009,
"name": "",
"desc": "锁定多个目标,发射冰锥,概率冰冻",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600010,
"name": "",
"desc": "点燃目标",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600011,
"name": "",
"desc": "直线火龙",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600012,
"name": "",
"desc": "漫天箭雨",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600013,
"name": "",
"desc": "飞枪冲锋",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600014,
"name": "",
"desc": "击杀爆炸",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
},
{
"id": 600015,
"name": "",
"desc": "攻击距离+1,伤害增加",
"icon": "",
"type": 0,
"level": 0,
"cd": 0,
"priority": 0,
"animation": "",
"showeffect": "",
"target_stage": 0,
"range": 0,
"atk_type": "",
"fly": "",
"atk_para": "",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
"condition": "",
"result": ""
}
]

@ -46,6 +46,10 @@ export interface ScatterRay {
buff2: string;
/** 已命中(每箭每敌一次) */
hitIds: EntityId[];
/** 是否穿透:12 穿透;17 不穿透(首中即消耗) */
pierce: boolean;
/** 非穿透命中消耗回调(回收表现箭);逻辑命中时刻触发 */
onConsume?: () => void;
}
/** 连发箭:落地伤锁定目标 */
@ -138,13 +142,14 @@ export class BowAtkHost {
}
/**
* 生成一支散射穿透箭。
* 生成一支散射箭(12 穿透 / 17 不穿透)。
* @param ray 弹道
*/
spawnRay(ray: ScatterRay): void {
this._rays.push(ray);
const session = this._session;
if (session) this._hitRay(session, ray, 0);
if (!session) return;
if (this._hitRay(session, ray, 0)) this._removeRay(ray);
}
/**
@ -168,8 +173,8 @@ export class BowAtkHost {
d.x += d.ux * step;
d.y += d.uy * step;
d.left -= dt;
this._hitRay(session, d, step * 0.5);
if (d.left <= 0) this._rays.splice(i, 1);
const consumed = this._hitRay(session, d, step * 0.5);
if (consumed || d.left <= 0) this._rays.splice(i, 1);
}
for (let i = this._volleys.length - 1; i >= 0; i--) {
const d = this._volleys[i];
@ -204,17 +209,19 @@ export class BowAtkHost {
/**
* 散射体盒内敌方各伤一次(Cocos 空间,与画面箭一致)。
* 非穿透(atk_type=17)首中即结算并消耗,触发 onConsume 回收表现箭。
* @param session 战斗会话
* @param d 在飞箭
* @param extraLen 本帧步进半长,防穿透漏检
* @returns 箭是否被消耗(非穿透命中)
*/
private _hitRay(session: BattleSession, d: ScatterRay, extraLen: number): void {
if (!session.world.isAlive(d.caster)) return;
private _hitRay(session: BattleSession, d: ScatterRay, extraLen: number): boolean {
if (!session.world.isAlive(d.caster)) return false;
const casterTeam = session.world.getComponent(d.caster, TeamComp);
const logical = SkillPresentationEnv.toLogical(d.caster, d.x, d.y);
if (!logical) {
console.log('[BowAtkHost] 散射箭无法映射逻辑格', d.caster);
return;
return false;
}
const cellSize = Math.max(1, session.grid.cellSize);
const halfBody = cellSize * 0.5;
@ -237,7 +244,23 @@ export class BowAtkHost {
if (Math.abs(ly) > d.halfWid + halfBody) continue;
d.hitIds.push(id);
this._deal(session, d.caster, id, d.abilityId, d.scale, d.buff1, d.buff2);
if (!d.pierce) {
const onConsume = d.onConsume;
d.onConsume = undefined;
onConsume?.();
return true;
}
}
return false;
}
/**
* 移除已消耗的在飞箭。
* @param ray 弹道
*/
private _removeRay(ray: ScatterRay): void {
const idx = this._rays.indexOf(ray);
if (idx >= 0) this._rays.splice(idx, 1);
}
/**

@ -37,9 +37,9 @@ export interface SkillPresentationParams {
stageStrategy: number;
stageRangePx: number;
pickCount: number;
/** atk_type=5 / 10 / 14 总段数(10=连砸次数) */
/** atk_type=5 / 10 / 14 / 16 总段数(10=连砸次数) */
hitCount?: number;
/** atk_type=5 / 10 / 14 段间间隔(秒) */
/** atk_type=5 / 10 / 14 / 16 段间间隔(秒) */
hitIntervalSec?: number;
}
@ -299,9 +299,21 @@ export class SkillAbilityBuilder {
hitIntervalSec: Math.max(0, Number(atkParts[1]) || 0),
} : {}),
...(atkHead === 10 || atkHead === 14 ? this._countIntervalHitParams(row, atkParts, atkHead) : {}),
...(atkHead === 16 ? this._fanMultiHitParams(row, atkParts) : {}),
};
}
/**
* atk_type=16:段数=第1段,间隔=第2段(角度/系数供索敌与伤害用,不进表现 params)。
* @param row Skill 行
* @param atkParts atk_para 分段
*/
private static _fanMultiHitParams(row: ITableSkill, atkParts: number[]): Pick<SkillPresentationParams, 'hitCount' | 'hitIntervalSec'> {
const parsed = this._parseFanMultiHit(row, atkParts);
if (!parsed) return {};
return { hitCount: parsed.hitCount, hitIntervalSec: parsed.intervalSec };
}
/**
* atk_type=10 / 14:数量,间隔秒,伤害万分比。
* @param row Skill 行
@ -331,6 +343,24 @@ export class SkillAbilityBuilder {
return { hitCount, intervalSec, wan };
}
/**
* atk_type=16 扇形多段:atk_para=伤害段数,伤害间隔秒,角度,伤害万分比。
* 非法则 warn 并返回 null。
* @param row Skill 行
* @param atkParts atk_para 分段
*/
private static _parseFanMultiHit(row: ITableSkill, atkParts: number[]): { hitCount: number; intervalSec: number; angleDeg: number; wan: number } | null {
const hitCount = Math.floor(atkParts[0] || 0);
const intervalSec = Number(atkParts[1]);
const angleDeg = Number(atkParts[2]);
const wan = Number(atkParts[3]);
if (atkParts.length < 4 || !(hitCount > 0) || !Number.isFinite(intervalSec) || intervalSec < 0 || !(angleDeg > 0) || !Number.isFinite(wan)) {
console.log('[SkillAbilityBuilder] atk_type=16 参数须为「伤害段数,伤害间隔秒,角度,伤害万分比」', row.id, row.atk_para);
return null;
}
return { hitCount, intervalSec, angleDeg, wan };
}
/**
* 怪物 atk_buff1/2(类型 6/9/12、target_stage=2 由 Host 处理全部目标,不走此阶段)。
* @param row Skill 行
@ -440,9 +470,12 @@ export class SkillAbilityBuilder {
}
// 3 360°:atk_para=[伤害系数];6 扇形:atk_para=[角度,伤害系数]
// 14 360°多段:atk_para=[段数,间隔秒,伤害万分比];圈半径仍用 Skill.range
// 16 扇形多段:atk_para=[段数,间隔秒,角度,伤害万分比];角度取第3段
// 索敌/命中圈用 Skill.range(≤0 则塔射程),勿把伤害万分比当半径
if (atkHead === 3 || atkHead === 6 || atkHead === 14) {
const angle = atkHead === 6 ? Math.max(1, atkParts[0] || 90) : 360;
if (atkHead === 3 || atkHead === 6 || atkHead === 14 || atkHead === 16) {
let angle = 360;
if (atkHead === 6) angle = Math.max(1, atkParts[0] || 90);
else if (atkHead === 16) angle = Math.max(1, atkParts[2] || 60);
const radius = rangePx > 0 ? rangePx : undefined;
return this._withAttackRangeFallback(rangePx, {
team,
@ -475,8 +508,8 @@ export class SkillAbilityBuilder {
},
});
}
// 4 抛物线 / 12 散射瞄准 / 13 连发:开战只锁主目标
if (atkHead === 4 || atkHead === 12 || atkHead === 13) {
// 4 抛物线 / 12 散射瞄准 / 13 连发 / 17 不穿透散射:开战只锁主目标
if (atkHead === 4 || atkHead === 12 || atkHead === 13 || atkHead === 17) {
return this._withAttackRangeFallback(rangePx, {
team,
selector: 'Nearest',
@ -573,7 +606,8 @@ export class SkillAbilityBuilder {
}]];
}
if (atkHead === 12 || atkHead === 13) {
// 12 散射 / 13 连发 / 17 不穿透散射:伤害由 BowAtkHost 弹道结算,无 deferred 段
if (atkHead === 12 || atkHead === 13 || atkHead === 17) {
return [];
}
@ -581,13 +615,18 @@ export class SkillAbilityBuilder {
return [];
}
if (atkHead === 5 || atkHead === 10 || atkHead === 14) {
if (atkHead === 5 || atkHead === 10 || atkHead === 14 || atkHead === 16) {
let hitCount = Math.max(1, Math.floor(atkParts[2] || 1));
if (atkHead === 10 || atkHead === 14) {
const parsed = this._parseCountIntervalWan(row, atkParts, atkHead);
if (!parsed) return [];
hitCount = parsed.hitCount;
}
if (atkHead === 16) {
const parsed = this._parseFanMultiHit(row, atkParts);
if (!parsed) return [];
hitCount = parsed.hitCount;
}
const scale = this._resolveDamageScale(atkHead, atkParts, extraParts);
const stages: EffectSpec[][] = [];
for (let i = 0; i < hitCount; i++) {
@ -677,6 +716,8 @@ export class SkillAbilityBuilder {
return this._damageScale(atkParts[1] ?? 10000);
case 6:
return this._damageScale(atkParts[1] ?? 10000);
case 16:
return this._damageScale(atkParts[3] ?? 10000);
case 7:
return this._damageScale(atkParts[1] ?? 10000);
case 10:

@ -11,6 +11,8 @@ import {
import { BattleAudio } from '../audio/BattleAudio';
import { HeroSkin } from '../view/HeroSkin';
import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback';
import { AttrId } from 'db://assets/mx/module/battle/ecs/Components';
import { BOW_ATK_ARROW_SPEED, SCATTER_RANGE_MUL, getBowAtkHost } from '../combat/BowAtkRuntime';
import { HeroActor } from './HeroActor';
const { ccclass } = _decorator;
@ -41,6 +43,8 @@ const RELEASE_AT_BASE = 0.3667;
const BOW_ATTACK_MAX_SEC = 0.19;
/** 攻击2 定格后无连击则回到文字待机 */
const COMBO_WINDOW_SEC = 1;
/** 散射 17 首中被消耗的表现箭保底可视飞行时长(秒):近距命中箭不凭空消失 */
const SCATTER_CONSUME_MIN_FLY_SEC = 0.08;
type BowPhase = 'idle' | 'aiming' | 'engaged' | 'returning';
@ -257,16 +261,32 @@ export class Gong extends HeroActor {
this._heroSkin?.playIdleAnim();
}
/** skillId Presenter 入口 */
/** skillId Presenter 入口;普攻形态被覆盖为 12/17 时走散射 */
presentNormAttack(ctx: SkillPresentationContext): void {
if (!this.isValid || !this.node?.isValid) {
ctx.finish();
return;
}
const head = Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0);
if (head === 12 || head === 17) {
this.presentScatterAttack(ctx);
return;
}
this._bindNodes();
this._playAttackBow(ctx);
}
/** atk_type 12/17 散射(BowScatterAtkPresenter 委托):用自身「英雄弓-攻击」投掷物扇出 */
presentScatterAttack(ctx: SkillPresentationContext): void {
if (!this.isValid || !this.node?.isValid) {
ctx.finish();
return;
}
const head = Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0);
this._bindNodes();
this._playAttackBow(ctx, head === 12 ? 12 : 17);
}
abortPresentation(): void {
this.abortPresentationCast();
this._abortPendingPresentation();
@ -280,8 +300,10 @@ export class Gong extends HeroActor {
* 弓普攻:只打 ctx.targets[0](路径最前),箭飞到该目标落地才结算。
* 藏字形「弓」,显「英雄弓-攻击」插值瞄准后播攻击2;拉满发射投掷物。
* 1 秒内连击再播攻击2;窗内无下一箭则插值回休息角切文字待机。
* @param ctx 表现上下文
* @param shape 0 单箭;12 穿透散射;17 不穿透散射
*/
private _playAttackBow(ctx: SkillPresentationContext) {
private _playAttackBow(ctx: SkillPresentationContext, shape = 0) {
if (!ctx.isAlive) {
ctx.finish();
return;
@ -380,6 +402,13 @@ export class Gong extends HeroActor {
this._setProjectileTemplateVisible(false);
unlockCast();
if (shape === 12 || shape === 17) {
// 散射(12 穿透 / 17 不穿透):逻辑弹道+表现箭各自回收,不阻塞下一击
this._launchScatterVolley(ctx, destId, shape);
finishVisual();
return;
}
pendingFlights = 1;
const onOneFlightDone = () => {
pendingFlights--;
@ -707,6 +736,122 @@ export class Gong extends HeroActor {
);
}
/**
* 散射出手(12 穿透 / 17 不穿透):以自身「投掷物」为模板扇出直线箭,
* 飞距=出手时射程×倍率;逻辑伤害由 BowAtkHost 体盒结算。
* 17 首中即消耗并当场回收表现箭;没碰怪飞满全程消失。
* @param ctx 表现上下文
* @param destId 瞄准目标(中线对准)
* @param shape 12 / 17
*/
private _launchScatterVolley(ctx: SkillPresentationContext, destId: EntityId, shape: number): void {
const parts = String(ctx.params.atkPara ?? '').split(',').map(Number);
const count = Math.floor(parts[0] || 0);
const stepDeg = parts[1];
const wan = parts[2];
const rangePx = SkillPresentationEnv.getCasterAttr(ctx.caster, AttrId.Range);
const template = this._projectileTemplate;
const ut = template?.isValid ? template.getComponent(UITransform) : null;
if (!(count > 0) || !(stepDeg > 0) || !(wan > 0) || !(rangePx > 0) || !template?.isValid || !ut) {
console.log('[Gong] 散射参数或投掷物模板无效', ctx.params.atkPara, rangePx);
return;
}
const targetWorld = SkillPresentationEnv.getTargetWorldPos(destId);
const fromWorld = template.worldPosition.clone();
const aimDeg = targetWorld
? (computeAimAngleDeg(fromWorld, targetWorld) ?? 0)
: (this._skillParent?.angle ?? 0);
const degs: number[] = [];
getBowAtkHost().fillScatterDegs(aimDeg, count, stepDeg, degs);
const ws = template.worldScale;
const halfLen = ut.width * Math.abs(ws.x) * 0.5;
const halfWid = ut.height * Math.abs(ws.y) * 0.5;
const dist = rangePx * SCATTER_RANGE_MUL;
const duration = dist / PROJECTILE_FLY_SPEED;
const flyLayer = SkillPresentationEnv.flyLayer;
const spawnAtMs = Date.now();
for (let i = 0; i < degs.length; i++) {
const deg = degs[i];
const clone = this._cloneProjectile();
if (!clone) break;
// 重挂 flyLayer 会丢世界坐标:先记弓上位置,重挂后按世界坐标摆回
const from = clone.worldPosition.clone();
if (flyLayer?.isValid) {
const wsClone = clone.worldScale.clone();
const flyUt = flyLayer.getComponent(UITransform);
flyLayer.active = true;
clone.parent = flyLayer;
clone.setWorldScale(wsClone);
if (flyUt) clone.setPosition(flyUt.convertToNodeSpaceAR(from));
else clone.setWorldPosition(from);
}
clone.angle = deg;
const rad = deg * Math.PI / 180;
const ux = Math.cos(rad);
const uy = Math.sin(rad);
const toWorld = new Vec3(from.x + ux * dist, from.y + uy * dist, from.z);
this._flyScatterClone(clone, flyLayer, from, toWorld, duration);
getBowAtkHost().spawnRay({
caster: ctx.caster,
abilityId: ctx.abilityId,
x: from.x,
y: from.y,
ux,
uy,
speed: BOW_ATK_ARROW_SPEED,
left: duration,
scale: wan / 10000,
halfLen,
halfWid,
buff1: String(ctx.params.buff1 ?? ''),
buff2: String(ctx.params.buff2 ?? ''),
hitIds: [],
pierce: shape !== 17,
onConsume: shape === 17 ? this._consumeScatterClone.bind(this, clone, flyLayer, spawnAtMs) : undefined,
});
}
}
/** 散射表现箭直线飞行(体盒伤害在逻辑层,表现只跟位置) */
private _flyScatterClone(clone: Node, flyLayer: Node | null, fromWorld: Readonly<Vec3>, toWorld: Readonly<Vec3>, duration: number): void {
const flyUt = flyLayer?.getComponent(UITransform) ?? null;
const fromLocal = flyUt ? flyUt.convertToNodeSpaceAR(fromWorld) : fromWorld.clone();
const toLocal = flyUt ? flyUt.convertToNodeSpaceAR(toWorld) : toWorld.clone();
SkillTransformDriver.animate({
node: clone,
duration,
fromPos: fromLocal,
toPos: toLocal,
easing: SkillTransformDriver.easeLinear,
onComplete: () => this._recycleScatterClone(clone, flyLayer),
});
}
/**
* 17 首中被消耗:出手瞬间命中(近距首中)时箭还没飞出来就回收会像没发射,
* 保底再飞 SCATTER_CONSUME_MIN_FLY_SEC 再回收;已飞够时长则当场回收。
*/
private _consumeScatterClone(clone: Node, flyLayer: Node | null, spawnAtMs: number): void {
const elapsedSec = (Date.now() - spawnAtMs) / 1000;
const waitSec = SCATTER_CONSUME_MIN_FLY_SEC - elapsedSec;
if (waitSec <= 0) {
this._recycleScatterClone(clone, flyLayer);
return;
}
this.schedulePresentation(() => this._recycleScatterClone(clone, flyLayer), waitSec);
}
/** 回收散射表现箭(飞到终点或 17 首中被消耗) */
private _recycleScatterClone(clone: Node, flyLayer: Node | null): void {
if (clone?.isValid) {
SkillTransformDriver.cancelNode(clone);
clone.destroy();
}
const idx = this._flyingClones.indexOf(clone);
if (idx >= 0) this._flyingClones.splice(idx, 1);
this._maybeHideFlyLayer(flyLayer);
}
private _onFlightComplete(ctx: SkillPresentationContext, destId: EntityId, projectileNode: Node, flyLayer: Node, onComplete: () => void): void {
this._settleBowLand(ctx, destId);
if (projectileNode.isValid) {

@ -1,7 +1,10 @@
import { _decorator, Node, sp } from 'cc';
import { _decorator, instantiate, Node, sp, UITransform, Vec3 } from 'cc';
import { GBundle } from '../../game/ConfigRes';
import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
import { PRES_HIT_AT_BASE } from '../presentation/SkillVisualChannel';
import { SkillTransformDriver } from '../skill/SkillTransformDriver';
import { getBowAtkHost } from '../combat/BowAtkRuntime';
import { HeroSkin } from '../view/HeroSkin';
import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback';
import { HeroActor } from './HeroActor';
@ -11,10 +14,32 @@ const { ccclass } = _decorator;
/** battleHero/prefab/skill/英雄棍-攻击 */
const ATK_PREFAB = '英雄棍-攻击';
const ATK_PREFAB_PATH = 'prefab/skill/';
/** 龙影攻击预制体(棍 200005 环形刀光,保存名「棍」) */
const RING_PREFAB = '棍';
/** 攻效节点名 */
const NODE_ANIM = 'anim';
/** 攻效 Spine 动画 */
const SPINE_ATK_FX = '攻击2';
/** 龙影链式发射间距(像素):上一支飞出该距离后才发下一支 */
const RING_CHAIN_GAP_PX = 100;
/** 龙影起始发射延迟(秒):攻效开播后延迟发起,配合棍的攻击动作 */
const RING_START_DELAY_SEC = 0.5;
/** 龙影起始角(度):从水平向右起逆时针;atk_para 不含角度参数,取代码约定值 */
const RING_START_DEG = 30;
/** 龙影邻支角度间隔(度):数量 = 360/间隔 均分整圆 */
const RING_STEP_DEG = 20;
/** 龙影飞行像素/秒 */
const RING_FLY_SPEED = 500;
/** 龙影飞行距离(像素):足够飞出屏幕 */
const RING_FLY_DIST = 1200;
/** 龙影出生外扩(像素):从字的攻击边缘发出 */
const RING_SPAWN_OFFSET = 200;
/** 龙影数量上限(角度间隔最小 18°) */
const RING_MAX_COUNT = 18;
/** 龙影 Spine 默认动画(预制体未勾 playOnLoad,需显式播放) */
const RING_ANIM = '剑气红';
/** 龙影首中被消耗的保底可视飞行时长(秒):贴脸命中不凭空消失 */
const RING_CONSUME_MIN_FLY_SEC = 0.08;
/**
* 棍:HeroActor 壳;普攻/技能表现共用「英雄棍-攻击」(播攻击2),字形播「攻击」。
@ -26,9 +51,22 @@ export class Gun extends HeroActor {
private _atkAnim: sp.Skeleton | null = null;
/** 攻效「攻击2」播放中;命中结算会 onIdle,勿此时藏攻效 */
private _fxPlaying = false;
/** 当前攻效对应的播控序号;>0 且 multi=true 表示多段收招判定 */
private _atkFxSeq = 0;
/** 本次攻效是否多段(atk_type=16) */
private _atkFxMultiHit = false;
/** 本次攻效是否环形刀光(200005):发射与攻效同播,收招等攻效播完 */
private _atkFxRing = false;
/** 200005 环形刀光:待发射的展示上下文(攻效播完置入) */
private _ringCtx: SkillPresentationContext | null = null;
/** 龙影模板(仅用于 instantiate),来自 battleHero/prefab/skill/棍 */
private _ringTemplate: Node | null = null;
/** 在飞龙影克隆 */
private readonly _ringClones: Node[] = [];
detachPooledFx(): void {
this._disposeAtk();
this._disposeRing();
super.detachPooledFx();
}
@ -40,6 +78,8 @@ export class Gun extends HeroActor {
abortPresentation(): void {
this._fxPlaying = false;
this._atkFxRing = false;
this._ringCtx = null;
this._hideAtk();
super.abortPresentation();
}
@ -49,12 +89,22 @@ export class Gun extends HeroActor {
this._presentGunFx(ctx);
}
/** atk_type=16 扇形多段:同一套攻效,命中按段间隔逐段结算 */
presentFanMultiHit(ctx: SkillPresentationContext): void {
this._presentGunFx(ctx, true);
}
/** 200005 环形刀光:播完攻效后从字的位置环形发出龙影 */
presentRingAttack(ctx: SkillPresentationContext): void {
this._presentGunFx(ctx, false, true);
}
/** 主动技能与普攻共用预制体(表暂无 act_skill,预留入口) */
presentSkill(ctx: SkillPresentationContext): void {
this._presentGunFx(ctx);
}
private _presentGunFx(ctx: SkillPresentationContext): void {
private _presentGunFx(ctx: SkillPresentationContext, multiHit = false, ring = false): void {
if (!ctx.isAlive || !this.isValid || !this.node?.isValid) {
ctx.finish();
return;
@ -64,6 +114,14 @@ export class Gun extends HeroActor {
this.syncAtkFxSkin(this._atkAnim);
this._showAtk();
const seq = this.beginPresentationCast(ctx);
this._atkFxSeq = seq;
this._atkFxMultiHit = multiHit;
this._atkFxRing = ring;
this._ringCtx = ring ? ctx : null;
if (ring) {
// 字形「攻击」动画先于攻效播完:挂起收招,等攻效播完再收招
this.holdPresentationUntilFxComplete();
}
const aspd = this.visualChannel.playCastAnim({
caster: ctx.caster,
animation: String(ctx.params.animation ?? ''),
@ -74,19 +132,202 @@ export class Gun extends HeroActor {
if (this._atkAnim?.isValid) {
this._fxPlaying = true;
this.playSpine(this._atkAnim, SPINE_ATK_FX, this._onAtkFxCompleteBound, aspd, false);
// 跟攻击动作一起播:攻效开播后延迟 RING_START_DELAY_SEC 发起
if (ring) this.schedulePresentation(() => this._startRing(), RING_START_DELAY_SEC);
} else {
this._fxPlaying = false;
console.log('[Gun] 攻效预制体未加载', ATK_PREFAB);
if (ring) {
this.markStaffSmashAnimsComplete(seq);
this._startRing();
}
}
this.schedulePresentationCast(seq, PRES_HIT_AT_BASE / Math.max(0.01, aspd));
if (ring) {
// 伤害全在龙影弹道结算:不安排常规收招 hit,攻效播完由 _startRing 发射并收招
return;
}
const hitDelay = PRES_HIT_AT_BASE / Math.max(0.01, aspd);
if (multiHit) this.schedulePresentationMultiHit(seq, ctx, hitDelay);
else this.schedulePresentationCast(seq, hitDelay);
}
private readonly _onAtkFxCompleteBound = (): void => {
this._fxPlaying = false;
this._hideAtk();
if (this.isValid) super.onIdle();
if (!this.isValid) return;
const seq = this._atkFxSeq;
if (this._atkFxRing && seq > 0 && this.isPresentationCast(seq)) {
// 环形刀光:龙影已随攻效发射,攻效播完即收招
this.markStaffSmashAnimsComplete(seq);
return;
}
if (this._atkFxMultiHit && seq > 0 && this.isPresentationCast(seq)) {
// 多段:攻效播完即解锁收招,不等字形攻击动画(可能已被待机打断)
this.markStaffSmashAnimsComplete(seq);
return;
}
super.onIdle();
};
/**
* 200005 环形刀光:起始角/数量用代码约定(RING_START_DEG/RING_STEP_DEG),
* 伤害系数取 atk_para 第 3 段(与 14 多段语义的第 3 段一致)。
*/
private _startRing(): void {
const ctx = this._ringCtx;
this._ringCtx = null;
if (!ctx?.isAlive || !this.isValid || !this.node?.isValid) return;
const parts = String(ctx.params.atkPara ?? '').split(',').map(Number);
const wan = parts[2];
if (!(wan > 0)) {
console.log('[Gun] 200005 atk_para 伤害系数非法', ctx.params.atkPara);
return;
}
let count = Math.round(360 / RING_STEP_DEG);
if (count > RING_MAX_COUNT) count = RING_MAX_COUNT;
if (count < 1) count = 1;
const step = 360 / count;
const origin = this.node.worldPosition.clone();
const template = this._ensureRingTemplate();
if (!template?.isValid) {
console.log('[Gun] 龙影预制体未加载', RING_PREFAB);
return;
}
for (let i = 0; i < count; i++) {
const deg = RING_START_DEG + i * step;
// 链式发射:上一支飞出 RING_CHAIN_GAP_PX 后才发下一支
const delaySec = (i * RING_CHAIN_GAP_PX) / RING_FLY_SPEED;
if (i === 0) this._spawnRingArrow(ctx, origin, deg, wan);
else this.schedulePresentation(() => this._spawnRingArrow(ctx, origin, deg, wan), delaySec);
}
}
/** 发一支龙影:直线飞出,首中即结算消耗(无怪飞满距离消失) */
private _spawnRingArrow(ctx: SkillPresentationContext, origin: Vec3, deg: number, wan: number): void {
if (!ctx.isAlive || !this.isValid || !this.node?.isValid) return;
const rad = deg * Math.PI / 180;
const ux = Math.cos(rad);
const uy = Math.sin(rad);
const from = new Vec3(origin.x + ux * RING_SPAWN_OFFSET, origin.y + uy * RING_SPAWN_OFFSET, origin.z);
const to = new Vec3(origin.x + ux * (RING_SPAWN_OFFSET + RING_FLY_DIST), origin.y + uy * (RING_SPAWN_OFFSET + RING_FLY_DIST), origin.z);
const ut = this._ringTemplate?.isValid ? this._ringTemplate.getComponent(UITransform) : null;
if (!ut) {
console.log('[Gun] 龙影预制体缺少 UITransform,跳过', RING_PREFAB);
return;
}
const ws = this._ringTemplate!.worldScale;
const duration = RING_FLY_DIST / RING_FLY_SPEED;
const flyLayer = SkillPresentationEnv.flyLayer;
let visual: Node | null = null;
if (flyLayer?.isValid) {
visual = this._cloneRingArrow();
if (!visual) console.log('[Gun] 龙影克隆失败,仅逻辑结算');
}
const spawnAtMs = Date.now();
if (visual) {
const wsClone = visual.worldScale.clone();
const flyUt = flyLayer.getComponent(UITransform);
flyLayer.active = true;
visual.parent = flyLayer;
visual.setWorldScale(wsClone);
visual.angle = deg;
visual.setPosition(flyUt ? flyUt.convertToNodeSpaceAR(from) : from.clone());
const toLocal = flyUt ? flyUt.convertToNodeSpaceAR(to) : to.clone();
SkillTransformDriver.animate({
node: visual,
duration,
fromPos: visual.position.clone(),
toPos: toLocal,
easing: SkillTransformDriver.easeLinear,
onComplete: () => this._recycleRingClone(visual as Node),
});
}
getBowAtkHost().spawnRay({
caster: ctx.caster,
abilityId: ctx.abilityId,
x: from.x,
y: from.y,
ux,
uy,
speed: RING_FLY_SPEED,
left: duration,
scale: wan / 10000,
halfLen: ut.width * Math.abs(ws.x) * 0.5,
halfWid: ut.height * Math.abs(ws.y) * 0.5,
buff1: String(ctx.params.buff1 ?? ''),
buff2: String(ctx.params.buff2 ?? ''),
hitIds: [],
pierce: false,
onConsume: visual ? () => this._consumeRingClone(visual as Node, spawnAtMs) : undefined,
});
}
/**
* 龙影首中被消耗:贴脸命中时还没飞出来就回收会像没发射,
* 保底再飞 RING_CONSUME_MIN_FLY_SEC 再回收;已飞够时长则当场回收。
*/
private _consumeRingClone(visual: Node, spawnAtMs: number): void {
const elapsedSec = (Date.now() - spawnAtMs) / 1000;
const waitSec = RING_CONSUME_MIN_FLY_SEC - elapsedSec;
if (waitSec <= 0) {
this._recycleRingClone(visual);
return;
}
this.schedulePresentation(() => this._recycleRingClone(visual), waitSec);
}
/** 取龙影模板(懒加载自 battleHero/prefab/skill/棍)并克隆一支;克隆需显式播 Spine(未勾 playOnLoad) */
private _cloneRingArrow(): Node | null {
const tpl = this._ensureRingTemplate();
if (!tpl?.isValid) return null;
const clone = instantiate(tpl);
clone.active = true;
const skel = clone.getComponentInChildren(sp.Skeleton);
if (skel?.isValid) {
skel.timeScale = 1;
skel.setAnimation(0, String(skel.defaultAnimation || RING_ANIM), true);
} else {
console.log('[Gun] 龙影缺少 Spine 动画节点', RING_PREFAB);
}
this._ringClones.push(clone);
return clone;
}
private _ensureRingTemplate(): Node | null {
if (!this._ringTemplate?.isValid) {
const tpl = gg.res.getNode(ATK_PREFAB_PATH, RING_PREFAB, GBundle.BattleHero);
if (!tpl?.isValid) return null;
tpl.active = false;
this._ringTemplate = tpl;
}
return this._ringTemplate;
}
/** 回收龙影(首中消耗或飞满距离) */
private _recycleRingClone(visual: Node): void {
if (visual?.isValid) {
SkillTransformDriver.cancelNode(visual);
visual.destroy();
}
const idx = this._ringClones.indexOf(visual);
if (idx >= 0) this._ringClones.splice(idx, 1);
}
/** 回池前清龙影:销毁克隆、归还模板 */
private _disposeRing(): void {
this._ringCtx = null;
for (let i = 0; i < this._ringClones.length; i++) {
const c = this._ringClones[i];
if (c?.isValid) {
SkillTransformDriver.cancelNode(c);
c.destroy();
}
}
this._ringClones.length = 0;
if (this._ringTemplate?.isValid) gg.res.putNode(this._ringTemplate);
this._ringTemplate = null;
}
private _ensureAtk(): void {
if (!this._atkRoot?.isValid) {
const atk = gg.res.getNode(ATK_PREFAB_PATH, ATK_PREFAB, GBundle.BattleHero);

@ -61,7 +61,7 @@ export class LineRectAtkPresenter implements ISkillPresenter {
}
/**
* atk_type=5 直线多段 / 14 360°多段:按 hitIntervalSec 多次 hit,每段独立结算范围内伤害。
* atk_type=5 直线多段 / 14 360°多段 / 16 扇形多段:按 hitIntervalSec 多次 hit,每段独立结算范围内伤害。
*/
export class LineMultiHitAtkPresenter implements ISkillPresenter {
present(ctx: SkillPresentationContext, host: Skill): void {
@ -229,7 +229,7 @@ export class FallbackAtkPresenter implements ISkillPresenter {
}
/**
* atk_type=12 穿透散射:出手时刻扇出直线箭,伤害由 BowAtkHost 结算。
* atk_type=12 穿透散射 / 17 不穿透散射:出手时刻扇出直线箭,伤害由 BowAtkHost 结算。
*/
export class ScatterPierceAtkPresenter implements ISkillPresenter {
present(ctx: SkillPresentationContext, host: Skill): void {
@ -252,7 +252,8 @@ export class ScatterPierceAtkPresenter implements ISkillPresenter {
aimDirect: true,
onAnimComplete: host.onPresentationAnimCompleteBound,
});
host.scheduleBowShapeLaunch(seq, host.visualChannel.resolveBowShapeLaunchDelay(animTs), 12);
const head = Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0);
host.scheduleBowShapeLaunch(seq, host.visualChannel.resolveBowShapeLaunchDelay(animTs), head === 17 ? 17 : 12);
}
}

@ -115,14 +115,17 @@ export class SkillPresenterRouter {
this.registerAtkType(3, instant);
this.registerAtkType(5, lineMulti);
this.registerAtkType(14, lineMulti);
this.registerAtkType(16, lineMulti);
this.registerAtkType(15, instant);
this.registerAtkType(6, instant);
this.registerAtkType(7, instant);
this.registerAtkType(10, new SequentialSingleAtkPresenter());
this.registerAtkType(8, summon);
this.registerAtkType(11, summon);
const projectile = new ProjectileAtkPresenter();
this.registerAtkType(4, projectile);
this.registerAtkType(12, new ScatterPierceAtkPresenter());
this.registerAtkType(17, new ScatterPierceAtkPresenter());
this.registerAtkType(13, new VolleyArrowAtkPresenter());
}

@ -724,7 +724,7 @@ export class SkillVisualChannel {
};
/**
* atk_type=12:扇出直线穿透箭。
* atk_type=12/17:扇出直线散射箭。12 穿透;17 不穿透,首中即回收表现箭。
* @param caster 施法者
* @param abilityId Ability id
* @param target 瞄准目标
@ -734,8 +734,9 @@ export class SkillVisualChannel {
* @param scale 伤害倍率
* @param buff1 atk_buff1
* @param buff2 atk_buff2
* @param pierce 是否穿透
*/
launchScatterPierce(caster: EntityId, abilityId: string, target: EntityId, count: number, stepDeg: number, rangePx: number, scale: number, buff1: string, buff2: string): void {
launchScatterArrows(caster: EntityId, abilityId: string, target: EntityId, count: number, stepDeg: number, rangePx: number, scale: number, buff1: string, buff2: string, pierce: boolean): void {
const template = this._weapon.arrowTemplate;
if (!template?.isValid) {
console.log('[SkillVisualChannel] 散射箭模板缺失', this._weapon.weaponId);
@ -797,6 +798,8 @@ export class SkillVisualChannel {
buff1,
buff2,
hitIds: [],
pierce,
onConsume: pierce ? undefined : this._recycleBowArrow.bind(this, clone),
});
}
}
@ -815,7 +818,7 @@ export class SkillVisualChannel {
}
/**
* atk_type 12/13 首次出手延迟(逻辑秒):弓对齐拉满帧,其它对齐 PRES_HIT。
* atk_type 12/13/17 首次出手延迟(逻辑秒):弓对齐拉满帧,其它对齐 PRES_HIT。
* @param animScale playCastAnim 返回的攻速倍速
*/
resolveBowShapeLaunchDelay(animScale: number): number {

@ -2,6 +2,7 @@ import type { Skill } from '../skill/Skill';
import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
import type { ISkillPresenter } from './SkillPresenter';
import { SkillPresenterRouter } from './SkillPresenterRouter';
import { LineMultiHitAtkPresenter, ScatterPierceAtkPresenter } from './AtkTypePresenters';
import { NezhaChargePresenter } from './NezhaChargePresenter';
import { ArrowRainPresenter } from './ArrowRainPresenter';
import { SpearTossPresenter } from './SpearTossPresenter';
@ -18,6 +19,8 @@ const GUN_ATK_SKILL_IDS: readonly number[] = [10031, 10032, 10033, 10034, 10035]
const GONG_ATK_SKILL_IDS: readonly number[] = [10041, 10042, 10043, 10044, 10045];
/** 玲珑宝塔武器技能 */
const PAGODA_SKILL_ID = 20509;
/** 棍 200005 环形刀光(龙影) */
const GUN_RING_SKILL_ID = 200005;
/** 枪攻效:委托 Qiang(英雄枪-攻击 / 攻击2 + 瞄准) */
export class QiangThrustPresenter implements ISkillPresenter {
@ -43,6 +46,38 @@ export class GunAtkPresenter implements ISkillPresenter {
}
}
/**
* atk_type=16 扇形多段:棍宿主播「英雄棍-攻击」攻效(技能 id 不再固定,无法按 skillId 注册);
* 其余宿主走通用多段表现(字形攻击 + 按段间隔 hit)。
*/
export class FanMultiHitAtkPresenter implements ISkillPresenter {
private readonly _generic = new LineMultiHitAtkPresenter();
present(ctx: SkillPresentationContext, host: Skill): void {
if (host instanceof Gun) {
host.presentFanMultiHit(ctx);
return;
}
this._generic.present(ctx, host);
}
}
/**
* atk_type 12/17 散射:弓宿主委托 Gong(自身「英雄弓-攻击」投掷物扇出;
* 弓字形塔未挂武器通道,通用散射箭模板缺失);其余宿主走通用散射。
*/
export class BowScatterAtkPresenter implements ISkillPresenter {
private readonly _generic = new ScatterPierceAtkPresenter();
present(ctx: SkillPresentationContext, host: Skill): void {
if (host instanceof Gong) {
host.presentScatterAttack(ctx);
return;
}
this._generic.present(ctx, host);
}
}
/** 弓拉弦抛物线:委托 Gong */
export class GongBowPresenter implements ISkillPresenter {
present(ctx: SkillPresentationContext, host: Skill): void {
@ -55,6 +90,18 @@ export class GongBowPresenter implements ISkillPresenter {
}
}
/** 棍 200005 环形刀光:委托 Gun(播完攻效后从字位置环形发出龙影) */
export class GunRingAtkPresenter implements ISkillPresenter {
present(ctx: SkillPresentationContext, host: Skill): void {
if (host instanceof Gun) {
host.presentRingAttack(ctx);
return;
}
console.log('[GunRingAtkPresenter] 宿主非 Gun,逻辑结算', ctx.params.skillId);
host.drainLogicPresentation(ctx);
}
}
let _specialsReady = false;
/** 注册 skillId 特殊表现与 Skill.skill 额外效果(可重复调用) */
@ -75,6 +122,12 @@ export function ensureSpecialSkillPresenters(): void {
router.registerSkillId(GONG_ATK_SKILL_IDS[i], gong);
}
router.registerSkillId(PAGODA_SKILL_ID, new PagodaSmashPresenter());
router.registerSkillId(GUN_RING_SKILL_ID, new GunRingAtkPresenter());
// 覆盖内置 atk_type=16:棍宿主播攻效,其余走通用多段
router.registerAtkType(16, new FanMultiHitAtkPresenter());
// 覆盖内置 atk_type 12/17:弓宿主散射走 Gong 自身投掷物,其余通用
router.registerAtkType(12, new BowScatterAtkPresenter());
router.registerAtkType(17, new BowScatterAtkPresenter());
router.registerExtraSkill(1, new NezhaChargePresenter());
router.registerExtraSkill(2, new ArrowRainPresenter());
router.registerExtraSkill(7, new SpearTossPresenter());

@ -81,6 +81,8 @@ export class Skill extends Component {
private _presAnimSeq = 0;
/** 本次播控动画已结束(多段 hit 不得早于动画 finish) */
private _presAnimComplete = false;
/** 字形动画播完不收招,等攻效播完再收招(环形刀光等) */
private _presHoldUntilFxComplete = false;
/** atk_type=4 弓:当前施法伤害等箭落地,动画结束不得 drain */
private _presBowLand = false;
/** 弓箭落地任务自增 id */
@ -231,6 +233,7 @@ export class Skill extends Component {
}
this._presFinished = false;
this._presAnimComplete = false;
this._presHoldUntilFxComplete = false;
this._presBowLand = false;
this._presBowLandId = 0;
this._presCtx = ctx;
@ -239,6 +242,14 @@ export class Skill extends Component {
return seq;
}
/**
* 字形动画播完不收招,等攻效(Spine)播完由攻效侧 markStaffSmashAnimsComplete 收招。
* 环形刀光(棍 200005)等「攻效播完才发射」的表现使用。
*/
holdPresentationUntilFxComplete(): void {
this._presHoldUntilFxComplete = true;
}
/** 冲锋等:本次播控不由武器/字形动画 complete 收招 */
disarmAnimComplete(): void {
this._presAnimSeq = 0;
@ -249,11 +260,12 @@ export class Skill extends Component {
return seq === this._presSeq && !this._presFinished;
}
/** 当前是否 atk_type=14(多段不门闩预兆) */
private _isAtkType14(): boolean {
/** 多段范围技(14 360°多段 / 16 扇形多段):不门闩预兆,伤害出手即起跳 */
private _isMultiSegRangeAtk(): boolean {
const ctx = this._presCtx;
if (!ctx) return false;
return Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0) === 14;
const head = Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0);
return head === 14 || head === 16;
}
/**
@ -339,7 +351,7 @@ export class Skill extends Component {
this.schedulePresentation(this._onPresCastHitTick, delayLogicalSec);
}
/** 多段 hit:按 hitIntervalSec 逐段推进 deferred 切片(atk_type=5 / 10 / 14) */
/** 多段 hit:按 hitIntervalSec 逐段推进 deferred 切片(atk_type=5 / 10 / 14 / 16) */
schedulePresentationMultiHit(seq: number, ctx: SkillPresentationContext, firstDelayLogicalSec: number): void {
this._presMultiHitSeq = seq;
this._presMultiHitCtx = ctx;
@ -350,7 +362,7 @@ export class Skill extends Component {
const locked = ctx.targets;
for (let i = 0; i < locked.length; i++) this._presSmashPool.push(locked[i]);
}
if (atkHead !== 14 && this._showEffectGate && !this._showEffectApplied) {
if (atkHead !== 14 && atkHead !== 16 && this._showEffectGate && !this._showEffectApplied) {
this._presMultiHitArmed = true;
return;
}
@ -359,10 +371,10 @@ export class Skill extends Component {
}
/**
* atk_type 12/13:出手时刻发射弹道,不 drain deferred。
* atk_type 12/13/17:出手时刻发射弹道,不 drain deferred。
* @param seq 播控序号
* @param delayLogicalSec 首次出手延迟
* @param shape 12 或 13
* @param shape 12 / 13 / 17
*/
scheduleBowShapeLaunch(seq: number, delayLogicalSec: number, shape: number): void {
this._presCastAwaitSeq = seq;
@ -414,6 +426,8 @@ export class Skill extends Component {
if (this._presAnimSeq <= 0 || this._presAnimSeq !== this._presSeq) return;
this._presAnimComplete = true;
if (this._presMultiHitCtx) return;
// 环形刀光等:等攻效播完再收招发射,字形动画播完不 finish
if (this._presHoldUntilFxComplete) return;
// 连发:动画结束绝不提前 finish;只标记等待,射完再收招
if (this._presBowShape === 13) {
this._presAnimWait = true;
@ -424,7 +438,7 @@ export class Skill extends Component {
this._presAnimWait = true;
return;
}
if (this._showEffectGate && !this._showEffectApplied && !this._isAtkType14()) {
if (this._showEffectGate && !this._showEffectApplied && !this._isMultiSegRangeAtk()) {
this._presAnimWaitForShow = true;
return;
}
@ -529,7 +543,7 @@ export class Skill extends Component {
if (this._presFinished || this._presCastAwaitSeq !== this._presSeq) return;
const ctx = this._presCtx;
if (!ctx?.isAlive) return;
if (this._presBowShape === 12) {
if (this._presBowShape === 12 || this._presBowShape === 17) {
this._doScatter(ctx);
return;
}
@ -567,7 +581,7 @@ export class Skill extends Component {
}
/**
* 散射出手。
* 散射出手(12 穿透 / 17 不穿透)。
* @param ctx 表现上下文
*/
private _doScatter(ctx: SkillPresentationContext): void {
@ -576,11 +590,13 @@ export class Skill extends Component {
const stepDeg = parts[1];
const wan = parts[2];
const target = ctx.targets[0] ?? 0;
const head = Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0);
const pierce = head !== 17;
const rangePx = SkillPresentationEnv.getCasterAttr(ctx.caster, AttrId.Range);
if (!(count > 0) || !(stepDeg > 0) || !(wan > 0) || !(rangePx > 0) || !(target > 0)) {
console.log('[Skill] atk_type=12 参数无效', ctx.params.atkPara, rangePx, target);
console.log('[Skill] atk_type=12/17 参数无效', ctx.params.atkPara, rangePx, target);
} else {
this.visualChannel.launchScatterPierce(ctx.caster, ctx.abilityId, target, count, stepDeg, rangePx, wan / 10000, String(ctx.params.buff1 ?? ''), String(ctx.params.buff2 ?? ''));
this.visualChannel.launchScatterArrows(ctx.caster, ctx.abilityId, target, count, stepDeg, rangePx, wan / 10000, String(ctx.params.buff1 ?? ''), String(ctx.params.buff2 ?? ''), pierce);
}
this._scatterLaunched = true;
if (this._presAnimWait) this._finishBowShape();

@ -9,7 +9,7 @@ export interface TableRowSource {
}
/** 索敌已实现的 atk_type 头 */
const ATK_LOGIC: readonly number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
const ATK_LOGIC: readonly number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
/** Skill.skill 额外效果已落地的头 */
const EXTRA_SKILL: readonly number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
/** Weapon.type */

@ -168,6 +168,8 @@ declare global {
readonly icon: string;
/** 资源 */
readonly res: string;
/** 等级 */
readonly level: number;
/** 默认武器 */
readonly startweapon: number;
/** 专属武器 */
@ -354,7 +356,9 @@ declare global {
12、穿透散射箭
13、连发箭
14、360°范围多段伤害
15、目标圆形范围 */
15、目标圆形范围
16、扇形多段
17、不穿透散射箭 */
readonly atk_type: string;
/** 普攻投射物 */
readonly fly: string;
@ -373,7 +377,9 @@ declare global {
12、散射箭数量,角度,伤害系数
13、连发数量,间隔时间,伤害系数
14、伤害段数,伤害间隔,伤害系数
15、半径,伤害系数 */
15、半径,伤害系数
16、伤害段数,伤害间隔,角度,伤害系数
17、散射箭数量,角度,伤害系数 */
readonly atk_para: string;
/** 命中后效果:buff类型,参数
1、眩晕,参数:概率,眩晕时长

@ -1,4 +1,4 @@
# 方案:atk_type 12 穿透散射箭 / 13 连发箭
# 方案:atk_type 12 穿透散射箭 / 13 连发箭 / 17 不穿透散射箭
> 状态:已按确认规则落地
> 日期:2026-09-02
@ -10,9 +10,11 @@
|----------|------|----------|
| 12 | 穿透散射箭 | 散射箭数量, 邻箭夹角(度), 伤害系数(万分比) |
| 13 | 连发箭 | 连发数量, 间隔时间(秒), 伤害系数(万分比) |
| 17 | 不穿透散射箭 | 散射箭数量, 邻箭夹角(度), 伤害系数(万分比) |
- 灵宝弓 `12,5,12,10000`:5 箭、夹角 12°、100% 攻击
- 射日弓 `13,3,0.2,10000`:3 发、间隔 0.2s、100% 攻击
- 试作 `17,2,12,10000` / `17,3,12,10000`:2/3 箭、夹角 12°、100% 攻击(400002/400003)
被动 result 10 把上述形态写到宿主普攻。
@ -27,6 +29,13 @@
- 开战只锁主目标当瞄准;伤害由体盒 tick,不是 `ctx.hit()`。
- 体盒与表现箭同在 **Cocos 世界**(Y 向上),对怪腰点做 OBB,占地按半格放大;`worldToCell` 仅用于候选查询(先把箭位映回逻辑格)。不可把格子逻辑坐标(Y 向下)拿去跟画面箭比。
### 17 不穿透散射
- 扇出、瞄准、飞行距离、体盒判定与 12 完全一致(`atk_para` 同构)。
- **首中即消耗**:每支箭只伤碰到的第一只怪,结算后逻辑弹道移除;表现箭若已飞出可见距离当场回收,出手瞬间命中(近距首中)则保底再飞 0.08s 再回收,避免像没发射;未碰怪则飞满全程消失。
- 其余规则(奇偶补支、开战只锁主目标当瞄准、伤害可暴击走 atk_buff)同 12。
- **弓字形塔(Gong)宿主**:武器通道未挂载(`useWeapon=false`),通用散射箭模板缺失,故 12/17 注册 `BowScatterAtkPresenter`——宿主为 Gong 时用其自身「英雄弓-攻击/投掷物」扇出(拉满时刻发射,直线飞行);普攻行被被动 result 10 覆盖形态时(skillId 仍 10041~10045),`Gong.presentNormAttack` 按 `atkType` 头分流到同一散射出手。
### 13 连发
- 每发只打**首次锁定**的那个目标,不重新索敌。
@ -44,10 +53,10 @@
| 层 | 位置 |
|----|------|
| Ability | `SkillAbilityBuilder`:12/13 索敌单体;timeline 仅 PlayPresentation |
| 逻辑弹道 | `BowAtkRuntime`(对标直线火龙 OBB) |
| 表现 | `SkillVisualChannel.launchScatterPierce` / `launchVolleyShot` |
| 播控 | `ScatterPierceAtkPresenter` / `VolleyArrowAtkPresenter`;`Skill.scheduleBowShapeLaunch` |
| Ability | `SkillAbilityBuilder`:12/13/17 索敌单体;timeline 仅 PlayPresentation |
| 逻辑弹道 | `BowAtkRuntime`(对标直线火龙 OBB;12 穿透 / 17 首中消耗) |
| 表现 | `SkillVisualChannel.launchScatterArrows` / `launchVolleyShot` |
| 播控 | `ScatterPierceAtkPresenter`(12/17)/ `VolleyArrowAtkPresenter`;`Skill.scheduleBowShapeLaunch` |
速度与普通弓箭相同:`1280` px/s。
@ -59,3 +68,6 @@
| 2026-09-08 | 修复连发提前收招只见两发;holdCasting + 弦中点高低/左右弧 |
| 2026-09-11 | 连发改为中/左/右三弹道循环:中间直线,左右向外弯抛物线 |
| 2026-09-11 | 射日弓飞出投掷物缩放 0.6,其它弓保持 1 |
| 2026-09-30 | atk_type=17 不穿透散射箭:首中即消耗(伤害+回收表现箭),扇出/瞄准/体盒同 12(400002/400003) |
| 2026-09-30 | 弓字形塔宿主散射:12/17 注册 BowScatterAtkPresenter,Gong 用自身「英雄弓-攻击/投掷物」扇出;普攻被 result 10 覆盖为 12/17 时 presentNormAttack 同步分流 |
| 2026-09-30 | 修复散射箭出发点偏移(重挂 flyLayer 丢世界坐标)与近距首中箭秒删像没发射(消耗保底可视飞行 0.08s) |

@ -119,6 +119,10 @@ Hero(合成等级 level)
`Skill.range` 内先锁主目标,再以主目标为圆心、`atk_para`=`半径(px),伤害万分比` 收圆内敌(可含射程外)。半径与黄圈同一套:配表像素/83=格,按格心距;并扫格子占地,避免漏塔。圆内仍排除组合字。表现层瞬时类;怪物 `skill=9` 时在主目标处挂 `干扰迷雾`(尺寸由资源控制,不等于逻辑半径)。
### atk_type 16 扇形多段
`atk_para`=`段数,间隔秒,角度,伤害万分比`(例 `2,0.3,60,10000` = 2 段、间隔 0.3s、扇形 60°、每段 100%)。索敌同 atk_type 6 扇形:圈半径=`Skill.range`(≤0 则塔射程),朝威胁最高的怪开口,保留夹角 ≤ 角度/2 的目标。每段 `hit()` 结算一段,段间按间隔推进。`atk_buff1/2` 仍按原命中效果随每段 `DealDamage` 施加(如击退 `2,5000,50`)。
## 6b. 血量上限(配表)
`Skill.atk_buff` type 4、营召唤 `atk_buff` type 4、局外被动 functiontype=15:上限 `×(1+万分比/10000)`,**保持原当前血量/上限比**(例:100 上限 70 当前 +20% → 120 / 84)。允许时播 `血量上限` 并飘「血上限」+`上箭头`,**不播** `怪物加血`;**营召唤出生加成不播**。不是单纯加血(type 10 / skill extra 4)。
@ -156,3 +160,4 @@ Hero(合成等级 level)
| 2026-09-11 | atk_type=10:连续多个单体(次数,间隔秒,伤害万分比),如意金箍棒 20501 |
| 2026-09-11 | atk_type=10:每次砸独立武器实例,不在原武器上重播 |
| 2026-09-11 | target_stage=2:maxCount 仅为上限,每人一次,不循环补位(悟净治疗术/悟能疾行术) |
| 2026-09-30 | atk_type=16:扇形多段(段数,间隔秒,角度,伤害万分比);atk_buff1/2 命中效果保持不变(100001~100005) |

@ -57,10 +57,11 @@ PlayPresentation cue
| atk_type | 表现 |
|----------|------|
| 1 / 3 / 6 / 7 / 15 | 通道播动画 → drain hit(15:逻辑已锁主目标圆内目标) |
| 2 / 5 / 14 | 朝向 + 动画(5 直线多段、14 为 360° 圈内多段 hit) |
| 2 / 5 / 14 / 16 | 朝向 + 动画(5 直线多段、14 为 360° 圈内多段 hit、16 扇形多段 hit) |
| 10 | 连砸:开战锁最多 N 个;每次独立克隆武器播 `Skill.animation`,砸出帧起按间隔 `hit()` |
| 4 | 弓「箭」抛物线;非弓隐藏原武器且不播原武器攻击,飞「玉帝飞剑」(皮肤=Weapon.skin,动画=「玉帝普攻」,朝向切线);不读 Skill.fly;**伤害对齐箭/飞剑落地**(出弦不扣血;落地后动画结束不得再 drain,避免近距两段伤) |
| 12 | 直线散射穿透:中线对准目标,左右按夹角扇出;飞距=射程×2 |
| 17 | 散射扇出同 12;不穿透,首中即结算并回收表现箭(`launchScatterArrows`) |
| 13 | 连发:锁首发目标,间隔再射,落地才伤;中直线 / 左外弯 / 右外弯三弹道循环 |
| 8 / 11 | 武器按待机角播 `Skill.animation` 后 hit → `SummonFromSkill`;`atk_para` 第 4 段 1=施法处出现后跳入进路 / 2=自身周围 |
@ -72,6 +73,12 @@ PlayPresentation cue
`Skill.skill` 额外效果头优先于 `atk_type`(见 [方案-Skill额外效果.md](./方案-Skill额外效果.md))。type 1 哪吒冲锋由 `NezhaChargePresenter` 播控。玲珑宝塔 `Skill.id=20509`:主目标处播 `prefab/skill/宝塔` 的「宝塔砸地技能2」,播完回池;命中与施法锁仍走 atk_type=3(武器通道 `宝塔砸地技能1`)。
例外:`atk_type=16` 注册 `FanMultiHitAtkPresenter`(覆盖内置)——宿主为 `Gun`(棍)时播「英雄棍-攻击」攻效(`Gun.presentFanMultiHit`,按段间隔逐段 hit);其余宿主走通用多段表现。棍普攻 skill id 已不再固定(Hero 3 现指 100003),无法按 skillId 注册。
例外:`atk_type` 12/17 注册 `BowScatterAtkPresenter`(覆盖内置)——宿主为 `Gong`(弓字形塔,未挂武器通道)时委托 `Gong.presentScatterAttack`,用自身「英雄弓-攻击/投掷物」扇出直线箭;其余宿主走通用 `launchScatterArrows`。
棍环形刀光 `Skill.id=200005` 注册 `GunRingAtkPresenter`:宿主为 `Gun` 时播「英雄棍-攻击/攻击2」攻效,攻效开播延迟 0.5s 后从字的位置逆时针链式发射龙影(`prefab/skill/棍`,上一支飞出 200px 发下一支);数量=360/角度间隔 均分整圆(起始角/角度间隔为代码约定 `RING_START_DEG`/`RING_STEP_DEG`,上限 18),伤害系数取 `atk_para` 第 3 段;龙影走体盒弹道,首中即结算消耗,无怪飞满距离消失;`atk_type` 保留 14 多段配置但表现不 drain 延迟段(无双伤),伤害全在弹道;攻效播完即收招(不等字形攻击动画)。其余宿主逻辑结算。
## 5. 代码位置
| 模块 | 路径 |
@ -126,3 +133,9 @@ PlayPresentation cue
| 2026-09-12 | 背包重建不得 setData 地图格;落子/升级/合成只写绑定期变化的槽/格,禁止扫场 setData |
| 2026-09-12 | 叠字根因:合成把飞来块挂到已占格、飞行中按空格 spawn、rebuild 按 weapon2 把别格字拽过来。禁止造第二块,不靠事后清 |
| 2026-09-12 | atk_type=4 弓:落地后保持「等箭落地」标记到收招,近距箭先落地时动画结束不再 drain 第二段(嫦娥普攻) |
| 2026-09-30 | atk_type=16:扇形多段 hit(复用 5/14 多段表现通道,100001~100005) |
| 2026-09-30 | atk_type=16 棍:FanMultiHitAtkPresenter 覆盖内置,Gun 宿主播「英雄棍-攻击」攻效;攻效播完即解锁收招(Hero 3 atk_id → 100003) |
| 2026-09-30 | atk_type=17 不穿透散射箭:散射扇出同 12,首中即结算并当场回收表现箭(400002/400003) |
| 2026-09-30 | 修复弓字形塔散射不发射:Gong 宿主 12/17 委托自身「英雄弓-攻击/投掷物」扇出(BowScatterAtkPresenter),result 10 覆盖普攻形态同步分流 |
| 2026-09-30 | 补注册 atk_type=11 召唤(10051~10055)→ SummonAtkPresenter;表内 1~17 表现已全覆盖,仅 9(将武器形 30406)按兜底结算待策划定义攻效 |
| 2026-09-30 | 棍 200005 环形刀光:GunRingAtkPresenter,攻效播完从字位置按 atk_para(起始角,角度间隔,伤害系数)逆时针发射龙影(prefab/skill/棍),体盒弹道首中即消耗;atk_type 改 17(锁单体、伤害全在弹道) |

@ -1,5 +1,5 @@
{
"name": "AdLikeXy",
"name": "AdLike2048",
"uuid": "d7fce9be-f307-4e7f-a8f5-3d0b5ca1e096",
"creator": {
"version": "3.8.8"

Loading…
Cancel
Save