中毒和冰球术

master
humingxing 18 hours ago
parent abd9d76b99
commit 9a5592a40e
  1. 262
      assets/bundles/battleGameMonster/prefab/怪_中毒.prefab
  2. 13
      assets/bundles/battleGameMonster/prefab/怪_中毒.prefab.meta
  3. 262
      assets/bundles/battleHero/prefab/skill/冰球.prefab
  4. 13
      assets/bundles/battleHero/prefab/skill/冰球.prefab.meta
  5. 5
      assets/mx/module/gm/GmEasy.ts
  6. 139
      assets/res/battleGameWeapon/spine/技能特效/BUFF/中毒效果.atlas
  7. 12
      assets/res/battleGameWeapon/spine/技能特效/BUFF/中毒效果.atlas.meta
  8. 1
      assets/res/battleGameWeapon/spine/技能特效/BUFF/中毒效果.json
  9. 13
      assets/res/battleGameWeapon/spine/技能特效/BUFF/中毒效果.json.meta
  10. BIN
      assets/res/battleGameWeapon/spine/技能特效/BUFF/中毒效果.png
  11. 134
      assets/res/battleGameWeapon/spine/技能特效/BUFF/中毒效果.png.meta
  12. 9
      assets/res/battleGameWeapon/spine/技能特效/陨落冰球.meta
  13. 1
      assets/res/battleGameWeapon/spine/技能特效/陨落冰球/.WeDrive
  14. 139
      assets/res/battleGameWeapon/spine/技能特效/陨落冰球/冰球陨石.atlas
  15. 12
      assets/res/battleGameWeapon/spine/技能特效/陨落冰球/冰球陨石.atlas.meta
  16. 1
      assets/res/battleGameWeapon/spine/技能特效/陨落冰球/冰球陨石.json
  17. 13
      assets/res/battleGameWeapon/spine/技能特效/陨落冰球/冰球陨石.json.meta
  18. BIN
      assets/res/battleGameWeapon/spine/技能特效/陨落冰球/冰球陨石.png
  19. 134
      assets/res/battleGameWeapon/spine/技能特效/陨落冰球/冰球陨石.png.meta
  20. 18
      assets/script/battle/BattleService.ts
  21. 19
      assets/script/battle/ai/bridge/AiBattleBridge.ts
  22. 88
      assets/script/battle/combat/PassiveSkillRuntime.ts
  23. 2
      assets/script/battle/config/ChapterLevelBuilder.ts
  24. 7
      assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts
  25. 271
      assets/script/battle/presentation/IceBallAtkPresenter.ts
  26. 9
      assets/script/battle/presentation/IceBallAtkPresenter.ts.meta
  27. 2
      assets/script/battle/presentation/SpecialSkillPresenters.ts
  28. 3
      assets/script/battle/runtime/BattleRuntime.ts
  29. 25
      assets/script/battle/view/CocosBattleCombatPresenter.ts
  30. 3
      assets/script/game/GameController.ts
  31. 64
      assets/script/gm/ProjectGmActions.ts
  32. 8
      assets/script/gm/ProjectGmBootstrap.ts
  33. 369
      assets/script/gm/ProjectGmPanels.ts
  34. 27
      assets/script/manager/DailyShopDataManager.ts
  35. 60
      assets/script/ui/BattleGame/Monster.ts

@ -0,0 +1,262 @@
[
{
"__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__": 10
}
],
"_prefab": {
"__id__": 12
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "anim",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
},
{
"__id__": 7
}
],
"_prefab": {
"__id__": 9
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 40.832,
"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__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 105.44999694824219,
"height": 198.25999450683594
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.4653390338067923,
"y": 0.22924443671046482
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "03h5CL5PFEtKYvKtyo9JFN"
},
{
"__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__": "25a41a00-4bf7-4021-87d4-f8c80309ec53",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "default",
"defaultAnimation": "animation",
"_premultipliedAlpha": false,
"_timeScale": 1,
"_preCacheMode": 2,
"_cacheMode": 2,
"_sockets": [],
"_useTint": false,
"_debugMesh": false,
"_debugBones": false,
"_debugSlots": false,
"_enableBatch": true,
"loop": true,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "3dGN5uYgVBA5YfZxnqBCr0"
},
{
"__type__": "cc.Sorting2D",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 8
},
"_sortingLayer": 4,
"_sortingOrder": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "3eg+71aSNIHpRSdR16zp+r"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "b2X8MHIMdL4oWgMCIc8jCb",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 11
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "8fKe6v5I1Jeq7/0RxYFv/t"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "ccwpiVeu1GJohxospbaLDW",
"instance": null,
"targetOverrides": null
}
]

@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "27d71c32-ea24-42f6-bc98-4b6d07251929",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "怪_中毒"
}
}

@ -0,0 +1,262 @@
[
{
"__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__": 10
}
],
"_prefab": {
"__id__": 12
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "冰球陨石",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
},
{
"__id__": 7
}
],
"_prefab": {
"__id__": 9
},
"_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__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 622.1900024414062,
"height": 413.239990234375
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.10895385758293562,
"y": 0.44748814862778713
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "179HuDnA9Jo7TgGvC9xZhc"
},
{
"__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__": "781dbed2-3bf9-4e54-bdba-cb12c23529ec",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "default",
"defaultAnimation": "animation",
"_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": "a8P1wKkP9DwKwfmMn/qsjD"
},
{
"__type__": "cc.Sorting2D",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 8
},
"_sortingLayer": 4,
"_sortingOrder": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "22RwYWwElLeJ6kJ0xw9tQg"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "f5NqVBVAFNbqc4oknHHrnQ",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 11
},
"_contentSize": {
"__type__": "cc.Size",
"width": 200,
"height": 200
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "451RzMpQpFLalWlPKOafD2"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "38JEit66ZI+Y7iGQ+17T7W",
"instance": null,
"targetOverrides": null
}
]

@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "e5090e75-028c-4066-87d5-ab02f82fcaff",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "冰球"
}
}

@ -143,8 +143,10 @@ export class GmEasy {
/**
* 当前 Tab 直接嵌入内容面板(整页展示,无列表入口)。
* @param panelId 已注册面板 id
* @param panelData 面板数据(可选)
*/
embed(panelId: string): this {
embed(panelId: string, panelData?: unknown): this {
this._push({
id: this._id('embed'),
title: this._group,
@ -152,6 +154,7 @@ export class GmEasy {
kind: GmKind.Content,
order: ++this._order,
panelId,
panelData,
embed: true,
});
return this;

@ -0,0 +1,139 @@
中毒效果.png
size: 698,329
format: RGBA8888
filter: Linear,Linear
repeat: none
无标题1
rotate: false
xy: 601, 11
size: 69, 87
orig: 74, 92
offset: 2, 3
index: -1
无标题10
rotate: false
xy: 103, 156
size: 77, 173
orig: 81, 178
offset: 2, 3
index: -1
无标题11
rotate: false
xy: 263, 159
size: 77, 170
orig: 81, 176
offset: 2, 3
index: -1
无标题12
rotate: false
xy: 451, 4
size: 76, 161
orig: 81, 167
offset: 2, 3
index: -1
无标题13
rotate: false
xy: 222, 4
size: 74, 151
orig: 79, 157
offset: 3, 3
index: -1
无标题14
rotate: false
xy: 376, 27
size: 73, 131
orig: 77, 136
offset: 2, 3
index: -1
无标题15
rotate: false
xy: 342, 160
size: 74, 169
orig: 78, 175
offset: 2, 3
index: -1
无标题16
rotate: false
xy: 418, 167
size: 74, 162
orig: 79, 166
offset: 3, 2
index: -1
无标题17
rotate: false
xy: 150, 0
size: 70, 154
orig: 74, 160
offset: 2, 3
index: -1
无标题18
rotate: false
xy: 529, 7
size: 70, 91
orig: 74, 96
offset: 2, 2
index: -1
无标题2
rotate: false
xy: 494, 198
size: 71, 131
orig: 77, 137
offset: 3, 4
index: -1
无标题3
rotate: true
xy: 567, 260
size: 69, 131
orig: 73, 136
offset: 2, 3
index: -1
无标题4
rotate: false
xy: 0, 1
size: 72, 135
orig: 76, 139
offset: 2, 2
index: -1
无标题5
rotate: false
xy: 74, 3
size: 74, 133
orig: 79, 138
offset: 2, 3
index: -1
无标题6
rotate: false
xy: 298, 25
size: 76, 132
orig: 80, 138
offset: 2, 3
index: -1
无标题7
rotate: true
xy: 567, 180
size: 78, 130
orig: 83, 135
offset: 2, 2
index: -1
无标题8
rotate: true
xy: 529, 100
size: 78, 123
orig: 83, 128
offset: 2, 3
index: -1
无标题9
rotate: false
xy: 182, 157
size: 79, 172
orig: 83, 178
offset: 2, 3
index: -1
毒雾
rotate: false
xy: 0, 138
size: 101, 191
orig: 101, 196
offset: 0, 5
index: -1

@ -0,0 +1,12 @@
{
"ver": "1.0.0",
"importer": "*",
"imported": true,
"uuid": "c7574f4d-50fa-49f3-9ffd-f193fac7ee9c",
"files": [
".atlas",
".json"
],
"subMetas": {},
"userData": {}
}

@ -0,0 +1 @@
{"skeleton":{"hash":"ae4vm7m4zHliUH2j9YM0oDF2390","spine":"3.8.75","x":-49.07,"y":-45.45,"width":105.45,"height":198.26,"images":"./images/","audio":"E:/工作文件/1项目汇总/5战斗游戏/【新】数字大作战/动效/局内效果/毒物"},"bones":[{"name":"root"},{"name":"毒物","parent":"root","length":38.78,"rotation":0.83,"x":1.53,"y":-13.42,"scaleY":0.8306},{"name":"毒雾","parent":"root","length":71.22,"rotation":89.76,"x":3.43,"y":-14.19},{"name":"毒雾2","parent":"root","length":71.22,"rotation":89.76,"x":3.43,"y":-14.19,"scaleY":-1}],"slots":[{"name":"毒物","bone":"毒物","attachment":"无标题1"},{"name":"毒雾","bone":"毒雾","attachment":"毒雾"},{"name":"毒雾2","bone":"毒雾2","attachment":"毒雾"}],"skins":[{"name":"default","attachments":{"毒雾2":{"毒雾":{"x":67.87,"y":-0.05,"rotation":-88.93,"width":101,"height":196}},"毒物":{"无标题1":{"x":0.68,"y":32.14,"rotation":-0.95,"width":74,"height":92},"无标题2":{"x":-0.06,"y":15.65,"rotation":-0.83,"width":77,"height":137},"无标题3":{"x":1.01,"y":20.64,"rotation":-0.83,"width":73,"height":136},"无标题4":{"x":-0.39,"y":27.16,"rotation":-0.83,"width":76,"height":139},"无标题5":{"x":0.21,"y":34.15,"rotation":-0.83,"width":79,"height":138},"无标题6":{"x":0.79,"y":40.14,"rotation":-0.83,"width":80,"height":138},"无标题7":{"x":1.39,"y":46.64,"rotation":-0.83,"width":83,"height":135},"无标题8":{"x":1.48,"y":53.13,"rotation":-0.83,"width":83,"height":128},"无标题9":{"x":2.17,"y":31.62,"rotation":-0.83,"width":83,"height":178},"无标题10":{"x":0.21,"y":34.15,"rotation":-0.83,"width":81,"height":178},"无标题11":{"x":0.27,"y":38.65,"rotation":-0.83,"width":81,"height":176},"无标题12":{"x":0.37,"y":45.65,"rotation":-0.83,"width":81,"height":167},"无标题13":{"x":-0.54,"y":51.66,"rotation":-0.83,"width":79,"height":157},"无标题14":{"x":-0.5,"y":54.16,"rotation":-0.83,"width":77,"height":136},"无标题15":{"x":-0.23,"y":38.16,"rotation":-0.83,"width":78,"height":175},"无标题16":{"x":0.36,"y":44.65,"rotation":-0.83,"width":79,"height":166},"无标题17":{"x":-0.06,"y":50.16,"rotation":-0.83,"width":74,"height":160},"无标题18":{"x":0.59,"y":26.14,"rotation":-0.83,"width":74,"height":96}},"毒雾":{"毒雾":{"x":67.87,"y":-0.05,"rotation":-88.93,"width":101,"height":196}}}}],"animations":{"animation":{"slots":{"毒雾":{"color":[{"color":"ffffff00"},{"time":0.6,"color":"ffffffff"},{"time":1.2333,"color":"ffffff00"}]},"毒雾2":{"color":[{"color":"ffffffff"},{"time":0.6333,"color":"ffffff00"},{"time":1.2333,"color":"ffffffff"}]},"毒物":{"attachment":[{"time":0.0667,"name":"无标题2"},{"time":0.1333,"name":"无标题3"},{"time":0.2,"name":"无标题4"},{"time":0.2667,"name":"无标题5"},{"time":0.3333,"name":"无标题6"},{"time":0.4,"name":"无标题7"},{"time":0.4667,"name":"无标题8"},{"time":0.5333,"name":"无标题9"},{"time":0.6,"name":"无标题10"},{"time":0.7,"name":"无标题11"},{"time":0.7667,"name":"无标题12"},{"time":0.8333,"name":"无标题13"},{"time":0.9,"name":"无标题14"},{"time":0.9667,"name":"无标题15"},{"time":1.0667,"name":"无标题16"},{"time":1.1333,"name":"无标题17"},{"time":1.2,"name":"无标题18"}]}},"bones":{"毒雾":{"translate":[{"x":-1.58,"y":-23.25}],"scale":[{"x":0.752},{"time":1.2},{"time":1.2333,"x":0.752}]},"毒雾2":{"translate":[{"x":-1.58,"y":-23.25}],"scale":[{"x":0.876},{"time":0.6},{"time":0.6333,"x":0.752},{"time":1.2333,"x":0.876}]}}}}}

@ -0,0 +1,13 @@
{
"ver": "1.2.7",
"importer": "spine-data",
"imported": true,
"uuid": "25a41a00-4bf7-4021-87d4-f8c80309ec53",
"files": [
".json"
],
"subMetas": {},
"userData": {
"atlasUuid": "c7574f4d-50fa-49f3-9ffd-f193fac7ee9c"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "12a7084a-71ac-476f-8fdd-c6ec2a3dd801",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "12a7084a-71ac-476f-8fdd-c6ec2a3dd801@6c48a",
"displayName": "中毒效果",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "12a7084a-71ac-476f-8fdd-c6ec2a3dd801",
"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": "12a7084a-71ac-476f-8fdd-c6ec2a3dd801@f9941",
"displayName": "中毒效果",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 698,
"height": 329,
"rawWidth": 698,
"rawHeight": 329,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-349,
-164.5,
0,
349,
-164.5,
0,
-349,
164.5,
0,
349,
164.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
329,
698,
329,
0,
0,
698,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-349,
-164.5,
0
],
"maxPos": [
349,
164.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "12a7084a-71ac-476f-8fdd-c6ec2a3dd801@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "12a7084a-71ac-476f-8fdd-c6ec2a3dd801@6c48a"
}
}

@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6f2bef0b-9419-4eee-84fb-01fd55a1715e",
"files": [],
"subMetas": {},
"userData": {}
}

@ -0,0 +1 @@
D:\WXWork\1688855084654441\WeDrive\中昱智云\抖音游戏\战斗类\【新】扑克牌请神\动效\局内效果\陨落冰球

@ -0,0 +1,139 @@
冰球陨石.png
size: 650,518
format: RGBA8888
filter: Linear,Linear
repeat: none
冰球
rotate: true
xy: 528, 394
size: 124, 119
orig: 126, 121
offset: 1, 1
index: -1
冲击波
rotate: false
xy: 0, 299
size: 526, 219
orig: 535, 237
offset: 3, 7
index: -1
图层 1179
rotate: false
xy: 410, 265
size: 29, 32
orig: 31, 34
offset: 1, 1
index: -1
图层 1180
rotate: true
xy: 603, 255
size: 50, 45
orig: 52, 47
offset: 1, 1
index: -1
图层 1181
rotate: false
xy: 441, 261
size: 12, 15
orig: 14, 17
offset: 1, 1
index: -1
图层 1182
rotate: true
xy: 441, 278
size: 19, 17
orig: 21, 19
offset: 1, 1
index: -1
图层 1183
rotate: false
xy: 455, 264
size: 10, 12
orig: 12, 14
offset: 1, 1
index: -1
图层 1184
rotate: false
xy: 291, 261
size: 43, 36
orig: 45, 38
offset: 1, 1
index: -1
图层 1185
rotate: false
xy: 609, 354
size: 41, 38
orig: 43, 40
offset: 1, 1
index: -1
图层 1186
rotate: true
xy: 420, 41
size: 62, 57
orig: 65, 59
offset: 1, 1
index: -1
图层 1187
rotate: false
xy: 479, 51
size: 52, 52
orig: 54, 54
offset: 1, 1
index: -1
图层 1188
rotate: true
xy: 336, 264
size: 33, 44
orig: 35, 46
offset: 1, 1
index: -1
图层 1189
rotate: false
xy: 382, 264
size: 26, 33
orig: 29, 35
offset: 2, 1
index: -1
图层 1190
rotate: true
xy: 528, 329
size: 63, 79
orig: 65, 81
offset: 1, 1
index: -1
图层 1191
rotate: false
xy: 528, 261
size: 73, 66
orig: 75, 71
offset: 1, 4
index: -1
图层 1192
rotate: false
xy: 609, 307
size: 40, 45
orig: 56, 47
offset: 15, 1
index: -1
拖尾
rotate: true
xy: 420, 105
size: 148, 224
orig: 152, 239
offset: 2, 9
index: -1
法阵11
rotate: true
xy: 0, 3
size: 294, 289
orig: 296, 290
offset: 1, 0
index: -1
裂隙
rotate: true
xy: 291, 0
size: 253, 127
orig: 272, 130
offset: 18, 1
index: -1

@ -0,0 +1,12 @@
{
"ver": "1.0.0",
"importer": "*",
"imported": true,
"uuid": "0569be0e-0391-4613-a3c9-3ef1407de3c7",
"files": [
".atlas",
".json"
],
"subMetas": {},
"userData": {}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,13 @@
{
"ver": "1.2.7",
"importer": "spine-data",
"imported": true,
"uuid": "781dbed2-3bf9-4e54-bdba-cb12c23529ec",
"files": [
".json"
],
"subMetas": {},
"userData": {
"atlasUuid": "0569be0e-0391-4613-a3c9-3ef1407de3c7"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "016b4c5e-2cc0-4f67-92b1-7451af8adac5",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "016b4c5e-2cc0-4f67-92b1-7451af8adac5@6c48a",
"displayName": "冰球陨石",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "016b4c5e-2cc0-4f67-92b1-7451af8adac5",
"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": "016b4c5e-2cc0-4f67-92b1-7451af8adac5@f9941",
"displayName": "冰球陨石",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 650,
"height": 518,
"rawWidth": 650,
"rawHeight": 518,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-325,
-259,
0,
325,
-259,
0,
-325,
259,
0,
325,
259,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
518,
650,
518,
0,
0,
650,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-325,
-259,
0
],
"maxPos": [
325,
259,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "016b4c5e-2cc0-4f67-92b1-7451af8adac5@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "016b4c5e-2cc0-4f67-92b1-7451af8adac5@6c48a"
}
}

@ -44,6 +44,7 @@ export class BattleService implements IBattleService {
this.end();
BattleBuffCarryStore.clear();
BattleBuffSelectService.resetAll();
this._seedGmCarryBuffs(params.localPlayerId ?? 0);
const runtime = createBattleRuntime(params);
if (!runtime) {
console.error(`[BattleService] start failed level=${params.levelId} rank=${params.playerRankId} opp=${params.opponentRankId}`);
@ -53,6 +54,23 @@ export class BattleService implements IBattleService {
return runtime;
}
/**
* GM 词条:开战 clear 后按 gg.game.gmCarryBuffIds 灌入本机 CarryStore。
* @param localPlayerId 本机槽位
*/
private _seedGmCarryBuffs(localPlayerId: number): void {
const ids = gg.game.gmCarryBuffIds;
if (!ids?.length) return;
const pid = Math.max(0, Math.floor(Number(localPlayerId) || 0));
let ok = 0;
for (let i = 0; i < ids.length; i++) {
const parsed = BattleBuffCarryStore.appendBuffId(pid, ids[i]);
if (parsed) ok++;
else console.warn('[BattleService] GM 词条灌入失败', ids[i]);
}
console.log(`[BattleService] GM 词条灌入 player=${pid} count=${ok}/${ids.length}`);
}
end(): void {
BattleUiPauseGate.clear();
const levelId = this._match?.levelId;

@ -401,7 +401,24 @@ export class AiBattleBridge {
private _mountAiBuffs(profile: AiMatchProfile, battle: ProjectBattleHandle): void {
const aiPid = AiHeadlessBagHost.AI_PLAYER_ID;
BattleBuffCarryStore.setFromBuffIds(aiPid, [], []);
aiOp(`AI buff 赛前清空(局内解锁三选一)profilePassive=[${profile.passiveBuffIds.join(',')}]`);
const gmPassive = gg.game.gmAiPassiveBuffIds;
const gmActive = gg.game.gmAiActiveBuffIds;
let seeded = 0;
if (gmActive?.length) {
for (let i = 0; i < gmActive.length; i++) {
if (BattleBuffCarryStore.appendBuffId(aiPid, gmActive[i])) seeded++;
}
}
if (gmPassive?.length) {
for (let i = 0; i < gmPassive.length; i++) {
if (BattleBuffCarryStore.appendBuffId(aiPid, gmPassive[i])) seeded++;
}
}
if (seeded > 0) {
aiOp(`GM 覆盖 AI buff 灌入 count=${seeded} profilePassive=[${profile.passiveBuffIds.join(',')}]`);
} else {
aiOp(`AI buff 赛前清空(局内解锁三选一)profilePassive=[${profile.passiveBuffIds.join(',')}]`);
}
getBattleBuffMoveSpeedHost().bind(battle);
getBattleBuffUnitAttackSpeedHost().bind(battle);
getBattleBuffBaseThunderHost().refreshCarry(battle);

@ -163,6 +163,10 @@ export const SWORD_WAVE_EVENT = 'SwordWaveApplied';
export const LIGHTNING_CHAIN_EVENT = 'LightningChainApplied';
/** 分裂冰锥表现事件名 */
export const ICE_CONE_SPLIT_EVENT = 'IceConeSplitApplied';
/** 中毒表现事件名(上毒/刷新) */
export const POISON_APPLIED_EVENT = 'PoisonApplied';
/** 中毒清除表现事件名(目标上已无毒 DoT) */
export const POISON_REMOVED_EVENT = 'PoisonRemoved';
/** 直线火龙预制体宽 100,命中半长(再加半格占地) */
const LINE_DRAGON_HALF_LEN = 50;
/** 直线火龙预制体高 30,命中半宽(再加半格占地) */
@ -198,6 +202,14 @@ export interface SwordArrayCue {
radius: number;
}
/** 中毒表现 cue */
export interface PoisonCue {
/** 中毒目标 */
target: EntityId;
/** 持续秒(刷新时为新剩余时长) */
duration: number;
}
/** 爆炸火球一枚(径向直线) */
export interface HomingDragonMissile {
/** 方向单位 X */
@ -808,9 +820,14 @@ export class PassiveSkillHost {
this._removeHit(b, target);
}
}
let removed = false;
for (let i = this._dots.length - 1; i >= 0; i--) {
if (this._dots[i].target === target) this._dots.splice(i, 1);
if (this._dots[i].target === target) {
this._dots.splice(i, 1);
removed = true;
}
}
if (removed) this._syncPoisonFx(target);
}
/**
@ -1244,7 +1261,7 @@ export class PassiveSkillHost {
}
/**
* 上毒:立即首跳,再按间隔跳至结束。
* 上毒:立即首跳,再按间隔跳至结束;并发中毒表现 cue。
* @param caster 施法者
* @param target 目标
* @param skillId Skill.id
@ -1267,10 +1284,12 @@ export class PassiveSkillHost {
d.scale = scale;
d.left = dur;
d.wait = 0;
this._emitPoisonApplied(target);
return;
}
}
this._dots.push({ caster, target, skillId, interval, scale, left: dur, wait: 0 });
this._emitPoisonApplied(target);
}
/**
@ -2173,7 +2192,9 @@ export class PassiveSkillHost {
for (let i = this._dots.length - 1; i >= 0; i--) {
const d = this._dots[i];
if (!session.world.isAlive(d.caster) || !session.world.isAlive(d.target)) {
const target = d.target;
this._dots.splice(i, 1);
this._syncPoisonFx(target);
continue;
}
d.left -= dt;
@ -2275,14 +2296,61 @@ export class PassiveSkillHost {
* @param dot 毒跳
*/
private _removeDot(dot: PoisonDot): void {
const target = dot.target;
for (let i = this._dots.length - 1; i >= 0; i--) {
if (this._dots[i] === dot) {
this._dots.splice(i, 1);
this._syncPoisonFx(target);
return;
}
}
}
/**
* 目标上剩余毒 DoT 的最大时长。
* @param target 目标
*/
private _poisonLeftMax(target: EntityId): number {
let max = 0;
for (let i = 0; i < this._dots.length; i++) {
const d = this._dots[i];
if (d.target === target && d.left > max) max = d.left;
}
return max;
}
/**
* 同步中毒表现:有毒则刷新时长,无毒则清除。
* @param target 目标
*/
private _syncPoisonFx(target: EntityId): void {
const left = this._poisonLeftMax(target);
if (left > 0) this._emitPoisonApplied(target);
else this._emitPoisonRemoved(target);
}
/**
* 发出中毒表现 cue(时长取目标上剩余毒最大值)。
* @param target 中毒目标
*/
private _emitPoisonApplied(target: EntityId): void {
const session = this._session;
const duration = this._poisonLeftMax(target);
if (!session || !(target > 0) || !(duration > 0)) return;
const cue: PoisonCue = { target, duration };
session.events.emit(BattleEventFactory.custom(POISON_APPLIED_EVENT, cue));
}
/**
* 发出中毒清除 cue。
* @param target 目标
*/
private _emitPoisonRemoved(target: EntityId): void {
const session = this._session;
if (!session || !(target > 0)) return;
session.events.emit(BattleEventFactory.custom(POISON_REMOVED_EVENT, { target }));
}
/** 卸下时清攻速/射程 modifier */
private _stripMods(entityId: EntityId): void {
const session = this._session;
@ -2300,8 +2368,22 @@ export class PassiveSkillHost {
/** 卸下施法者时清其毒 */
private _removeDotsByCaster(caster: EntityId): void {
const touched: EntityId[] = [];
for (let i = this._dots.length - 1; i >= 0; i--) {
if (this._dots[i].caster === caster) this._dots.splice(i, 1);
const d = this._dots[i];
if (d.caster !== caster) continue;
let dup = false;
for (let j = 0; j < touched.length; j++) {
if (touched[j] === d.target) {
dup = true;
break;
}
}
if (!dup) touched.push(d.target);
this._dots.splice(i, 1);
}
for (let i = 0; i < touched.length; i++) {
this._syncPoisonFx(touched[i]);
}
}

@ -281,7 +281,7 @@ export class ChapterLevelBuilder {
* - 上阵武器 skill / debuff 关联特效
*/
static collectLevelStatusFxPrefabs(levelId: number): string[] {
const prefabs = membershipFromArray<string>(['怪_攻击buff', '怪_加速', '怪物加血', '血量上限', '冰冻']);
const prefabs = membershipFromArray<string>(['怪_攻击buff', '怪_加速', '怪物加血', '血量上限', '冰冻', '怪_中毒']);
for (const fx of ALWAYS_BATTLE_DEBUFF_STATUS_FX) {
membershipAdd(prefabs, fx);
}

@ -11,6 +11,9 @@ import {
normalAbilityId,
} from '../weapon1/Weapon1BattleIds';
/** 冰球术 Skill.id(表现层落地结算) */
const ICE_BALL_SKILL_ID = 305201;
/** 技能槽位(驱动 cue / Ability id) */
export type SkillAbilityKind = 'normal' | 'hero' | 'weapon' | 'monster';
@ -695,9 +698,9 @@ export class SkillAbilityBuilder {
// 12 散射 / 13 连发 / 17 不穿透散射 / 19 冰锥:伤害由 BowAtkHost 弹道结算,无 deferred 段
// 14 360°多段 / 18 / 20:每段按圆重新索敌结算,无 deferred 段
// 飞剑主动(atk_type=10 子集):落地结算,无 deferred 段
// 飞剑主动(atk_type=10 子集)/ 冰球术 305201:表现层落地结算,无 deferred 段
if (atkHead === 12 || atkHead === 13 || atkHead === 14 || atkHead === 17 || atkHead === 18 || atkHead === 19 || atkHead === 20
|| isFlySwordSkillId(row.id)) {
|| isFlySwordSkillId(row.id) || row.id === ICE_BALL_SKILL_ID) {
return [];
}

@ -0,0 +1,271 @@
import { isValid, Node, UITransform, Vec3, sp } from 'cc';
import type { EntityId } from 'db://assets/mx/module/ecs';
import { TransformComp } from 'db://assets/mx/module/battle/ecs/Components';
import { GBundle, GPath } from '../../game/ConfigRes';
import { logicalWorldToCocos } from '../bridge/CocosBattleMovementBridge';
import type { Skill } from '../skill/Skill';
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
import type { ISkillPresenter } from './SkillPresenter';
import { BattleSpinePlayback } from './BattleSpinePlayback';
import { buildShadowDealContext } from './SkillShadowContext';
import { logicPosOf, pulseHostileCircle } from './ThrownCircleAtkPresenter';
/** 冰球术 Skill.id(Battlebuff 3052) */
export const ICE_BALL_SKILL_ID = 305201;
/** battleHero/prefab/skill/冰球 */
const ICE_BALL_PREFAB = '冰球';
/** 默认动画名 */
const ICE_BALL_ANIM = 'animation';
/** Spine「冰球」附件卸下时刻兜底(爆炸) */
const ICE_BALL_EXPLODE_FALLBACK = 1.1667;
/** 整段动画时长兜底 */
const ICE_BALL_DURATION_FALLBACK = 2;
/** 影子伤害类型 */
const ICE_BALL_DMG = 'ice_ball';
/**
* 是否冰球术 Skill.id。
* @param skillId Skill.id
*/
export function isIceBallSkillId(skillId: number): boolean {
return Math.floor(Number(skillId) || 0) === ICE_BALL_SKILL_ID;
}
/** 一次陨落冰球 */
interface IceBallRun {
/** 施法者 */
caster: EntityId;
/** 影子伤害上下文 */
ctx: SkillPresentationContext;
/** 圆心逻辑 X */
x: number;
/** 圆心逻辑 Y */
y: number;
/** 半径像素 */
radius: number;
/** 伤害倍率 */
scale: number;
/** 距爆炸剩余秒 */
explodeWait: number;
/** 爆炸后距回池剩余秒 */
endWait: number;
/** 是否已结算爆炸伤害 */
exploded: boolean;
/** 冰球节点 */
node: Node | null;
}
let _host: IceBallHost | null = null;
/** 冰球术表现宿主 */
export function getIceBallHost(): IceBallHost {
if (!_host) _host = new IceBallHost();
return _host;
}
/**
* Skill 305201:释放施法锁后在主目标落点生成「冰球」,动画近结束爆炸并圈伤。
*/
export class IceBallAtkPresenter implements ISkillPresenter {
present(ctx: SkillPresentationContext, host: Skill): void {
if (!ctx.isAlive) {
ctx.finish();
return;
}
const seq = host.beginPresentationCast(ctx);
if (!(seq > 0)) return;
host.disarmAnimComplete();
getIceBallHost().start(ctx);
ctx.releasePresentationLock();
host.finishPresentationCast(seq, false);
}
}
/** 冰球陨落:独立计时爆炸圈伤 */
export class IceBallHost {
private readonly _runs: IceBallRun[] = [];
private _activePulse: IceBallRun | null = null;
/**
* 开战:锁定落点并生成冰球。
* @param ctx 表现上下文
*/
start(ctx: SkillPresentationContext): void {
const parts = String(ctx.params.atkPara ?? '').split(',').map(Number);
const radius = parts[0];
const wan = parts[1];
if (!(radius > 0) || !Number.isFinite(wan) || !(wan > 0)) {
console.log('[IceBall] atk_para 无效', ctx.params.skillId, ctx.params.atkPara);
return;
}
const session = SkillPresentationEnv.getBattleSession();
if (!session) {
console.log('[IceBall] 无战斗会话', ctx.params.skillId);
return;
}
const primary = ctx.targets[0] ?? 0;
const lockId = primary > 0 ? primary : ctx.caster;
const center = logicPosOf(session, lockId) ?? logicPosOf(session, ctx.caster);
if (!center) {
console.log('[IceBall] 无落点', ctx.params.skillId);
return;
}
const placed = this._placeBall(lockId, center);
if (!placed) return;
this._runs.push({
caster: ctx.caster,
ctx: buildShadowDealContext(ctx, String(ctx.params.buff1 ?? ''), String(ctx.params.buff2 ?? '')),
x: center.x,
y: center.y,
radius,
scale: wan / 10000,
explodeWait: placed.explodeAt,
endWait: Math.max(0.05, placed.duration - placed.explodeAt),
exploded: false,
node: placed.node,
});
}
/**
* 推进爆炸与回池。
* @param dt 逻辑秒
*/
tick(dt: number): void {
if (!(dt > 0) || this._runs.length <= 0) return;
const session = SkillPresentationEnv.getBattleSession();
for (let i = this._runs.length - 1; i >= 0; i--) {
const run = this._runs[i];
if (!session || !session.world.isAlive(run.caster) || !run.ctx.isAlive) {
this._drop(run);
this._runs.splice(i, 1);
continue;
}
if (!run.exploded) {
run.explodeWait -= dt;
if (run.explodeWait <= 0) {
this._explode(session, run);
run.exploded = true;
}
continue;
}
run.endWait -= dt;
if (run.endWait <= 0) {
this._drop(run);
this._runs.splice(i, 1);
}
}
}
/** 战斗结束清空 */
abortAll(): void {
for (let i = 0; i < this._runs.length; i++) this._drop(this._runs[i]);
this._runs.length = 0;
}
private _explode(session: NonNullable<ReturnType<typeof SkillPresentationEnv.getBattleSession>>, run: IceBallRun): void {
this._activePulse = run;
pulseHostileCircle(session, run.caster, run.x, run.y, run.radius, this._onPulseHit);
this._activePulse = null;
}
private readonly _onPulseHit = (id: EntityId): void => {
const run = this._activePulse;
if (!run) return;
run.ctx.dealDamage(id, { scale: run.scale, damageType: ICE_BALL_DMG });
};
/**
* 在目标落点生成冰球并播默认动画。
* @param lockId 锁定目标
* @param center 逻辑圆心
*/
private _placeBall(lockId: EntityId, center: { x: number; y: number }): { node: Node; explodeAt: number; duration: number } | null {
const flyLayer = SkillPresentationEnv.flyLayer;
if (!flyLayer?.isValid) {
console.log('[IceBall] 无飞行层');
return null;
}
let node: Node | null = null;
try {
node = gg.res.getNode(GPath.HeroSkillPrefab(), ICE_BALL_PREFAB, GBundle.BattleHero);
} catch (e) {
console.log('[IceBall] 预制体加载异常', String(e));
return null;
}
if (!node?.isValid) {
console.log('[IceBall] 预制体未加载', ICE_BALL_PREFAB);
return null;
}
const ut = flyLayer.getComponent(UITransform);
if (!ut) {
gg.res.putNode(node);
return null;
}
const world = this._resolvePlaceWorld(lockId, center);
if (!world) {
console.log('[IceBall] 无放置世界坐标', lockId);
gg.res.putNode(node);
return null;
}
flyLayer.active = true;
node.parent = flyLayer;
node.active = true;
node.layer = flyLayer.layer;
node.setScale(1, 1, 1);
node.setPosition(ut.convertToNodeSpaceAR(world));
node.setSiblingIndex(flyLayer.children.length - 1);
const sk = node.getComponentInChildren(sp.Skeleton);
if (!sk?.isValid) {
console.log('[IceBall] 缺 Spine', ICE_BALL_PREFAB);
gg.res.putNode(node);
return null;
}
const typed = sk as sp.Skeleton & { defaultAnimation?: string };
const anim = String(typed.defaultAnimation || ICE_BALL_ANIM).trim() || ICE_BALL_ANIM;
if (!BattleSpinePlayback.play(sk, anim, { loop: false })) {
console.log('[IceBall] 动画起播失败', anim);
gg.res.putNode(node);
return null;
}
const durationRaw = BattleSpinePlayback.getAnimDuration(sk, anim);
const duration = durationRaw != null && durationRaw > 0 ? durationRaw : ICE_BALL_DURATION_FALLBACK;
const explodeRaw = BattleSpinePlayback.findFirstAttachmentClearTime(sk, anim, '冰球');
let explodeAt = explodeRaw != null && explodeRaw > 0 ? explodeRaw : ICE_BALL_EXPLODE_FALLBACK;
if (explodeAt >= duration) explodeAt = Math.max(0.05, duration * 0.85);
return { node, explodeAt, duration };
}
/**
* 放置世界坐标:优先怪节点,否则逻辑坐标映射。
* @param lockId 锁定目标
* @param center 逻辑圆心
*/
private _resolvePlaceWorld(lockId: EntityId, center: { x: number; y: number }): Vec3 | null {
const fromMonster = SkillPresentationEnv.getTargetWorldPos(lockId);
if (fromMonster) return fromMonster;
const session = SkillPresentationEnv.getBattleSession();
const map = SkillPresentationEnv.getMap();
if (!session || !map) return null;
const tf = session.world.getComponent(lockId, TransformComp);
if (!tf) return null;
return logicalWorldToCocos(session, map, tf.row, tf.col, center.x, center.y);
}
private _drop(run: IceBallRun): void {
const node = run.node;
run.node = null;
if (!node?.isValid) return;
const skins = node.getComponentsInChildren(sp.Skeleton);
for (let i = 0; i < skins.length; i++) {
BattleSpinePlayback.clearPendingComplete(skins[i]);
BattleSpinePlayback.resetTracks(skins[i]);
}
if (node['resKey']) gg.res.putNode(node);
else if (isValid(node)) node.destroy();
const flyLayer = SkillPresentationEnv.flyLayer;
if (flyLayer?.isValid && flyLayer.children.length === 0) flyLayer.active = false;
}
}

@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "3380c6f6-3d3d-4da3-bfcf-e7273d59bee3",
"files": [],
"subMetas": {},
"userData": {}
}

@ -10,6 +10,7 @@ import { RingProjectilesPresenter } from './RingProjectilesPresenter';
import { SpearTossPresenter } from './SpearTossPresenter';
import { PagodaSmashPresenter } from './PagodaSmashPresenter';
import { FlySwordAtkPresenter } from './FlySwordAtkPresenter';
import { IceBallAtkPresenter, ICE_BALL_SKILL_ID } from './IceBallAtkPresenter';
import { FLY_SWORD_SKILL_IDS } from '../config/weapon1/Weapon1BattleIds';
import { Gong } from '../heros/Gong';
import { Gun } from '../heros/Gun';
@ -131,6 +132,7 @@ export function ensureSpecialSkillPresenters(): void {
for (let i = 0; i < FLY_SWORD_SKILL_IDS.length; i++) {
router.registerSkillId(FLY_SWORD_SKILL_IDS[i], flySword);
}
router.registerSkillId(ICE_BALL_SKILL_ID, new IceBallAtkPresenter());
// 覆盖内置 atk_type=16:棍宿主播攻效,其余走通用多段
router.registerAtkType(16, new FanMultiHitAtkPresenter());
// 覆盖内置 atk_type 12/17:弓宿主散射走 Gong 自身投掷物,其余通用

@ -41,6 +41,7 @@ import { getArrowRainHost } from '../presentation/ArrowRainPresenter';
import { getExtraRingHost } from '../presentation/RingProjectilesPresenter';
import { getExtraStrikeHost } from '../presentation/ExtraStrikePresenter';
import { getGroundCircleHost, getThrownCircleHost } from '../presentation/ThrownCircleAtkPresenter';
import { getIceBallHost } from '../presentation/IceBallAtkPresenter';
import { getFlySwordHost } from '../presentation/FlySwordAtkPresenter';
import { getSpearTossHost } from '../presentation/SpearTossPresenter';
import { getPagodaSmashHost } from '../presentation/PagodaSmashPresenter';
@ -992,6 +993,7 @@ export class BattleRuntime implements IBattleRuntimeHandle {
getThrownCircleHost().tick(scaledDt);
getGroundCircleHost().tick(scaledDt);
getFlySwordHost().tick(scaledDt);
getIceBallHost().tick(scaledDt);
return;
default:
return;
@ -1236,6 +1238,7 @@ export class BattleRuntime implements IBattleRuntimeHandle {
getThrownCircleHost().abortAll();
getGroundCircleHost().abortAll();
getFlySwordHost().abortAll();
getIceBallHost().abortAll();
getSpearTossHost().abortAll();
getPagodaSmashHost().abortAll();
getStaffSmashHost().abortAll();

@ -24,12 +24,15 @@ import {
SWORD_ARRAY_EVENT,
SWORD_WAVE_EVENT,
LIGHTNING_CHAIN_EVENT,
POISON_APPLIED_EVENT,
POISON_REMOVED_EVENT,
type HomingDragonCue,
type IceConeSplitCue,
type LineDragonCue,
type SwordArrayCue,
type SwordWaveCue,
type LightningChainCue,
type PoisonCue,
} from '../combat/PassiveSkillRuntime';
import { logicalWorldToCocos } from '../bridge/CocosBattleMovementBridge';
import { BattleMapSpec } from '../map/BattleMapSpec';
@ -58,6 +61,8 @@ type MonsterView = IBattleMonsterView & {
clearGhostBiteFx?(): void;
showElecFx?(durationSec: number): void;
clearElecFx?(): void;
showPoisonFx?(durationSec: number): void;
clearPoisonFx?(): void;
};
/** 阎王主动「生死簿」Skill.id 区间 */
@ -229,6 +234,7 @@ export class CocosBattleCombatPresenter {
this._onBleedRemoved(event.victim);
this._onGhostBiteRemoved(event.victim);
this._onElecRemoved(event.victim);
this._onPoisonRemoved(event.victim);
break;
case BattleEventType.UnitDespawned:
this._onStunRemoved(event.entity);
@ -238,6 +244,7 @@ export class CocosBattleCombatPresenter {
this._onBleedRemoved(event.entity);
this._onGhostBiteRemoved(event.entity);
this._onElecRemoved(event.entity);
this._onPoisonRemoved(event.entity);
break;
case BattleEventType.Custom:
if (event.name === KNOCKBACK_APPLIED_EVENT) {
@ -257,6 +264,11 @@ export class CocosBattleCombatPresenter {
this._playLightningChain(event.payload as LightningChainCue | undefined);
} else if (event.name === ICE_CONE_SPLIT_EVENT) {
this._playIceConeSplit(event.payload as IceConeSplitCue | undefined);
} else if (event.name === POISON_APPLIED_EVENT) {
this._onPoisonApplied(event.payload as PoisonCue | undefined);
} else if (event.name === POISON_REMOVED_EVENT) {
const payload = event.payload as { target?: EntityId } | undefined;
if (payload?.target != null) this._onPoisonRemoved(payload.target);
}
break;
}
@ -340,6 +352,19 @@ export class CocosBattleCombatPresenter {
this._getMonsterView(target)?.clearElecFx?.();
}
/** 中毒:挂「怪_中毒」并飘「中毒」 */
private _onPoisonApplied(cue: PoisonCue | undefined): void {
if (!cue || !(cue.target > 0) || !(cue.duration > 0)) return;
const monster = this._getMonsterView(cue.target);
if (!monster) return;
monster.showPoisonFx?.(Math.max(0.05, cue.duration));
this._playStatusText(cue.target, '中毒');
}
private _onPoisonRemoved(target: EntityId): void {
this._getMonsterView(target)?.clearPoisonFx?.();
}
/** 怪物头顶状态飘字 */
private _playStatusText(target: EntityId, text: string): void {
getMonsterStatusText().play(target, text);

@ -123,6 +123,8 @@ export class GameController extends Component {
/** GM:背包刷新仅从该 Weapon2 列表中随机(null=正常权重池) */
gmBagWeapon2Ids: number[] | null = null;
/** GM:本机开局灌入 CarryStore 的 Battlebuff.id(null=不灌;开战 clear 后再写入) */
gmCarryBuffIds: number[] | null = null;
/** GM:AI 主动 Battlebuff.id(null=每局匹配随机;有值则跨局常驻覆盖直至清除) */
gmAiActiveBuffIds: number[] | null = null;
/** GM:AI 被动 Battlebuff.id(null=每局匹配随机;有值则跨局常驻覆盖直至清除) */
@ -409,6 +411,7 @@ export class GameController extends Component {
this.isShowObstacle = true
this.monsterNumScale = 1;
this.gmBagWeapon2Ids = null;
this.gmCarryBuffIds = null;
this.gmAiActiveBuffIds = null;
this.gmAiPassiveBuffIds = null;
this.gmMapId = 0;

@ -6,12 +6,10 @@ import { TableNames } from '../game/ConfigTableData';
import { UIID } from '../game/ConfigRes';
import { sdkConfig } from '../sdk/sdkConfig';
import { DailyShopDataManager } from '../manager/DailyShopDataManager';
import { DailyShopKind } from '../manager/DailyShopTypes';
import { EmolumentDataManager } from '../manager/EmolumentDataManager';
import { HeadIconManager } from '../manager/HeadIconManager';
import { RankBoardManager } from '../manager/RankBoardManager';
import { RankDataManager } from '../manager/RankDataManager';
import { BattleBuffCarryStore } from '../manager/battleBuff/BattleBuffCarryStore';
/** 俸禄 GM 测试快照星级 */
const GM_EMOLUMENT_SNAP_STARS = 50;
@ -191,10 +189,11 @@ export class ProjectGmActions {
gg.ui.showToast('已重置每日商店/抽奖(保留携带道具)');
}
/** 清空携带 BUFF */
/** 清空 GM 词条覆盖 */
static clearCarriedBuffs(): void {
gg.game.gmCarryBuffIds = null;
DailyShopDataManager.gmClearCarriedBuffs();
gg.ui.showToast('已清空携带的每日商店BUFF');
gg.ui.showToast('已清空 GM 词条');
}
/** 清除 AI 主动 BUFF */
@ -220,24 +219,61 @@ export class ProjectGmActions {
/** AI BUFF 摘要 */
static aiBuffSummary(active: boolean): string {
const cur = active ? gg.game.gmAiActiveBuffIds : gg.game.gmAiPassiveBuffIds;
return cur?.length ? cur.join(',') : '每局随机';
if (!cur?.length) return '每局随机';
const parts: string[] = [];
for (let i = 0; i < cur.length; i++) {
const id = cur[i];
const row = gg.data.table.getTableData<ITableBattlebuff>(TableNames.Battlebuff, id);
parts.push(row?.name ? `${id}:${row.name}` : `${id}`);
}
return parts.join(', ');
}
/** 写入被动携带第一槽 */
/** 写入被动携带(追加单个,兼容旧调用) */
static setPassiveCarry(buffId: number): void {
const result = DailyShopDataManager.gmSetFirstCarrySlot(DailyShopKind.Passive, String(buffId));
if (!result.ok) {
gg.ui.showToast(result.message ?? '写入失败');
return;
this.setCarryBuffIds([buffId]);
}
/**
* 覆盖 GM 词条(跨局常驻,开战灌入 CarryStore)。
* @param ids Battlebuff.id 列表
* @param quiet 为 true 时不弹 toast(列表即时写入用)
*/
static setCarryBuffIds(ids: number[], quiet = false): void {
const cleaned: number[] = [];
for (let i = 0; i < ids.length; i++) {
const id = Math.floor(Number(ids[i]) || 0);
if (id <= 0) continue;
const row = gg.data.table.getTableData<ITableBattlebuff>(TableNames.Battlebuff, id);
if (!row) {
console.warn('[GM] 词条 id 不存在', id);
continue;
}
cleaned.push(id);
}
gg.game.gmCarryBuffIds = cleaned.length > 0 ? cleaned : null;
if (!quiet) {
gg.ui.showToast(cleaned.length > 0 ? `已设置 ${cleaned.length} 个词条(开战生效)` : '已清空词条');
}
gg.data.saveToServer();
gg.ui.showToast(`被动 buff 已写入 ${buffId}`);
}
/** 当前 GM 词条 id 列表 */
static getCarryBuffIds(): number[] {
const cur = gg.game.gmCarryBuffIds;
return cur?.length ? cur.slice() : [];
}
/** 被动携带摘要 */
static passiveCarrySummary(): string {
const first = BattleBuffCarryStore.getPassives(0)[0];
return first ? `${first.id}(${first.name})` : '空';
const ids = this.getCarryBuffIds();
if (!ids.length) return '空';
const parts: string[] = [];
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const row = gg.data.table.getTableData<ITableBattlebuff>(TableNames.Battlebuff, id);
parts.push(row?.name ? `${id}:${row.name}` : `${id}`);
}
return parts.join(', ');
}
/** 排行星级&胜场测试 */

@ -59,11 +59,9 @@ export class ProjectGmBootstrap {
.sw('敌方营地不掉血', () => A.getEnemyCampImmune(), (v) => A.setCampImmune(false, v))
.page('固定地图', P.levelPicker, () => A.fixedMapSummary())
.num('玩家局内血量', () => A.getWallHp() || 1000, (v) => A.setWallHp(v), 1)
.page('设置被动BUFF', P.buffPicker, () => A.passiveCarrySummary(), { expectType: 2, mode: 'carryPassive' })
.page('设置AI主动BUFF', P.buffPicker, () => A.aiBuffSummary(true), { expectType: 1, mode: 'aiActive' })
.page('设置AI被动BUFF', P.buffPicker, () => A.aiBuffSummary(false), { expectType: 2, mode: 'aiPassive' })
.btn('清除AI主动BUFF', () => A.clearAiActiveBuff())
.btn('清除AI被动BUFF', () => A.clearAiPassiveBuff())
.tab('词条')
.embed(P.buffPicker, { expectType: 0, mode: 'carryPassive' })
.tab('存档')
.embed(P.saveEditor)

@ -1,5 +1,5 @@
import {
Button, Color, Label, Layout, Mask, Node, ScrollView, Sprite, UITransform, Widget,
Button, Color, Graphics, Label, Layout, Mask, Node, ScrollView, Sprite, UITransform, Widget,
HorizontalTextAlignment, VerticalTextAlignment, Layers, Overflow,
} from 'cc';
import { SpriteLoad } from '../../mx/components/SpriteLoad';
@ -7,6 +7,8 @@ import type { IGmContentPanel, IGmPanelContext } from '../../mx/module/gm';
import { GmRegistry } from '../../mx/module/gm';
import { GBundle, GPath } from '../game/ConfigRes';
import { TableNames } from '../game/ConfigTableData';
import { battleBuffSlotType } from '../manager/BattleBuffParse';
import { BattleBuffSlotType } from '../manager/battleBuff/BattleBuffTypes';
import { RankDataManager } from '../manager/RankDataManager';
import { ProjectGmActions } from './ProjectGmActions';
import { GmSaveEditorPanel } from './ProjectGmSavePanel';
@ -22,14 +24,47 @@ export const ProjectGmPanelId = {
/**
* buffPicker 面板数据。
* 新表已无 type 列时 expectType 请传 0,否则列表会被滤空。
*/
export interface IGmBuffPickerData {
/** 1=主动 2=被动;0=不限 */
/** BattleBuffSlotType:1主动 2被动 3自动;0=不限(推荐) */
expectType: number;
/** 写入目标 */
mode: 'carryPassive' | 'aiActive' | 'aiPassive';
}
const BUFF_C = {
text: new Color(255, 255, 255, 255),
dim: new Color(180, 190, 210, 255),
accent: new Color(255, 220, 70, 255),
rowBg: new Color(28, 36, 56, 255),
rowLine: new Color(100, 160, 240, 160),
rowOn: new Color(255, 220, 70, 255),
btn: new Color(45, 105, 190, 255),
btnClear: new Color(210, 120, 35, 255),
btnBack: new Color(70, 78, 96, 255),
};
const QUALITY_COLOR: { [q: number]: Color } = {
1: new Color(90, 210, 120, 255),
2: new Color(80, 160, 255, 255),
3: new Color(190, 110, 255, 255),
4: new Color(255, 170, 60, 255),
};
const QUALITY_NAME: { [q: number]: string } = {
1: '绿',
2: '蓝',
3: '紫',
4: '橙',
};
const SLOT_NAME: { [t: number]: string } = {
[BattleBuffSlotType.Active]: '主动',
[BattleBuffSlotType.Passive]: '被动',
[BattleBuffSlotType.Auto]: '自动',
};
/**
* 创建文字按钮。
*/
@ -220,73 +255,315 @@ class GmItemPickerPanel implements IGmContentPanel {
}
/**
* BUFF 列表点选。
* 词条页:上=未选、下=已选;点击即对调并立刻写入。
*/
class GmBuffPickerPanel implements IGmContentPanel {
private _root: Node | null = null;
private _ctx: IGmPanelContext | null = null;
private _data: IGmBuffPickerData | null = null;
private _listW = 640;
private _allRows: ITableBattlebuff[] = [];
private _selected: number[] = [];
private _availContent: Node | null = null;
private _selContent: Node | null = null;
private _lbAvail: Label | null = null;
private _lbSel: Label | null = null;
open(parent: Node, ctx: IGmPanelContext): void {
this.close();
const data = ctx.command.panelData as IGmBuffPickerData;
if (!data?.mode) {
console.warn('[GM] buffPicker 缺少 panelData');
ctx.host.toast('面板配置错误');
ctx.goBack();
return;
}
this._ctx = ctx;
const raw = ctx.command.panelData as IGmBuffPickerData | undefined;
const data: IGmBuffPickerData = {
expectType: raw?.expectType ?? 0,
mode: raw?.mode ?? 'carryPassive',
};
this._data = data;
this._selected = this._loadCurrentIds();
const pUt = parent.getComponent(UITransform);
const W = pUt?.width || 660;
const H = pUt?.height || 900;
this._listW = W - 16;
this._root = new Node('buffPicker');
this._root.layer = Layers.Enum.UI_2D;
this._root.addComponent(UITransform).setContentSize(660, 900);
this._root.addComponent(UITransform).setContentSize(W, H);
const rw = this._root.addComponent(Widget);
rw.isAlignLeft = rw.isAlignRight = rw.isAlignTop = rw.isAlignBottom = true;
rw.left = rw.right = rw.top = rw.bottom = 0;
parent.addChild(this._root);
const title = new Node('t');
title.layer = Layers.Enum.UI_2D;
title.addComponent(UITransform).setContentSize(600, 36);
const tl = title.addComponent(Label);
tl.string = ctx.command.title;
tl.fontSize = 26;
tl.color = Color.WHITE;
tl.horizontalAlign = HorizontalTextAlignment.CENTER;
title.setPosition(0, 420);
this._root.addChild(title);
const headH = 36;
const gap = 10;
const clearH = 44;
const usable = Math.max(200, H - headH * 2 - gap - clearH - 8);
const availH = Math.floor(usable * 0.58);
const selH = usable - availH;
this._lbAvail = this._addHead(this._root, '未选择(点选加入)', this._listW, headH);
this._lbAvail.node.setPosition(0, H / 2 - headH / 2 - 2);
const { host: availHost, content: availContent } = makeVScroll(this._root, this._listW, availH);
this._availContent = availContent;
const availLay = availContent.getComponent(Layout);
if (availLay) {
availLay.spacingY = 8;
availLay.paddingBottom = 12;
}
availHost.setPosition(0, H / 2 - headH - availH / 2 - 4);
const selTopY = -H / 2 + clearH + selH + headH + 4;
this._lbSel = this._addHead(this._root, '已选择', this._listW, headH);
this._lbSel.node.setPosition(0, selTopY - headH / 2);
const { host: selHost, content: selContent } = makeVScroll(this._root, this._listW, selH);
this._selContent = selContent;
const selLay = selContent.getComponent(Layout);
if (selLay) {
selLay.spacingY = 8;
selLay.paddingBottom = 12;
}
selHost.setPosition(0, -H / 2 + clearH + selH / 2);
const { host, content } = makeVScroll(this._root, 640, 700);
host.setPosition(0, 20);
this._pillBtn(this._root, '清空已选', 200, 40, BUFF_C.btnClear, () => this._clearSelected())
.setPosition(0, -H / 2 + clearH / 2);
this._allRows = [];
const list = gg.data.table.getTableList<ITableBattlebuff>(TableNames.Battlebuff) ?? [];
list.sort((a, b) => Number(a.id) - Number(b.id));
for (let i = 0; i < list.length; i++) {
const row = list[i];
const t = Math.floor(Number((row as { type?: number }).type) || 0);
if (data.expectType > 0 && t !== data.expectType) continue;
const id = Number(row.id);
const label = `${id} ${row.name ?? ''} (t${t})`;
makeLabelBtn(content, label, 620, 44, () => {
if (data.mode === 'carryPassive') ProjectGmActions.setPassiveCarry(id);
else if (data.mode === 'aiActive') ProjectGmActions.setAiBuffIds([id], true);
else ProjectGmActions.setAiBuffIds([id], false);
ctx.refreshList();
ctx.goBack();
});
const id = Math.floor(Number(row.id) || 0);
if (id <= 0) continue;
const slot = battleBuffSlotType(row);
if (data.expectType > 0 && slot !== data.expectType) continue;
this._allRows.push(row);
}
makeLabelBtn(content, '清除覆盖/空', 620, 44, () => {
if (data.mode === 'carryPassive') {
ctx.host.toast('被动携带请选具体 id');
return;
}
ProjectGmActions.setAiBuffIds(null, data.mode === 'aiActive');
ctx.refreshList();
ctx.goBack();
});
content.getComponent(Layout)?.updateLayout();
makeLabelBtn(this._root, '返回', 140, 44, () => ctx.goBack()).setPosition(0, -400);
if (this._allRows.length <= 0) {
console.warn('[GM] buffPicker 列表为空', data);
}
this._rebuildLists();
}
close(): void {
if (this._root?.isValid) this._root.destroy();
this._root = null;
this._ctx = null;
this._data = null;
this._allRows = [];
this._selected = [];
this._availContent = null;
this._selContent = null;
this._lbAvail = null;
this._lbSel = null;
}
/** 读取当前已生效 id */
private _loadCurrentIds(): number[] {
if (!this._data) return [];
if (this._data.mode === 'carryPassive') return ProjectGmActions.getCarryBuffIds();
const cur = this._data.mode === 'aiActive'
? gg.game.gmAiActiveBuffIds
: gg.game.gmAiPassiveBuffIds;
return cur?.length ? cur.slice() : [];
}
private _hasId(id: number): boolean {
for (let i = 0; i < this._selected.length; i++) {
if (this._selected[i] === id) return true;
}
return false;
}
/** 点击未选 → 加入;点击已选 → 移除;立刻写入 */
private _move(id: number, toSelected: boolean): void {
if (toSelected) {
if (!this._hasId(id)) this._selected.push(id);
} else {
const next: number[] = [];
for (let i = 0; i < this._selected.length; i++) {
if (this._selected[i] !== id) next.push(this._selected[i]);
}
this._selected = next;
}
this._applyNow();
this._rebuildLists();
}
private _clearSelected(): void {
this._selected = [];
this._applyNow();
this._rebuildLists();
}
private _applyNow(): void {
if (!this._data) return;
const ids = this._selected.slice();
if (this._data.mode === 'carryPassive') {
ProjectGmActions.setCarryBuffIds(ids, true);
} else if (this._data.mode === 'aiActive') {
ProjectGmActions.setAiBuffIds(ids.length ? ids : null, true);
} else {
ProjectGmActions.setAiBuffIds(ids.length ? ids : null, false);
}
}
private _rebuildLists(): void {
if (!this._availContent?.isValid || !this._selContent?.isValid) return;
this._availContent.removeAllChildren();
this._selContent.removeAllChildren();
let availN = 0;
let selN = 0;
for (let i = 0; i < this._allRows.length; i++) {
const row = this._allRows[i];
const id = Math.floor(Number(row.id) || 0);
if (this._hasId(id)) {
this._addBuffCard(this._selContent, this._listW, row, false);
selN++;
} else {
this._addBuffCard(this._availContent, this._listW, row, true);
availN++;
}
}
if (availN <= 0) this._addEmptyHint(this._availContent, '已全部选中');
if (selN <= 0) this._addEmptyHint(this._selContent, '点击上方加入');
if (this._lbAvail) this._lbAvail.string = `未选择 ${availN}(点选加入)`;
if (this._lbSel) this._lbSel.string = `已选择 ${selN}(点选移除)`;
this._availContent.getComponent(Layout)?.updateLayout();
this._selContent.getComponent(Layout)?.updateLayout();
}
private _addEmptyHint(parent: Node, text: string): void {
const n = new Node('empty');
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(this._listW, 48);
const lb = n.addComponent(Label);
lb.string = text;
lb.fontSize = 22;
lb.color = BUFF_C.dim;
lb.horizontalAlign = HorizontalTextAlignment.CENTER;
parent.addChild(n);
}
private _addHead(parent: Node, text: string, w: number, h: number): Label {
const n = new Node('head');
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(w, h);
const lb = n.addComponent(Label);
lb.string = text;
lb.fontSize = 24;
lb.isBold = true;
lb.color = BUFF_C.accent;
lb.horizontalAlign = HorizontalTextAlignment.LEFT;
lb.verticalAlign = VerticalTextAlignment.CENTER;
lb.overflow = Overflow.CLAMP;
parent.addChild(n);
return lb;
}
/**
* @param toSelected true=点后加入已选;false=点后移回未选
*/
private _addBuffCard(parent: Node, w: number, row: ITableBattlebuff, toSelected: boolean): void {
const h = 84;
const id = Math.floor(Number(row.id) || 0);
const name = String(row.name ?? '');
const desc = String(row.desc ?? '');
const q = Math.max(0, Math.floor(Number(row.quality) || 0));
const slot = battleBuffSlotType(row);
const qColor = QUALITY_COLOR[q] || BUFF_C.dim;
const qName = QUALITY_NAME[q] || `Q${q}`;
const slotName = slot != null ? (SLOT_NAME[slot] || `t${slot}`) : '?';
const card = new Node(`buff${id}`);
card.layer = Layers.Enum.UI_2D;
card.addComponent(UITransform).setContentSize(w, h);
this._drawRound(card, BUFF_C.rowBg, w, h, 12, toSelected ? BUFF_C.rowLine : BUFF_C.rowOn);
const iconSz = 56;
const iconN = new Node('icon');
iconN.layer = Layers.Enum.UI_2D;
iconN.addComponent(UITransform).setContentSize(iconSz, iconSz);
const sp = iconN.addComponent(Sprite);
sp.sizeMode = Sprite.SizeMode.CUSTOM;
const loader = iconN.addComponent(SpriteLoad);
const iconName = String(row.icon || row.name || '').trim();
if (iconName) loader.setSprite(GPath.ShopSkill(iconName), GBundle.Items);
iconN.setPosition(-w / 2 + iconSz / 2 + 10, 0);
card.addChild(iconN);
const textW = w - iconSz - 28;
const head = new Node('head');
head.layer = Layers.Enum.UI_2D;
head.addComponent(UITransform).setContentSize(textW, 30);
const hl = head.addComponent(Label);
hl.string = `${id} ${name} · ${qName}/${slotName}`;
hl.fontSize = 22;
hl.isBold = true;
hl.color = qColor;
hl.horizontalAlign = HorizontalTextAlignment.LEFT;
hl.verticalAlign = VerticalTextAlignment.CENTER;
hl.overflow = Overflow.CLAMP;
hl.enableWrapText = false;
head.setPosition(iconSz / 2 + 4, 16);
card.addChild(head);
const body = new Node('desc');
body.layer = Layers.Enum.UI_2D;
body.addComponent(UITransform).setContentSize(textW, 34);
const bl = body.addComponent(Label);
bl.string = desc || '(无描述)';
bl.fontSize = 17;
bl.color = BUFF_C.dim;
bl.horizontalAlign = HorizontalTextAlignment.LEFT;
bl.verticalAlign = VerticalTextAlignment.TOP;
bl.overflow = Overflow.CLAMP;
bl.enableWrapText = true;
body.setPosition(iconSz / 2 + 4, -16);
card.addChild(body);
card.addComponent(Button);
card.on(Button.EventType.CLICK, () => this._move(id, toSelected), this);
parent.addChild(card);
}
private _pillBtn(parent: Node, text: string, w: number, h: number, bg: Color, onClick: () => void): Node {
const n = new Node(text);
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(w, h);
this._drawRound(n, bg, w, h, 10);
const lbN = new Node('lb');
lbN.layer = Layers.Enum.UI_2D;
lbN.addComponent(UITransform).setContentSize(w - 6, h - 4);
const lb = lbN.addComponent(Label);
lb.string = text;
lb.fontSize = 22;
lb.isBold = true;
lb.color = BUFF_C.text;
lb.horizontalAlign = HorizontalTextAlignment.CENTER;
lb.verticalAlign = VerticalTextAlignment.CENTER;
n.addChild(lbN);
n.addComponent(Button);
n.on(Button.EventType.CLICK, onClick, this);
parent.addChild(n);
return n;
}
private _drawRound(node: Node, fill: Color, w: number, h: number, r: number, stroke?: Color): void {
const g = node.getComponent(Graphics) || node.addComponent(Graphics);
g.clear();
g.fillColor = fill;
g.roundRect(-w / 2, -h / 2, w, h, r);
g.fill();
if (stroke) {
g.strokeColor = stroke;
g.lineWidth = stroke === BUFF_C.rowOn ? 3 : 2;
g.roundRect(-w / 2, -h / 2, w, h, r);
g.stroke();
}
}
}

@ -2,6 +2,7 @@
* 局外每日商店已下线:保留空壳 API,避免旧 UI/GM 引用编译失败。
* 局内 buff 请用 BattleBuffCarryStore / BattleBuffSelectService。
*/
import { BattleBuffCarryStore } from './battleBuff/BattleBuffCarryStore';
import type { DailyShopParsed } from './DailyShopTypes';
import {
DAILY_SHOP_ACTIVE_SLOTS,
@ -133,22 +134,40 @@ export class DailyShopDataManager {
}
static gmClearCarriedBuffs(): void {
// 延迟引用,避免与 CarryStore 启动顺序纠缠
const { BattleBuffCarryStore } = require('./battleBuff/BattleBuffCarryStore') as typeof import('./battleBuff/BattleBuffCarryStore');
BattleBuffCarryStore.clear();
}
/**
* GM:向本机 CarryStore 追加 buff。
* GM:向本机 CarryStore 追加单个 buff。
* @param _kind 忽略(一律 append)
* @param rawId Battlebuff.id
*/
static gmSetFirstCarrySlot(_kind: DailyShopKind, rawId: string): { ok: boolean; message?: string } {
const id = Math.floor(Number(rawId) || 0);
if (id <= 0) return { ok: false, message: '非法 id' };
const { BattleBuffCarryStore } = require('./battleBuff/BattleBuffCarryStore') as typeof import('./battleBuff/BattleBuffCarryStore');
const parsed = BattleBuffCarryStore.appendBuffId(0, id);
if (!parsed) return { ok: false, message: '写入失败' };
return { ok: true };
}
/**
* GM:覆盖本机携带为指定 id 列表(可多个)。
* @param ids Battlebuff.id 列表
*/
static gmSetCarryBuffIds(ids: readonly number[]): { ok: boolean; message?: string; count: number } {
BattleBuffCarryStore.setFromBuffIds(0, [], []);
let count = 0;
for (let i = 0; i < ids.length; i++) {
const id = Math.floor(Number(ids[i]) || 0);
if (id <= 0) continue;
const parsed = BattleBuffCarryStore.appendBuffId(0, id);
if (!parsed) {
console.warn('[DailyShop] GM 写入 buff 失败', id);
continue;
}
count++;
}
if (ids.length > 0 && count <= 0) return { ok: false, message: '全部写入失败', count: 0 };
return { ok: true, count };
}
}

@ -100,6 +100,9 @@ export class Monster extends Component {
/** 感电特效节点(怪_电) */
private _elecFx: Node | null = null;
private _elecFxSeq = 0;
/** 中毒特效节点(怪_中毒) */
private _poisonFx: Node | null = null;
private _poisonFxSeq = 0;
/** 加速特效节点(怪_加速) */
private _accelerateFx: Node | null = null;
/** 加速特效朝向用:上一帧世界坐标 */
@ -1265,6 +1268,62 @@ export class Monster extends Component {
this.clearElecFx();
};
/**
* 显示中毒特效(prefab/怪_中毒),挂在 bloodPos(头顶),按时长自动回收;刷新则重置计时。
*/
showPoisonFx(durationSec: number): void {
if (!this.node?.isValid || this.IsDead) return;
const parent = this.bloodPos?.isValid ? this.bloodPos : (this.upEff?.isValid ? this.upEff : this.node);
++this._poisonFxSeq;
if (!this._poisonFx?.isValid) {
const node = gg.res.getNode('prefab/', '怪_中毒', GBundle.BattleGameMonster);
if (!node?.isValid) {
console.error('[Monster] 中毒特效预制体未加载', '怪_中毒');
return;
}
node.parent = parent;
node.setPosition(0, this._poisonFxLocalY(parent), 0);
node.active = true;
this._poisonFx = node;
const sk = node.getComponentInChildren(sp.Skeleton);
if (sk) {
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.SHARED_CACHE);
sk.setAnimation(0, 'animation', true);
}
} else {
if (this._poisonFx.parent !== parent) this._poisonFx.parent = parent;
this._poisonFx.setPosition(0, this._poisonFxLocalY(parent), 0);
}
const dur = Math.max(0.05, durationSec);
this.unschedule(this._onPoisonFxTimeout);
this.scheduleOnce(this._onPoisonFxTimeout, dur);
}
/**
* 中毒特效本地 Y:挂 bloodPos 时为 0;否则对齐 bloodPos 头顶高度。
* @param parent 挂载父节点
*/
private _poisonFxLocalY(parent: Node): number {
if (parent === this.bloodPos) return 0;
if (this.bloodPos?.isValid) return this.bloodPos.position.y;
return 0;
}
clearPoisonFx(): void {
this._poisonFxSeq++;
this.unschedule(this._onPoisonFxTimeout);
if (this._poisonFx?.isValid) {
gg.res.putNode(this._poisonFx);
}
this._poisonFx = null;
}
private _onPoisonFxTimeout = (): void => {
this.clearPoisonFx();
};
/** 按 Weapon1.atkPos 取受击点世界坐标(1头 2腰 3脚) */
getPosByAtkPosIndex(atkPosIndex: number): Vec3 {
const ui = this.node.getComponent(UITransform);
@ -1388,6 +1447,7 @@ export class Monster extends Component {
this.clearBleedFx();
this.clearGhostBiteFx();
this.clearElecFx();
this.clearPoisonFx();
this.clearAccelerateFx();
this.clearAddHpEff();
this.clearHpCapEff();

Loading…
Cancel
Save