-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathflamethrower.lua
More file actions
256 lines (218 loc) · 8.6 KB
/
flamethrower.lua
File metadata and controls
256 lines (218 loc) · 8.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
-- flamethrower.lua
-- Intercepts FUEL_CELL projectiles on their first movement tick.
-- Instead of flying, the cell is stopped in place and a jet of fire
-- is spawned from the shooter's position along the aimed direction.
--
-- Works in fortress mode, adventure mode, and arena mode.
-- The onProjItemCheckMovement event fires for all item projectiles in all
-- modes. The only mode-specific differences are:
-- * Direction: speed_x/speed_y are 0 in adventure/arena mode; fall back
-- to (target_pos - origin_pos) when speed is zero.
-- * Stopping: zeroing speed alone doesn't stop the projectile in
-- adventure/arena mode; also collapse target_pos to cur_pos.
local eventful = require("plugins.eventful")
local repeatUtil = require("repeat-util")
local GCfg = _G.__GunConfig
if not GCfg or not GCfg._loaded then
dfhack.printerr("[flamethrower] FATAL: __GunConfig not set — bw_autoload must run first")
return
end
local CALLBACK_ID = "flamethrower"
local CALLBACK_ID_POLL = "flamethrower:poll"
local CALLBACK_ID_CLEANUP = "flamethrower:cleanup"
local CLEANUP_INTERVAL = 1200
local enabled = false
local handled = {}
---------------------------------------------------------------------------
-- CONFIG (loaded from config_gun.lua)
---------------------------------------------------------------------------
-- Fire jet length (tiles) per weapon token.
local WEAPON_RANGES = GCfg.WEAPON_RANGES
local DEFAULT_RANGE = 6
---------------------------------------------------------------------------
-- HELPERS
---------------------------------------------------------------------------
local function get_item_subtype_id(item)
local ok, id = pcall(function()
return dfhack.items.getSubtypeDef(item:getType(), item:getSubtype()).id
end)
return ok and id or nil
end
local function get_flame_range(bow_id)
if bow_id == -1 then return DEFAULT_RANGE end
local weapon = df.item.find(bow_id)
if not weapon then return DEFAULT_RANGE end
local wid = get_item_subtype_id(weapon)
return (wid and WEAPON_RANGES[wid]) or DEFAULT_RANGE
end
local function spawn_fire_jet(origin, speed_x, speed_y, range)
local horiz = math.sqrt(speed_x * speed_x + speed_y * speed_y)
if horiz < 1 then return end
local nx = speed_x / horiz
local ny = speed_y / horiz
local fc = _G.__firecontrol
for i = 2, range do
local tx = math.floor(origin.x + nx * i + 0.5)
local ty = math.floor(origin.y + ny * i + 0.5)
local tz = origin.z
if dfhack.maps.isValidTilePos(tx, ty, tz) then
dfhack.maps.spawnFlow(xyz2pos(tx, ty, tz), df.flow_type.Fire, -1, -1, 100)
if fc and fc.register_fire then fc.register_fire(tx, ty, tz) end
end
end
end
-- Returns the (sx, sy) direction vector for a projectile.
-- Fortress mode: speed_x/speed_y carry the velocity.
-- Adventure/arena mode: speed fields are always 0; derive direction from
-- target_pos - origin_pos instead.
local function proj_direction(projectile)
if projectile.speed_x ~= 0 or projectile.speed_y ~= 0 then
return projectile.speed_x, projectile.speed_y
end
return projectile.target_pos.x - projectile.origin_pos.x,
projectile.target_pos.y - projectile.origin_pos.y
end
-- Stops a projectile. Zeroing speed is sufficient in fortress mode.
-- In adventure/arena mode the engine uses target_pos for movement, so
-- collapse the destination to the current tile as well.
local function stop_projectile(projectile)
projectile.speed_x = 0
projectile.speed_y = 0
projectile.target_pos.x = projectile.cur_pos.x
projectile.target_pos.y = projectile.cur_pos.y
projectile.target_pos.z = projectile.cur_pos.z
end
---------------------------------------------------------------------------
-- PROJECTILE HANDLER
---------------------------------------------------------------------------
local function on_proj_move(projectile)
if handled[projectile.id] then return end
local item = projectile.item
if not item then return end
-- Only act on FUEL_CELL ammo.
local ok, sub_id = pcall(function()
return dfhack.items.getSubtypeDef(
df.item_type.AMMO, item:getSubtype()).id
end)
if not ok or sub_id ~= "ITEM_AMMO_FUEL_CELL" then return end
handled[projectile.id] = true
local origin = xyz2pos(
projectile.origin_pos.x,
projectile.origin_pos.y,
projectile.origin_pos.z)
local sx, sy = proj_direction(projectile)
local range = get_flame_range(projectile.bow_id)
stop_projectile(projectile)
pcall(spawn_fire_jet, origin, sx, sy, range)
-- Signal maneuver.lua to backstep the shooter away from the flame jet.
-- Shooter is found by matching origin_pos (they haven't moved this tick).
local maneuver = _G.__maneuver
if maneuver and maneuver.on_flame_fired then
local shooter_id = nil
pcall(function()
for _, u in ipairs(df.global.world.units.active) do
if not dfhack.units.isDead(u)
and u.pos.x == origin.x
and u.pos.y == origin.y
and u.pos.z == origin.z then
shooter_id = u.id
break
end
end
end)
if shooter_id then maneuver.on_flame_fired(shooter_id, sx, sy) end
end
end
---------------------------------------------------------------------------
-- POLLING FALLBACK
-- Safety net for any FUEL_CELL that slips past the event handler.
---------------------------------------------------------------------------
local function check_fuel_cells()
if not enabled then return end
if not df.global.world.projectiles then return end
for _, proj in ipairs(df.global.world.projectiles.all) do
local item_proj = df.proj_itemst(proj)
if not item_proj then goto continue end
if handled[item_proj.id] then goto continue end
local item = item_proj.item
if not item then goto continue end
if item:getType() ~= df.item_type.AMMO then goto continue end
local ok, sub_id = pcall(function()
return dfhack.items.getSubtypeDef(
df.item_type.AMMO, item:getSubtype()).id
end)
if not ok or sub_id ~= "ITEM_AMMO_FUEL_CELL" then goto continue end
handled[item_proj.id] = true
local sx, sy = proj_direction(item_proj)
local range = get_flame_range(item_proj.bow_id)
local origin = xyz2pos(
item_proj.origin_pos.x,
item_proj.origin_pos.y,
item_proj.origin_pos.z)
stop_projectile(item_proj)
pcall(spawn_fire_jet, origin, sx, sy, range)
::continue::
end
end
local function cleanup_handled()
if not enabled then return end
pcall(function()
local proj_list = df.global.world.projectiles
if not proj_list then return end
local live = {}
for _, proj in ipairs(proj_list.all) do
live[proj.id] = true
end
for pid in pairs(handled) do
if not live[pid] then
handled[pid] = nil
end
end
end)
end
---------------------------------------------------------------------------
-- ENABLE / DISABLE / STATUS
---------------------------------------------------------------------------
local function enable()
if enabled then
print("[flamethrower] Already enabled")
return
end
eventful.onProjItemCheckMovement[CALLBACK_ID] = on_proj_move
repeatUtil.cancel(CALLBACK_ID_CLEANUP)
repeatUtil.scheduleEvery(CALLBACK_ID_POLL, 50, "ticks", check_fuel_cells)
repeatUtil.scheduleEvery(CALLBACK_ID_CLEANUP, CLEANUP_INTERVAL, "ticks", cleanup_handled)
enabled = true
handled = {}
print("[flamethrower] Enabled")
end
local function disable()
if not enabled then
print("[flamethrower] Already disabled")
return
end
eventful.onProjItemCheckMovement[CALLBACK_ID] = nil
repeatUtil.cancel(CALLBACK_ID_POLL)
repeatUtil.cancel(CALLBACK_ID_CLEANUP)
enabled = false
handled = {}
print("[flamethrower] Disabled")
end
local function status()
print(("[flamethrower] Status: %s"):format(
enabled and "ENABLED" or "DISABLED"))
end
---------------------------------------------------------------------------
-- ARGUMENT PARSING
---------------------------------------------------------------------------
local args = { ... }
local command = args[1] or "enable"
if command == "enable" then
enable()
elseif command == "disable" then
disable()
elseif command == "status" then
status()
else
print("[flamethrower] Usage: flamethrower [enable|disable|status]")
end