-
Notifications
You must be signed in to change notification settings - Fork 227
Expand file tree
/
Copy pathsiegemanager.lua
More file actions
560 lines (491 loc) · 16.7 KB
/
siegemanager.lua
File metadata and controls
560 lines (491 loc) · 16.7 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
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
local gui = require('gui')
local widgets = require('gui.widgets')
local textures = require('gui.textures')
--
-- Button label definitions
--
local function make_button(ascii, pens, x, y)
local out = {}
-- Grid of 3x3 tiles
for i=1,3 do
local tmp = {}
for j=1,3 do
table.insert(tmp, {
tile=dfhack.pen.parse{
ch=ascii[i][j],
fg=pens[i][j],
keep_lower=true,
tile=dfhack.screen.findGraphicsTile('INTERFACE_BITS',x+j-1,y+i-1)
},
})
end
table.insert(out, tmp)
end
return out
end
local function make_activity_button(ch, color, border_color, border_acolor, x, y, ax, ay)
local ascii = {
{218, 196, 191},
{179, ch, 179},
{192, 196, 217},
}
local function make_pens(border, main)
return {
{border, border, border},
{border, main, border},
{border, border, border},
}
end
return {
inactive = make_button(ascii, make_pens(border_color, color), x, y),
active = make_button(ascii, make_pens(border_acolor, color), ax, ay)
}
end
local activity_buttons = {
-- NotInUse
[0] = make_activity_button('-', COLOR_LIGHTRED, COLOR_DARKGREY, COLOR_YELLOW, 59, 18, 59, 15),
-- KeepLoaded
[1] = make_activity_button('L', COLOR_LIGHTCYAN, COLOR_DARKGREY, COLOR_YELLOW, 59, 24, 59, 21),
-- PrepareToFire
[2] = make_activity_button('P', COLOR_YELLOW, COLOR_DARKGREY, COLOR_YELLOW, 56, 18, 56, 15),
-- FireAtWill
[3] = make_activity_button('F', COLOR_LIGHTRED, COLOR_DARKGREY, COLOR_YELLOW, 53, 18, 53, 15),
-- PracticeFire
[4] = make_activity_button('T', COLOR_LIGHTGREEN, COLOR_DARKGREY, COLOR_YELLOW, 44, 42, 44, 39),
}
local goto_button_ascii = {
{218, 196, 191},
{26, 'X', 179},
{192, 196, 217},
}
local goto_button_color = {
{COLOR_LIGHTCYAN, COLOR_LIGHTCYAN, COLOR_LIGHTCYAN},
{COLOR_LIGHTCYAN, COLOR_LIGHTRED, COLOR_LIGHTCYAN},
{COLOR_LIGHTCYAN, COLOR_LIGHTCYAN, COLOR_LIGHTCYAN},
}
local goto_button = make_button(goto_button_ascii, goto_button_color, 32, 0)
-- TODO: The usage of these icons requires the ability to adjust screentexpos_flag
-- from a pen. Specifically anchor_subordinate, anchor_x_coord, and anchor_y_coord in order
-- to stretch a singular tile to fit the 5x5 area that vanilla displays portraits at.
--
-- local function make_engine_icon(ascii, color, tile_x, tile_y)
-- local icon = {}
-- local tile = dfhack.screen.findGraphicsTile('BUILDING_ICONS',tile_x, tile_y)
--
-- for y=1,5 do
-- icon[y] = {}
-- for x=1,5 do
-- local ch = 32
-- local fg = nil
-- -- Adapt indices for a 3x3 icon into 5x5 like the graphics icon
-- if x >= 2 and x <= 4 and y >= 2 and y <= 4 then
-- ch = ascii[y-1][x-1]
-- fg = color[y-1][x-1]
-- end
-- icon[y][x] = {
-- tile = dfhack.pen.parse{
-- ch=ch,
-- fg=fg,
-- keep_lower=true,
-- tile=subtile,
-- },
-- }
-- end
-- end
--
-- return icon
-- end
--
-- local catapult_icon_ascii = {
-- {177, 210, 177},
-- {177, 186, 177},
-- {177, 8, 177},
-- }
-- local catapult_icon_color = {
-- {COLOR_YELLOW, COLOR_BROWN, COLOR_YELLOW},
-- {COLOR_YELLOW, COLOR_BROWN, COLOR_YELLOW},
-- {COLOR_YELLOW, COLOR_BROWN, COLOR_YELLOW},
-- }
--
-- local ballista_icon_ascii = {
-- {220, 30, 220},
-- {221, 179, 222},
-- {92, 207, 47},
-- }
-- local ballista_icon_color = {
-- {COLOR_YELLOW, COLOR_BROWN, COLOR_YELLOW},
-- {COLOR_YELLOW, COLOR_BROWN, COLOR_YELLOW},
-- {COLOR_BROWN, COLOR_YELLOW, COLOR_BROWN},
-- }
--
-- local boltthrower_icon_ascii = {
-- {32, 32, 32},
-- {32, 147, 32},
-- {32, 32, 32},
-- }
-- local boltthrower_icon_color = {
-- {COLOR_BLACK, COLOR_BLACK, COLOR_BLACK},
-- {COLOR_BLACK, COLOR_YELLOW, COLOR_BLACK},
-- {COLOR_BLACK, COLOR_BLACK, COLOR_BLACK},
-- }
--
-- local siegeengine_icons = {
-- -- Catapult
-- [0] = make_engine_icon(catapult_icon_ascii, catapult_icon_color, 7, 11),
-- -- Ballista
-- [1] = make_engine_icon(ballista_icon_ascii, ballista_icon_color, 6, 11),
-- -- Bolt Thrower
-- [2] = make_engine_icon(boltthrower_icon_ascii, boltthrower_icon_color, 3, 12),
-- }
local function is_siege_ammo(item)
return df.item_ammost:is_instance(item)
or df.item_siegeammost:is_instance(item)
or df.item_boulderst:is_instance(item)
end
-- Obtain a list of siege engine buildings on the map with specific information
local function get_siege_engines()
local siege_list = {}
for _, building in ipairs(df.global.world.buildings.other.IN_PLAY) do
if not df.building_siegeenginest:is_instance(building) then goto continue end
if not building.flags.exists then goto continue end
-- Calculate amount of ammo stored in the building
local loaded_ammo = 0
for _, item in ipairs(building.contained_items) do
if item.use_mode == df.building_item_role_type.TEMP and is_siege_ammo(item.item) then
loaded_ammo = loaded_ammo + item.item.stack_size
end
end
-- Display information on active jobs involving this building.
local active_job = nil
for _, job in ipairs(building.jobs) do
if job.job_type == df.job_type.LoadCatapult
or job.job_type == df.job_type.LoadBallista
or job.job_type == df.job_type.LoadBoltThrower then
active_job = 'Loading'
elseif job.job_type == df.job_type.FireCatapult
or job.job_type == df.job_type.FireBallista
or job.job_type == df.job_type.FireBoltThrower then
-- Display `Ready` instead of firing when in standby mode
-- as the same job_type is used when actively firing and waiting.
-- This is to reduce confusion as no projectiles are fired
active_job = building.action == 2 and 'Ready' or 'Firing'
end
end
siege_list[building.id] = {
id = building.id,
type = building.type,
action = building.action,
loaded_ammo = loaded_ammo,
name = building.name,
active_job=active_job,
pos = {
x=building.centerx,
y=building.centery,
z=building.z,
},
}
::continue::
end
return siege_list
end
local function item_in_list(item, list)
for _, v in ipairs(list) do
if v == item then
return true
end
end
return false
end
-- Set siegeengine action, returning false if the building isn't found
local function set_siege_engine_action(id_list, action)
local count = 0
for _, building in ipairs(df.global.world.buildings.other.IN_PLAY) do
if item_in_list(building.id, id_list) then
if not df.building_siegeenginest:is_instance(building) then return false end
building.action = action
count = count + 1
if count == #id_list then
return count
end
end
end
return count
end
-- SiegeEngineList
SiegeEngineList = defclass(SiegeEngineList, widgets.Panel)
SiegeEngineList.ATTRS = {
view_id='list',
frame={l=0, r=0, t=1, b=7},
frame_style=gui.FRAME_INTERIOR,
-- Filters by siegeengine_type, -1 being all
type_filter=-1,
}
function SiegeEngineList:init()
self:refresh_data()
self.refresh_rate = 30
self.refresh_timer = 0
self.button_start_x = 24
self:addviews({
widgets.List {
view_id='list',
frame={l=0,t=0,b=0,r=0},
row_height=3,
}
})
self:refresh_view(true)
end
-- Used to manage how often the ui data refreshes
function SiegeEngineList:onRenderBody()
self.refresh_timer = self.refresh_timer + 1
if (self.refresh_timer > self.refresh_rate) then
self.refresh_timer = 0
self:refresh_data()
end
end
local siegeengine_type_string = {
[0] = 'Catapult',
[1] = 'Ballista',
[2] = 'Thrower'
}
local function concat_tables(to, from)
for _, val in ipairs(from) do
table.insert(to, val)
end
end
-- TODO: Replace constants with df.siegeengine_action enum once structures merged
local action_button_order={3, 4, 2, 1, 0}
local action_button_keybinds = {'CUSTOM_SHIFT_F', 'CUSTOM_SHIFT_T', 'CUSTOM_SHIFT_P', 'CUSTOM_SHIFT_L', 'CUSTOM_SHIFT_N'}
-- Add a multiline label definition from `from` to `to` starting at y=y_start or 0
local function add_multiline(to, from, y_start)
for i, item in pairs(from) do
concat_tables(to[i + (y_start or 0)], from[i])
end
end
-- Label string callbacks, used to update the display without resetting the scrolling List
local action_text_pen = dfhack.pen.parse({ fg=COLOR_GREEN })
function SiegeEngineList:get_action_text(id)
return self.engines[id].active_job or ''
end
local ammo_text_pen = dfhack.pen.parse({ fg=COLOR_GREY })
function SiegeEngineList:get_ammo_text(id)
return (self.engines[id].loaded_ammo or '?')
end
local ammo_out_pen = dfhack.pen.parse({ fg=COLOR_RED })
local ammo_pen = dfhack.pen.parse({ fg=COLOR_YELLOW })
function SiegeEngineList:get_ammo_pen(id)
return self.engines[id].loaded_ammo == 0 and ammo_out_pen or ammo_pen
end
function SiegeEngineList:get_name_text(id)
local engine = self.engines[id]
return siegeengine_type_string[engine.type]..' '..(#engine.name ==0 and 'Unnamed' or engine.name)
end
function SiegeEngineList:get_activity_button_tile(id, action, x, y)
return activity_buttons[action][self.engines[id].action == action and 'active' or 'inactive'][y][x].tile
end
-- Generate the multiline Label display for an engine
function SiegeEngineList:make_entry_text(engine)
local lines = {
{{text=self:callback('get_name_text', engine.id), width=self.button_start_x}},
{
{ text=self:callback('get_action_text', engine.id), pen=action_text_pen, width=9 },
{ text='ammo=', pen=ammo_text_pen },
{ text=self:callback('get_ammo_text', engine.id), pen=self:callback('get_ammo_pen', engine.id), width=self.button_start_x - 14},
},
{{text='', width=self.button_start_x}},
}
-- Goto Position Button
add_multiline(lines, goto_button)
-- Padding following goto button
add_multiline(lines, {{{text='', width=3}},{{text='', width=3}},{{text='', width=3}}})
-- Siege Engine activity selection buttons
for _, button_action in ipairs(action_button_order) do
for y=1,3 do
for x=1,3 do
table.insert(lines[y], { tile = self:callback('get_activity_button_tile', engine.id, button_action, x, y)})
end
end
end
-- Transform multiline label into a single label with newlines
local out_tokens = {}
for i=1,3 do
concat_tables(out_tokens, lines[i])
table.insert(out_tokens, NEWLINE)
end
return out_tokens
end
-- Refresh the engine information being displayed, but not the list.
-- Updating data here *does not* add or remove new/deleted engines.
function SiegeEngineList:refresh_data()
local old_engines = self.engines
self.engines = get_siege_engines()
if self.type_filter ~= -1 then
for id, eng in pairs(self.engines) do
if eng.type ~= self.type_filter then
self.engines[id] = nil
end
end
end
if not old_engines then return end
-- Determine if a listed engine was removed, if so refresh ui
for id, _ in pairs(old_engines) do
if self.engines[id] == nil then
self:refresh_view(false)
return
end
end
end
-- Refresh the engine list, updating to display new/deleted engines correctly.
function SiegeEngineList:refresh_view(refresh_data)
if refresh_data then self:refresh_data() end
local choices = {}
for _, data in pairs(self.engines) do
table.insert(choices, {
text=self:make_entry_text(data),
search_key="",
data=data.id
});
end
self.subviews.list:setChoices(choices)
end
function SiegeEngineList:reveal_selected()
local _, selected = self.subviews.list:getSelected()
if selected ~= nil then
dfhack.gui.revealInDwarfmodeMap(self.engines[selected.data].pos, true, true)
end
end
function SiegeEngineList:set_all_action(action)
local listed = {}
for key, _ in pairs(self.engines) do
listed[#listed+1] = key
end
local count = set_siege_engine_action(listed, action)
if count ~= #listed then
self:refresh_view(true)
return
end
for _, engine in ipairs(self.engines) do
engine.action = action
end
end
function SiegeEngineList:set_selected_action(action)
local _, selected = self.subviews.list:getSelected()
if not selected then
qerror('No siege engine selected')
end
local successful = set_siege_engine_action({selected.data}, action)
if not successful then
self:refresh_view(true)
return
end
-- Successfully updated, just update the cached state
self.engines[selected.data].action = action
end
function SiegeEngineList:onInput(keys)
if not keys._MOUSE_L then
SiegeEngineList.super.onInput(self, keys)
return
end
local list = self.subviews.list
local idx = list:getIdxUnderMouse()
if not idx then
SiegeEngineList.super.onInput(self, keys)
return
end
local x = list:getMousePos()
if x < self.button_start_x or x > self.button_start_x+(3*7) then
SiegeEngineList.super.onInput(self, keys)
return
end
list:setSelected(idx)
-- 0 is goto, 1 is blank, following are action buttons
local button_pressed = math.ceil((x-self.button_start_x+1)/3)-1
if button_pressed == 0 then
self:reveal_selected()
return
end
if button_pressed == 1 then
-- Blank space
return
end
local action = action_button_order[button_pressed-1]
self:set_selected_action(action)
end
-- SiegeManager
SiegeManager = defclass(SiegeManager, widgets.Window)
SiegeManager.ATTRS = {
frame_title = 'Siege Manager',
frame = {w=54,h=48,r=2,t=18},
resizable=true,
drag_anchors = {title=true},
engines=DEFAULT_NIL,
}
function SiegeManager:init()
self:addviews({
SiegeEngineList {},
widgets.CycleHotkeyLabel {
frame={b=6},
key='CUSTOM_T',
on_change=self:callback('set_type_filter'),
label='Show Types:',
options={
{label='All', value=-1},
{label='Ballista', value=df.siegeengine_type.Ballista},
{label='Bolt Thrower', value=df.siegeengine_type.BoltThrower},
{label='Catapult', value=df.siegeengine_type.Catapult},
},
initial_option=1,
},
widgets.ToggleHotkeyLabel {
view_id = 'configure_all',
frame={b=2},
key = 'CUSTOM_SHIFT_A',
key_sep = ': ',
label = 'Configure All',
initial_option=2,
},
widgets.HotkeyLabel {
frame={b=0},
key='CUSTOM_CTRL_C',
label='Reveal in World',
on_activate=self:callback('reveal_selected')
},
})
for i, action_button in ipairs(action_button_order) do
self:addviews({
widgets.HotkeyLabel {
frame = {b=3, l=(i - 1)*2},
key = action_button_keybinds[i],
key_sep = i == #action_button_order and ': ' or '',
label = i == #action_button_order and 'Set Action' or '',
on_activate = self:callback('set_action', action_button)
}
})
end
end
function SiegeManager:set_type_filter(new)
self.subviews.list.type_filter = new
self.subviews.list:refresh_view(true)
end
function SiegeManager:reveal_selected()
self.subviews.list:reveal_selected()
end
function SiegeManager:set_action(action)
if self.subviews.configure_all:getOptionValue() then
self.subviews.list:set_all_action(action)
else
self.subviews.list:set_selected_action(action)
end
end
-- SiegeManagerScreen
SiegeManagerScreen = defclass(SiegeManagerScreen, gui.ZScreen)
SiegeManagerScreen.ATTRS = {}
function SiegeManagerScreen:init()
self:addviews({SiegeManager{}})
end
function SiegeManagerScreen:onDismiss()
view = nil
end
if not dfhack.isMapLoaded() then
qerror('requires a map to be loaded')
end
view = view and view:raise() or SiegeManagerScreen{}:show()