-
Notifications
You must be signed in to change notification settings - Fork 198
/
exterminate.lua
311 lines (273 loc) · 9.54 KB
/
exterminate.lua
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
--@module = true
local argparse = require('argparse')
local function spawnLiquid(position, liquid_level, liquid_type, update_liquids)
local map_block = dfhack.maps.getTileBlock(position)
local tile = dfhack.maps.getTileFlags(position)
tile.flow_size = liquid_level
tile.liquid_type = liquid_type
tile.flow_forbid = false
map_block.flags.update_liquid = update_liquids
map_block.flags.update_liquid_twice = update_liquids
end
local function checkUnit(opts, unit)
if not dfhack.units.isActive(unit) or
(unit.body.blood_max ~= 0 and unit.body.blood_count == 0) or
unit.flags1.caged or
unit.flags1.chained
then
return false
end
if opts.only_visible and not dfhack.units.isVisible(unit) then
return false
end
if not opts.include_friendly and not dfhack.units.isDanger(unit) and not dfhack.units.isWildlife(unit) then
return false
end
if opts.selected_caste and opts.selected_caste ~= df.creature_raw.find(unit.race).caste[unit.caste].caste_id then
return false
end
return true
end
killMethod = {
INSTANT = 0,
BUTCHER = 1,
MAGMA = 2,
DROWN = 3,
VAPORIZE = 4,
DISINTEGRATE = 5,
KNOCKOUT = 6,
TRAUMATIZE = 7,
}
-- removes the unit from existence, leaving no corpse if the unit hasn't died
-- by the time the vanish countdown expires
local function vaporizeUnit(unit, target_value)
target_value = target_value or 1
unit.animal.vanish_countdown = target_value
end
-- Kills a unit by removing blood and also setting a vanish countdown as a failsafe.
local function destroyUnit(unit)
unit.body.blood_count = 0
vaporizeUnit(unit, 2)
end
-- Marks a unit for slaughter at the butcher's shop.
local function butcherUnit(unit)
unit.flags2.slaughter = true
end
-- Knocks a unit out for 30k ticks or the target value
local function knockoutUnit(unit, target_value)
target_value = target_value or 30000
unit.counters.unconscious = target_value
end
-- Traumatizes the unit, forcing them to stare off into space. Cuts down on pathfinding
local function traumatizeUnit(unit)
unit.mood = df.mood_type.Traumatized
end
local function drownUnit(unit, liquid_type)
previousPositions = previousPositions or {}
previousPositions[unit.id] = copyall(unit.pos)
local function createLiquid()
spawnLiquid(unit.pos, 7, liquid_type)
if not same_xyz(previousPositions[unit.id], unit.pos) then
spawnLiquid(previousPositions[unit.id], 0, nil, false)
previousPositions[unit.id] = copyall(unit.pos)
end
if unit.flags2.killed then
spawnLiquid(previousPositions[unit.id], 0, nil, false)
else
dfhack.timeout(1, 'ticks', createLiquid)
end
end
createLiquid()
end
local function destroyInventory(unit)
for index = #unit.inventory-1, 0, -1 do
local item = unit.inventory[index].item
dfhack.items.remove(item)
end
end
function killUnit(unit, method)
if method == killMethod.BUTCHER then
butcherUnit(unit)
elseif method == killMethod.MAGMA then
drownUnit(unit, df.tile_liquid.Magma)
elseif method == killMethod.DROWN then
drownUnit(unit, df.tile_liquid.Water)
elseif method == killMethod.VAPORIZE then
vaporizeUnit(unit)
elseif method == killMethod.DISINTEGRATE then
vaporizeUnit(unit)
destroyInventory(unit)
elseif method == killMethod.KNOCKOUT then
knockoutUnit(unit)
elseif method == killMethod.TRAUMATIZE then
traumatizeUnit(unit)
else
destroyUnit(unit)
end
end
local function getRaceCastes(race_id)
local unit_castes = {}
for _, caste in pairs(df.creature_raw.find(race_id).caste) do
unit_castes[caste.caste_id] = {}
end
return unit_castes
end
local function getMapRaces(opts)
local map_races = {}
for _, unit in pairs(df.global.world.units.active) do
if not checkUnit(opts, unit) then goto continue end
local race_name, display_name
if dfhack.units.isUndead(unit) then
race_name = 'UNDEAD'
display_name = 'UNDEAD'
else
local craw = df.creature_raw.find(unit.race)
race_name = craw.creature_id
if race_name:match('^FORGOTTEN_BEAST_[0-9]+$') or race_name:match('^TITAN_[0-9]+$') then
display_name = dfhack.units.getReadableName(unit)
else
display_name = craw.name[0]
end
end
local race = ensure_key(map_races, race_name)
race.id = unit.race
race.name = race_name
race.display_name = display_name
race.count = (race.count or 0) + 1
::continue::
end
return map_races
end
if dfhack_flags.module then
return
end
local options, args = {
help = false,
method = killMethod.INSTANT,
only_visible = false,
include_friendly = false,
limit = -1,
}, {...}
local positionals = argparse.processArgsGetopt(args, {
{'h', 'help', handler = function() options.help = true end},
{'m', 'method', handler = function(arg) options.method = killMethod[arg:upper()] end, hasArg = true},
{'o', 'only-visible', handler = function() options.only_visible = true end},
{'f', 'include-friendly', handler = function() options.include_friendly = true end},
{'l', 'limit', handler = function(arg) options.limit = argparse.positiveInt(arg, 'limit') end, hasArg = true},
})
if not dfhack.isMapLoaded() then
qerror('This script requires a fortress map to be loaded')
end
if positionals[1] == "help" or options.help then
print(dfhack.script_help())
return
end
if positionals[1] == "this" then
local selected_unit = dfhack.gui.getSelectedUnit()
if not selected_unit then
qerror("Select a unit and run the script again.")
end
killUnit(selected_unit, options.method)
print('Unit exterminated.')
return
end
local map_races = getMapRaces(options)
if not positionals[1] or positionals[1] == 'list' then
local sorted_races = {}
local max_width = 10
for _,v in pairs(map_races) do
max_width = math.max(max_width, #v.name)
table.insert(sorted_races, v)
end
table.sort(sorted_races, function(a, b)
if a.count == b.count then
local asuffix, bsuffix = a.name:match('([0-9]+)$'), b.name:match('([0-9]+)$')
if asuffix and bsuffix then
local aname, bname = a.name:match('(.*)_[0-9]+$'), b.name:match('(.*)_[0-9]+$')
local anum, bnum = tonumber(asuffix), tonumber(bsuffix)
if aname == bname and anum and bnum then
return anum < bnum
end
end
return a.name < b.name
end
return a.count > b.count
end)
for _,v in ipairs(sorted_races) do
local name_str = v.name
if name_str ~= 'UNDEAD' and v.display_name ~= string.lower(name_str):gsub('_', ' ') then
name_str = ('%-'..tostring(max_width)..'s (%s)'):format(name_str, v.display_name)
end
print(('%4s %s'):format(v.count, name_str))
end
return
end
local count, target = 0, 'creature(s)'
local race_name = table.concat(positionals, ' ')
if race_name:lower() == 'undead' then
target = 'undead'
if not map_races.UNDEAD then
qerror("No undead found on the map.")
end
for _, unit in pairs(df.global.world.units.active) do
if dfhack.units.isUndead(unit) and checkUnit(options, unit) then
killUnit(unit, options.method)
count = count + 1
end
end
elseif positionals[1]:split(':')[1] == "all" then
options.selected_caste = positionals[1]:split(':')[2]
for _, unit in ipairs(df.global.world.units.active) do
if options.limit > 0 and count >= options.limit then
break
end
if not checkUnit(options, unit) then
goto skipunit
end
killUnit(unit, options.method)
count = count + 1
:: skipunit ::
end
else
local selected_race, selected_caste = race_name, nil
if string.find(selected_race, ':') then
local tokens = selected_race:split(':')
selected_race, selected_caste = tokens[1], tokens[2]
end
if not map_races[selected_race] then
local selected_race_upper = selected_race:upper()
local selected_race_under = selected_race_upper:gsub(' ', '_')
if map_races[selected_race_upper] then
selected_race = selected_race_upper
elseif map_races[selected_race_under] then
selected_race = selected_race_under
else
qerror("No creatures of this race on the map (" .. selected_race .. ").")
end
end
local race_castes = getRaceCastes(map_races[selected_race].id)
if selected_caste and not race_castes[selected_caste] then
local selected_caste_upper = selected_caste:upper()
if race_castes[selected_caste_upper] then
selected_caste = selected_caste_upper
else
qerror("Invalid caste: " .. selected_caste)
end
end
target = selected_race
options.selected_caste = selected_caste
for _, unit in pairs(df.global.world.units.active) do
if options.limit > 0 and count >= options.limit then
break
end
if not checkUnit(options, unit) then
goto skipunit
end
if selected_race == df.creature_raw.find(unit.race).creature_id then
killUnit(unit, options.method)
count = count + 1
end
:: skipunit ::
end
end
print(([[Exterminated %d %s.]]):format(count, target))