Merge pull request #2999 from myk002/myk_buildingplan_refactor
refactor buildingplan into smaller filesdevelop
commit
d48dad052e
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,731 @@
|
||||
local _ENV = mkmodule('plugins.buildingplan.filterselection')
|
||||
|
||||
local gui = require('gui')
|
||||
local pens = require('plugins.buildingplan.pens')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
local uibs = df.global.buildreq
|
||||
local to_pen = dfhack.pen.parse
|
||||
|
||||
local function get_cur_filters()
|
||||
return dfhack.buildings.getFiltersByType({}, uibs.building_type,
|
||||
uibs.building_subtype, uibs.custom_type)
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- Slider
|
||||
--
|
||||
|
||||
Slider = defclass(Slider, widgets.Widget)
|
||||
Slider.ATTRS{
|
||||
num_stops=DEFAULT_NIL,
|
||||
get_left_idx_fn=DEFAULT_NIL,
|
||||
get_right_idx_fn=DEFAULT_NIL,
|
||||
on_left_change=DEFAULT_NIL,
|
||||
on_right_change=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
function Slider:preinit(init_table)
|
||||
init_table.frame = init_table.frame or {}
|
||||
init_table.frame.h = init_table.frame.h or 1
|
||||
end
|
||||
|
||||
function Slider:init()
|
||||
if self.num_stops < 2 then error('too few Slider stops') end
|
||||
self.is_dragging_target = nil -- 'left', 'right', or 'both'
|
||||
self.is_dragging_idx = nil -- offset from leftmost dragged tile
|
||||
end
|
||||
|
||||
local function slider_get_width_per_idx(self)
|
||||
return math.max(5, (self.frame_body.width-7) // (self.num_stops-1))
|
||||
end
|
||||
|
||||
function Slider:onInput(keys)
|
||||
if not keys._MOUSE_L_DOWN then return false end
|
||||
local x = self:getMousePos()
|
||||
if not x then return false end
|
||||
local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn()
|
||||
local width_per_idx = slider_get_width_per_idx(self)
|
||||
local left_pos = width_per_idx*(left_idx-1)
|
||||
local right_pos = width_per_idx*(right_idx-1) + 4
|
||||
if x < left_pos then
|
||||
self.on_left_change(self.get_left_idx_fn() - 1)
|
||||
elseif x < left_pos+3 then
|
||||
self.is_dragging_target = 'left'
|
||||
self.is_dragging_idx = x - left_pos
|
||||
elseif x < right_pos then
|
||||
self.is_dragging_target = 'both'
|
||||
self.is_dragging_idx = x - left_pos
|
||||
elseif x < right_pos+3 then
|
||||
self.is_dragging_target = 'right'
|
||||
self.is_dragging_idx = x - right_pos
|
||||
else
|
||||
self.on_right_change(self.get_right_idx_fn() + 1)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function slider_do_drag(self, width_per_idx)
|
||||
local x = self.frame_body:localXY(dfhack.screen.getMousePos())
|
||||
local cur_pos = x - self.is_dragging_idx
|
||||
cur_pos = math.max(0, cur_pos)
|
||||
cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos)
|
||||
local offset = self.is_dragging_target == 'right' and -2 or 1
|
||||
local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1
|
||||
local new_left_idx, new_right_idx
|
||||
if self.is_dragging_target == 'right' then
|
||||
new_right_idx = new_idx
|
||||
else
|
||||
new_left_idx = new_idx
|
||||
if self.is_dragging_target == 'both' then
|
||||
new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn()
|
||||
if new_right_idx > self.num_stops then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then
|
||||
self.on_left_change(new_left_idx)
|
||||
end
|
||||
if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then
|
||||
self.on_right_change(new_right_idx)
|
||||
end
|
||||
end
|
||||
|
||||
local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK}
|
||||
local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK}
|
||||
local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK}
|
||||
local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK}
|
||||
local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK}
|
||||
local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK}
|
||||
local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW}
|
||||
local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW}
|
||||
local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW}
|
||||
|
||||
function Slider:onRenderBody(dc, rect)
|
||||
local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn()
|
||||
local width_per_idx = slider_get_width_per_idx(self)
|
||||
-- draw track
|
||||
dc:seek(1,0)
|
||||
dc:char(nil, SLIDER_LEFT_END)
|
||||
dc:char(nil, SLIDER_TRACK)
|
||||
for stop_idx=1,self.num_stops-1 do
|
||||
local track_stop_pen = SLIDER_TRACK_STOP_SELECTED
|
||||
local track_pen = SLIDER_TRACK_SELECTED
|
||||
if left_idx > stop_idx or right_idx < stop_idx then
|
||||
track_stop_pen = SLIDER_TRACK_STOP
|
||||
track_pen = SLIDER_TRACK
|
||||
elseif right_idx == stop_idx then
|
||||
track_pen = SLIDER_TRACK
|
||||
end
|
||||
dc:char(nil, track_stop_pen)
|
||||
for i=2,width_per_idx do
|
||||
dc:char(nil, track_pen)
|
||||
end
|
||||
end
|
||||
if right_idx >= self.num_stops then
|
||||
dc:char(nil, SLIDER_TRACK_STOP_SELECTED)
|
||||
else
|
||||
dc:char(nil, SLIDER_TRACK_STOP)
|
||||
end
|
||||
dc:char(nil, SLIDER_TRACK)
|
||||
dc:char(nil, SLIDER_RIGHT_END)
|
||||
-- draw tabs
|
||||
dc:seek(width_per_idx*(left_idx-1))
|
||||
dc:char(nil, SLIDER_TAB_LEFT)
|
||||
dc:char(nil, SLIDER_TAB_CENTER)
|
||||
dc:char(nil, SLIDER_TAB_RIGHT)
|
||||
dc:seek(width_per_idx*(right_idx-1)+4)
|
||||
dc:char(nil, SLIDER_TAB_LEFT)
|
||||
dc:char(nil, SLIDER_TAB_CENTER)
|
||||
dc:char(nil, SLIDER_TAB_RIGHT)
|
||||
-- manage dragging
|
||||
if self.is_dragging_target then
|
||||
slider_do_drag(self, width_per_idx)
|
||||
end
|
||||
if df.global.enabler.mouse_lbut == 0 then
|
||||
self.is_dragging_target = nil
|
||||
self.is_dragging_idx = nil
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- QualityAndMaterialsPage
|
||||
--
|
||||
|
||||
QualityAndMaterialsPage = defclass(QualityAndMaterialsPage, widgets.Panel)
|
||||
QualityAndMaterialsPage.ATTRS{
|
||||
frame={t=0, l=0},
|
||||
index=DEFAULT_NIL,
|
||||
desc=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
local TYPE_COL_WIDTH = 20
|
||||
local HEADER_HEIGHT = 7
|
||||
local QUALITY_HEIGHT = 9
|
||||
local FOOTER_HEIGHT = 4
|
||||
|
||||
-- returns whether the items matched by the specified filter can have a quality
|
||||
-- rating. This also conveniently indicates whether an item can be decorated.
|
||||
local function can_be_improved(idx)
|
||||
local filter = get_cur_filters()[idx]
|
||||
if filter.flags2 and filter.flags2.building_material then
|
||||
return false;
|
||||
end
|
||||
return filter.item_type ~= df.item_type.WOOD and
|
||||
filter.item_type ~= df.item_type.BLOCKS and
|
||||
filter.item_type ~= df.item_type.BAR and
|
||||
filter.item_type ~= df.item_type.BOULDER
|
||||
end
|
||||
|
||||
local function mat_sort_by_name(a, b)
|
||||
return a.name < b.name
|
||||
end
|
||||
|
||||
local function mat_sort_by_quantity(a, b)
|
||||
return a.quantity > b.quantity or
|
||||
(a.quantity == b.quantity and mat_sort_by_name(a, b))
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:init()
|
||||
self.dirty = true
|
||||
self.summary = ''
|
||||
|
||||
local enable_item_quality = can_be_improved(self.index)
|
||||
|
||||
self:addviews{
|
||||
widgets.Panel{
|
||||
view_id='header',
|
||||
frame={l=0, t=0, h=HEADER_HEIGHT, r=0},
|
||||
frame_inset={l=1},
|
||||
subviews={
|
||||
widgets.Label{
|
||||
frame={l=0, t=0},
|
||||
text='Current filter:',
|
||||
},
|
||||
widgets.WrappedLabel{
|
||||
frame={l=16, t=0, h=2, r=0},
|
||||
text_pen=COLOR_LIGHTCYAN,
|
||||
text_to_wrap=function() return self.summary end,
|
||||
auto_height=false,
|
||||
},
|
||||
widgets.CycleHotkeyLabel{
|
||||
view_id='mat_sort',
|
||||
frame={l=0, t=3, w=21},
|
||||
label='Sort by:',
|
||||
key='CUSTOM_SHIFT_R',
|
||||
options={
|
||||
{label='name', value=mat_sort_by_name},
|
||||
{label='available', value=mat_sort_by_quantity}
|
||||
},
|
||||
on_change=function() self.dirty = true end,
|
||||
},
|
||||
widgets.ToggleHotkeyLabel{
|
||||
view_id='hide_zero',
|
||||
frame={l=0, t=4, w=24},
|
||||
label='Hide unavailable:',
|
||||
key='CUSTOM_SHIFT_H',
|
||||
initial_option=false,
|
||||
on_change=function() self.dirty = true end,
|
||||
},
|
||||
widgets.EditField{
|
||||
view_id='search',
|
||||
frame={l=26, t=3},
|
||||
label_text='Search: ',
|
||||
on_char=function(ch) return ch:match('[%l -]') end,
|
||||
},
|
||||
widgets.Label{
|
||||
frame={l=1, b=0},
|
||||
text='Type',
|
||||
text_pen=COLOR_LIGHTRED,
|
||||
},
|
||||
widgets.Label{
|
||||
frame={l=TYPE_COL_WIDTH, b=0},
|
||||
text='Material',
|
||||
text_pen=COLOR_LIGHTRED,
|
||||
},
|
||||
},
|
||||
},
|
||||
widgets.Panel{
|
||||
view_id='materials_lists',
|
||||
frame={l=0, t=HEADER_HEIGHT, r=0, b=FOOTER_HEIGHT+QUALITY_HEIGHT},
|
||||
frame_style=gui.INTERIOR_FRAME,
|
||||
subviews={
|
||||
widgets.List{
|
||||
view_id='materials_categories',
|
||||
frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3},
|
||||
scroll_keys={},
|
||||
icon_width=2,
|
||||
cursor_pen=COLOR_CYAN,
|
||||
on_submit=self:callback('toggle_category'),
|
||||
},
|
||||
widgets.FilteredList{
|
||||
view_id='materials_mats',
|
||||
frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0},
|
||||
icon_width=2,
|
||||
on_submit=self:callback('toggle_material'),
|
||||
},
|
||||
},
|
||||
},
|
||||
widgets.Panel{
|
||||
view_id='divider',
|
||||
frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=FOOTER_HEIGHT+QUALITY_HEIGHT, w=1},
|
||||
on_render=self:callback('draw_divider'),
|
||||
},
|
||||
widgets.Panel{
|
||||
view_id='quality_panel',
|
||||
frame={l=0, r=0, h=QUALITY_HEIGHT, b=FOOTER_HEIGHT},
|
||||
frame_style=gui.INTERIOR_FRAME,
|
||||
frame_title='Item quality',
|
||||
subviews={
|
||||
widgets.CycleHotkeyLabel{
|
||||
view_id='decorated',
|
||||
frame={l=0, t=1, w=23},
|
||||
key='CUSTOM_SHIFT_D',
|
||||
label='Decorated only:',
|
||||
options={
|
||||
{label='No', value=false},
|
||||
{label='Yes', value=true},
|
||||
},
|
||||
enabled=enable_item_quality,
|
||||
on_change=self:callback('set_decorated'),
|
||||
},
|
||||
widgets.CycleHotkeyLabel{
|
||||
view_id='min_quality',
|
||||
frame={l=0, t=3, w=18},
|
||||
label='Min quality:',
|
||||
label_below=true,
|
||||
key_back='CUSTOM_SHIFT_Z',
|
||||
key='CUSTOM_SHIFT_X',
|
||||
options={
|
||||
{label='Ordinary', value=0},
|
||||
{label='Well Crafted', value=1},
|
||||
{label='Finely Crafted', value=2},
|
||||
{label='Superior', value=3},
|
||||
{label='Exceptional', value=4},
|
||||
{label='Masterful', value=5},
|
||||
{label='Artifact', value=6},
|
||||
},
|
||||
enabled=enable_item_quality,
|
||||
on_change=function(val) self:set_min_quality(val+1) end,
|
||||
},
|
||||
widgets.CycleHotkeyLabel{
|
||||
view_id='max_quality',
|
||||
frame={r=1, t=3, w=18},
|
||||
label='Max quality:',
|
||||
label_below=true,
|
||||
key_back='CUSTOM_SHIFT_Q',
|
||||
key='CUSTOM_SHIFT_W',
|
||||
options={
|
||||
{label='Ordinary', value=0},
|
||||
{label='Well Crafted', value=1},
|
||||
{label='Finely Crafted', value=2},
|
||||
{label='Superior', value=3},
|
||||
{label='Exceptional', value=4},
|
||||
{label='Masterful', value=5},
|
||||
{label='Artifact', value=6},
|
||||
},
|
||||
enabled=enable_item_quality,
|
||||
on_change=function(val) self:set_max_quality(val+1) end,
|
||||
},
|
||||
Slider{
|
||||
frame={l=0, t=6},
|
||||
num_stops=7,
|
||||
get_left_idx_fn=function()
|
||||
return self.subviews.min_quality:getOptionValue() + 1
|
||||
end,
|
||||
get_right_idx_fn=function()
|
||||
return self.subviews.max_quality:getOptionValue() + 1
|
||||
end,
|
||||
on_left_change=self:callback('set_min_quality'),
|
||||
on_right_change=self:callback('set_max_quality'),
|
||||
active=enable_item_quality,
|
||||
},
|
||||
},
|
||||
},
|
||||
widgets.Panel{
|
||||
view_id='footer',
|
||||
frame={l=0, r=0, b=0, h=FOOTER_HEIGHT},
|
||||
frame_inset={t=1, l=1},
|
||||
subviews={
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=0, t=0},
|
||||
label='Toggle',
|
||||
auto_width=true,
|
||||
key='SELECT',
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=0, t=2},
|
||||
label='Done',
|
||||
auto_width=true,
|
||||
key='LEAVESCREEN',
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=30, t=0},
|
||||
label='Invert selection',
|
||||
auto_width=true,
|
||||
key='CUSTOM_SHIFT_I',
|
||||
on_activate=self:callback('invert_materials'),
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=30, t=2},
|
||||
label='Reset filter',
|
||||
auto_width=true,
|
||||
key='CUSTOM_SHIFT_X',
|
||||
on_activate=self:callback('clear_filter'),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
-- replace the FilteredList's built-in EditField with our own
|
||||
self.subviews.materials_mats.list.frame.t = 0
|
||||
self.subviews.materials_mats.edit.visible = false
|
||||
self.subviews.materials_mats.edit = self.subviews.search
|
||||
self.subviews.search.on_change = self.subviews.materials_mats:callback('onFilterChange')
|
||||
end
|
||||
|
||||
local MAT_ENABLED_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN}
|
||||
local MAT_DISABLED_PEN = to_pen{ch='x', fg=COLOR_RED}
|
||||
|
||||
local function make_cat_choice(label, cat, key, cats)
|
||||
local enabled = cats[cat]
|
||||
local icon = nil
|
||||
if not cats.unset then
|
||||
icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN
|
||||
end
|
||||
return {
|
||||
text=label,
|
||||
key=key,
|
||||
enabled=enabled,
|
||||
cat=cat,
|
||||
icon=icon,
|
||||
}
|
||||
end
|
||||
|
||||
local function make_mat_choice(name, props, enabled, cats)
|
||||
local quantity = tonumber(props.count)
|
||||
local text = ('%5d - %s'):format(quantity, name)
|
||||
local icon = nil
|
||||
if not cats.unset then
|
||||
icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN
|
||||
end
|
||||
return {
|
||||
text=text,
|
||||
enabled=enabled,
|
||||
icon=icon,
|
||||
name=name,
|
||||
cat=props.category,
|
||||
quantity=quantity,
|
||||
}
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:refresh()
|
||||
local summary = self.desc
|
||||
local subviews = self.subviews
|
||||
|
||||
local buildingplan = require('plugins.buildingplan')
|
||||
|
||||
local heat = buildingplan.getHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type)
|
||||
if heat >= 2 then summary = 'Magma safe ' .. summary
|
||||
elseif heat == 1 then summary = 'Fire safe ' .. summary
|
||||
else summary = 'Any ' .. summary
|
||||
end
|
||||
|
||||
local quality = buildingplan.getQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
|
||||
subviews.decorated:setOption(quality.decorated ~= 0)
|
||||
subviews.min_quality:setOption(quality.min_quality)
|
||||
subviews.max_quality:setOption(quality.max_quality)
|
||||
|
||||
local cats = buildingplan.getMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
|
||||
local category_choices={
|
||||
make_cat_choice('Stone', 'stone', 'CUSTOM_SHIFT_S', cats),
|
||||
make_cat_choice('Wood', 'wood', 'CUSTOM_SHIFT_O', cats),
|
||||
make_cat_choice('Metal', 'metal', 'CUSTOM_SHIFT_M', cats),
|
||||
make_cat_choice('Glass', 'glass', 'CUSTOM_SHIFT_G', cats),
|
||||
}
|
||||
self.subviews.materials_categories:setChoices(category_choices)
|
||||
|
||||
local mats = buildingplan.getMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
|
||||
local mat_choices = {}
|
||||
local hide_zero = self.subviews.hide_zero:getOptionValue()
|
||||
local enabled_mat_names = {}
|
||||
for name,props in pairs(mats) do
|
||||
local enabled = props.enabled == 'true' and cats[props.category]
|
||||
if not cats.unset and enabled then
|
||||
table.insert(enabled_mat_names, name)
|
||||
end
|
||||
if not hide_zero or tonumber(props.count) > 0 then
|
||||
table.insert(mat_choices, make_mat_choice(name, props, enabled, cats))
|
||||
end
|
||||
end
|
||||
table.sort(mat_choices, self.subviews.mat_sort:getOptionValue())
|
||||
|
||||
local prev_filter = self.subviews.search.text
|
||||
self.subviews.materials_mats:setChoices(mat_choices)
|
||||
self.subviews.materials_mats:setFilter(prev_filter)
|
||||
|
||||
if #enabled_mat_names > 0 then
|
||||
table.sort(enabled_mat_names)
|
||||
summary = summary .. (' of %s'):format(table.concat(enabled_mat_names, ', '))
|
||||
end
|
||||
|
||||
self.summary = summary
|
||||
self.dirty = false
|
||||
self:updateLayout()
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:toggle_category(_, choice)
|
||||
local cats = {}
|
||||
if not choice.icon then
|
||||
-- toggling from unset to something is set
|
||||
table.insert(cats, choice.cat)
|
||||
else
|
||||
choice.enabled = not choice.enabled
|
||||
for _,c in ipairs(self.subviews.materials_categories:getChoices()) do
|
||||
if c.enabled then
|
||||
table.insert(cats, c.cat)
|
||||
end
|
||||
end
|
||||
end
|
||||
require('plugins.buildingplan').setMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, cats)
|
||||
self.dirty = true
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:toggle_material(_, choice)
|
||||
local mats = {}
|
||||
if not choice.icon then
|
||||
-- toggling from unset to something is set
|
||||
table.insert(mats, choice.name)
|
||||
else
|
||||
for _,c in ipairs(self.subviews.materials_mats:getChoices()) do
|
||||
local enabled = c.enabled
|
||||
if choice.name == c.name then
|
||||
enabled = not c.enabled
|
||||
end
|
||||
if enabled then
|
||||
table.insert(mats, c.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
require('plugins.buildingplan').setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats)
|
||||
self.dirty = true
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:invert_materials()
|
||||
local mats = {}
|
||||
for _,c in ipairs(self.subviews.materials_mats:getChoices()) do
|
||||
if not c.icon then return end
|
||||
if not c.enabled then
|
||||
table.insert(mats, c.name)
|
||||
end
|
||||
end
|
||||
require('plugins.buildingplan').setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats)
|
||||
self.dirty = true
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:clear_filter()
|
||||
require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
|
||||
self.dirty = true
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:set_decorated(decorated)
|
||||
local subviews = self.subviews
|
||||
require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1,
|
||||
decorated and 1 or 0, subviews.min_quality:getOptionValue(), subviews.max_quality:getOptionValue())
|
||||
self.dirty = true
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:set_min_quality(idx)
|
||||
idx = math.min(6, math.max(0, idx-1))
|
||||
local subviews = self.subviews
|
||||
subviews.min_quality:setOption(idx)
|
||||
if subviews.max_quality:getOptionValue() < idx then
|
||||
subviews.max_quality:setOption(idx)
|
||||
end
|
||||
require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1,
|
||||
subviews.decorated:getOptionValue() and 1 or 0, idx, subviews.max_quality:getOptionValue())
|
||||
self.dirty = true
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:set_max_quality(idx)
|
||||
idx = math.min(6, math.max(0, idx-1))
|
||||
local subviews = self.subviews
|
||||
subviews.max_quality:setOption(idx)
|
||||
if subviews.min_quality:getOptionValue() > idx then
|
||||
subviews.min_quality:setOption(idx)
|
||||
end
|
||||
require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1,
|
||||
subviews.decorated:getOptionValue() and 1 or 0, subviews.min_quality:getOptionValue(), idx)
|
||||
self.dirty = true
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:draw_divider(dc)
|
||||
local y2 = dc.height - 1
|
||||
for y=0,y2 do
|
||||
dc:seek(0, y)
|
||||
if y == 0 then
|
||||
dc:char(nil, pens.VERT_TOP_PEN)
|
||||
elseif y == y2 then
|
||||
dc:char(nil, pens.VERT_BOT_PEN)
|
||||
else
|
||||
dc:char(nil, pens.VERT_MID_PEN)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function QualityAndMaterialsPage:onRenderFrame(dc, rect)
|
||||
QualityAndMaterialsPage.super.onRenderFrame(self, dc, rect)
|
||||
if self.dirty then
|
||||
self:refresh()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- GlobalSettingsPage
|
||||
--
|
||||
|
||||
GlobalSettingsPage = defclass(GlobalSettingsPage, widgets.ResizingPanel)
|
||||
GlobalSettingsPage.ATTRS{
|
||||
autoarrange_subviews=true,
|
||||
frame={t=0, l=0},
|
||||
frame_style=gui.INTERIOR_FRAME,
|
||||
}
|
||||
|
||||
function GlobalSettingsPage:init()
|
||||
self:addviews{
|
||||
widgets.WrappedLabel{
|
||||
frame={l=0},
|
||||
text_to_wrap='These options will affect the selection of "Generic Materials" for all future buildings.',
|
||||
},
|
||||
widgets.Panel{
|
||||
frame={h=1},
|
||||
},
|
||||
widgets.ToggleHotkeyLabel{
|
||||
view_id='blocks',
|
||||
frame={l=0},
|
||||
key='CUSTOM_B',
|
||||
label='Blocks',
|
||||
label_width=8,
|
||||
on_change=self:callback('update_setting', 'blocks'),
|
||||
},
|
||||
widgets.ToggleHotkeyLabel{
|
||||
view_id='logs',
|
||||
frame={l=0},
|
||||
key='CUSTOM_L',
|
||||
label='Logs',
|
||||
label_width=8,
|
||||
on_change=self:callback('update_setting', 'logs'),
|
||||
},
|
||||
widgets.ToggleHotkeyLabel{
|
||||
view_id='boulders',
|
||||
frame={l=0},
|
||||
key='CUSTOM_O',
|
||||
label='Boulders',
|
||||
label_width=8,
|
||||
on_change=self:callback('update_setting', 'boulders'),
|
||||
},
|
||||
widgets.ToggleHotkeyLabel{
|
||||
view_id='bars',
|
||||
frame={l=0},
|
||||
key='CUSTOM_R',
|
||||
label='Bars',
|
||||
label_width=8,
|
||||
on_change=self:callback('update_setting', 'bars'),
|
||||
},
|
||||
}
|
||||
|
||||
self:init_settings()
|
||||
end
|
||||
|
||||
function GlobalSettingsPage:init_settings()
|
||||
local settings = require('plugins.buildingplan').getGlobalSettings()
|
||||
local subviews = self.subviews
|
||||
subviews.blocks:setOption(settings.blocks)
|
||||
subviews.logs:setOption(settings.logs)
|
||||
subviews.boulders:setOption(settings.boulders)
|
||||
subviews.bars:setOption(settings.bars)
|
||||
end
|
||||
|
||||
function GlobalSettingsPage:update_setting(setting, val)
|
||||
dfhack.run_command('buildingplan', 'set', setting, tostring(val))
|
||||
self:init_settings()
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- FilterSelection
|
||||
--
|
||||
|
||||
FilterSelection = defclass(FilterSelection, widgets.Window)
|
||||
FilterSelection.ATTRS{
|
||||
frame_title='Choose filters',
|
||||
frame={w=55, h=53, l=30, t=8},
|
||||
frame_inset={t=1},
|
||||
resizable=true,
|
||||
index=DEFAULT_NIL,
|
||||
desc=DEFAULT_NIL,
|
||||
autoarrange_subviews=true,
|
||||
}
|
||||
|
||||
function FilterSelection:init()
|
||||
self:addviews{
|
||||
widgets.TabBar{
|
||||
frame={t=0},
|
||||
labels={
|
||||
'Quality and materials',
|
||||
'Global settings',
|
||||
},
|
||||
on_select=function(idx)
|
||||
self.subviews.pages:setSelected(idx)
|
||||
self:updateLayout()
|
||||
end,
|
||||
get_cur_page=function() return self.subviews.pages:getSelected() end,
|
||||
key='CUSTOM_CTRL_T',
|
||||
},
|
||||
widgets.Widget{
|
||||
frame={h=1},
|
||||
},
|
||||
widgets.Pages{
|
||||
view_id='pages',
|
||||
frame={t=5, l=0, b=0, r=0},
|
||||
subviews={
|
||||
QualityAndMaterialsPage{
|
||||
index=self.index,
|
||||
desc=self.desc
|
||||
},
|
||||
GlobalSettingsPage{},
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
FilterSelectionScreen = defclass(FilterSelectionScreen, gui.ZScreen)
|
||||
FilterSelectionScreen.ATTRS {
|
||||
focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection',
|
||||
pass_movement_keys=true,
|
||||
pass_mouse_clicks=false,
|
||||
defocusable=false,
|
||||
index=DEFAULT_NIL,
|
||||
desc=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
function FilterSelectionScreen:init()
|
||||
self:addviews{
|
||||
FilterSelection{
|
||||
index=self.index,
|
||||
desc=self.desc
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function FilterSelectionScreen:onShow()
|
||||
-- don't let the building "shadow" follow the mouse cursor while this screen is open
|
||||
df.global.game.main_interface.bottom_mode_selected = -1
|
||||
end
|
||||
|
||||
function FilterSelectionScreen:onDismiss()
|
||||
-- re-enable building shadow
|
||||
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
|
||||
end
|
||||
|
||||
return _ENV
|
@ -0,0 +1,148 @@
|
||||
local _ENV = mkmodule('plugins.buildingplan.inspectoroverlay')
|
||||
|
||||
local gui = require('gui')
|
||||
local overlay = require('plugins.overlay')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
reset_inspector_flag = false
|
||||
|
||||
local function get_building_filters()
|
||||
local bld = dfhack.gui.getSelectedBuilding()
|
||||
return dfhack.buildings.getFiltersByType({},
|
||||
bld:getType(), bld:getSubtype(), bld:getCustomType())
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- InspectorLine
|
||||
--
|
||||
|
||||
InspectorLine = defclass(InspectorLine, widgets.Panel)
|
||||
InspectorLine.ATTRS{
|
||||
idx=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
function InspectorLine:init()
|
||||
self.frame.h = 2
|
||||
self.visible = function() return #get_building_filters() >= self.idx end
|
||||
self:addviews{
|
||||
widgets.Label{
|
||||
frame={t=0, l=0},
|
||||
text={{text=self:callback('get_desc_string')}},
|
||||
},
|
||||
widgets.Label{
|
||||
frame={t=1, l=2},
|
||||
text={{text=self:callback('get_status_line')}},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function InspectorLine:get_desc_string()
|
||||
if self.desc then return self.desc end
|
||||
self.desc = require('plugins.buildingplan').getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1)
|
||||
return self.desc
|
||||
end
|
||||
|
||||
function InspectorLine:get_status_line()
|
||||
if self.status then return self.status end
|
||||
local queue_pos = require('plugins.buildingplan').getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1)
|
||||
if queue_pos <= 0 then
|
||||
return 'Item attached'
|
||||
end
|
||||
self.status = ('Position in line: %d'):format(queue_pos)
|
||||
return self.status
|
||||
end
|
||||
|
||||
function InspectorLine:reset()
|
||||
self.desc = nil
|
||||
self.status = nil
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- InspectorOverlay
|
||||
--
|
||||
|
||||
InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget)
|
||||
InspectorOverlay.ATTRS{
|
||||
default_pos={x=-41,y=14},
|
||||
default_enabled=true,
|
||||
viewscreens='dwarfmode/ViewSheets/BUILDING',
|
||||
frame={w=30, h=15},
|
||||
frame_style=gui.MEDIUM_FRAME,
|
||||
frame_background=gui.CLEAR_PEN,
|
||||
}
|
||||
|
||||
function InspectorOverlay:init()
|
||||
self:addviews{
|
||||
widgets.Label{
|
||||
frame={t=0, l=0},
|
||||
text='Waiting for items:',
|
||||
},
|
||||
InspectorLine{view_id='item1', frame={t=2, l=0}, idx=1},
|
||||
InspectorLine{view_id='item2', frame={t=4, l=0}, idx=2},
|
||||
InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3},
|
||||
InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4},
|
||||
widgets.HotkeyLabel{
|
||||
frame={t=11, l=0},
|
||||
label='adjust filters',
|
||||
key='CUSTOM_CTRL_F',
|
||||
visible=false, -- until implemented
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={t=12, l=0},
|
||||
label='make top priority',
|
||||
key='CUSTOM_CTRL_T',
|
||||
on_activate=self:callback('make_top_priority'),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function InspectorOverlay:reset()
|
||||
self.subviews.item1:reset()
|
||||
self.subviews.item2:reset()
|
||||
self.subviews.item3:reset()
|
||||
self.subviews.item4:reset()
|
||||
reset_inspector_flag = false
|
||||
end
|
||||
|
||||
function InspectorOverlay:make_top_priority()
|
||||
require('plugins.buildingplan').makeTopPriority(dfhack.gui.getSelectedBuilding())
|
||||
self:reset()
|
||||
end
|
||||
|
||||
local RESUME_BUTTON_FRAME = {t=15, h=3, r=73, w=25}
|
||||
|
||||
local function mouse_is_over_resume_button(rect)
|
||||
local x,y = dfhack.screen.getMousePos()
|
||||
if not x then return false end
|
||||
if y < RESUME_BUTTON_FRAME.t or y > RESUME_BUTTON_FRAME.t + RESUME_BUTTON_FRAME.h - 1 then
|
||||
return false
|
||||
end
|
||||
if x > rect.x2 - RESUME_BUTTON_FRAME.r + 1 or x < rect.x2 - RESUME_BUTTON_FRAME.r - RESUME_BUTTON_FRAME.w + 2 then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function InspectorOverlay:onInput(keys)
|
||||
if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then
|
||||
return false
|
||||
end
|
||||
if keys._MOUSE_L_DOWN and mouse_is_over_resume_button(self.frame_parent_rect) then
|
||||
return true
|
||||
elseif keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then
|
||||
self:reset()
|
||||
end
|
||||
return InspectorOverlay.super.onInput(self, keys)
|
||||
end
|
||||
|
||||
function InspectorOverlay:render(dc)
|
||||
if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then
|
||||
return
|
||||
end
|
||||
if reset_inspector_flag then
|
||||
self:reset()
|
||||
end
|
||||
InspectorOverlay.super.render(self, dc)
|
||||
end
|
||||
|
||||
return _ENV
|
@ -0,0 +1,347 @@
|
||||
local _ENV = mkmodule('plugins.buildingplan.itemselection')
|
||||
|
||||
local gui = require('gui')
|
||||
local pens = require('plugins.buildingplan.pens')
|
||||
local utils = require('utils')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
local uibs = df.global.buildreq
|
||||
local to_pen = dfhack.pen.parse
|
||||
|
||||
local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true}
|
||||
local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true}
|
||||
|
||||
-- map of building type -> {set=set of recently used, list=list of recently used}
|
||||
-- most recent entries are at the *end* of the list
|
||||
local recently_used = {}
|
||||
|
||||
local function sort_by_type(a, b)
|
||||
local ad, bd = a.data, b.data
|
||||
return ad.item_type < bd.item_type or
|
||||
(ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or
|
||||
(ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or
|
||||
(ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality)
|
||||
end
|
||||
|
||||
local function sort_by_recency(a, b)
|
||||
local tracker = recently_used[uibs.building_type]
|
||||
if not tracker then return sort_by_type(a, b) end
|
||||
local recent_a, recent_b = tracker.set[a.search_key], tracker.set[b.search_key]
|
||||
-- if they're both in the set, return the one with the greater index,
|
||||
-- indicating more recent
|
||||
if recent_a and recent_b then return recent_a > recent_b end
|
||||
if recent_a and not recent_b then return true end
|
||||
if not recent_a and recent_b then return false end
|
||||
return sort_by_type(a, b)
|
||||
end
|
||||
|
||||
local function sort_by_name(a, b)
|
||||
return a.search_key < b.search_key or
|
||||
(a.search_key == b.search_key and sort_by_type(a, b))
|
||||
end
|
||||
|
||||
local function sort_by_quantity(a, b)
|
||||
local ad, bd = a.data, b.data
|
||||
return ad.quantity > bd.quantity or
|
||||
(ad.quantity == bd.quantity and sort_by_type(a, b))
|
||||
end
|
||||
|
||||
ItemSelection = defclass(ItemSelection, widgets.Window)
|
||||
ItemSelection.ATTRS{
|
||||
frame_title='Choose items',
|
||||
frame={w=56, h=20, l=4, t=8},
|
||||
resizable=true,
|
||||
index=DEFAULT_NIL,
|
||||
desc=DEFAULT_NIL,
|
||||
quantity=DEFAULT_NIL,
|
||||
on_submit=DEFAULT_NIL,
|
||||
on_cancel=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
function ItemSelection:init()
|
||||
self.num_selected = 0
|
||||
self.selected_set = {}
|
||||
local plural = self.quantity == 1 and '' or 's'
|
||||
|
||||
self:addviews{
|
||||
widgets.Label{
|
||||
frame={t=0, l=0, r=10},
|
||||
text={
|
||||
self.desc,
|
||||
plural,
|
||||
NEWLINE,
|
||||
('Select up to %d item%s ('):format(self.quantity, plural),
|
||||
{text=function() return self.num_selected end},
|
||||
' selected)',
|
||||
},
|
||||
},
|
||||
widgets.Label{
|
||||
frame={r=0, w=9, t=0, h=3},
|
||||
text_pen=BUILD_TEXT_PEN,
|
||||
text_hpen=BUILD_TEXT_HPEN,
|
||||
text={
|
||||
' ', NEWLINE,
|
||||
' Build ', NEWLINE,
|
||||
' ',
|
||||
},
|
||||
on_click=self:callback('submit'),
|
||||
},
|
||||
widgets.FilteredList{
|
||||
view_id='flist',
|
||||
frame={t=3, l=0, r=0, b=4},
|
||||
case_sensitive=false,
|
||||
choices=self:get_choices(sort_by_recency),
|
||||
icon_width=2,
|
||||
on_submit=self:callback('toggle_group'),
|
||||
edit_on_char=function(ch) return ch:match('[%l -]') end,
|
||||
},
|
||||
widgets.CycleHotkeyLabel{
|
||||
frame={l=0, b=2},
|
||||
key='CUSTOM_SHIFT_R',
|
||||
label='Sort by:',
|
||||
options={
|
||||
{label='Recently used', value=sort_by_recency},
|
||||
{label='Name', value=sort_by_name},
|
||||
{label='Amount', value=sort_by_quantity},
|
||||
},
|
||||
on_change=self:callback('on_sort'),
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=0, b=1},
|
||||
key='SELECT',
|
||||
label='Use all/none',
|
||||
auto_width=true,
|
||||
on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end,
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=22, b=1},
|
||||
key='CUSTOM_SHIFT_B',
|
||||
label='Build',
|
||||
auto_width=true,
|
||||
on_activate=self:callback('submit'),
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=38, b=1},
|
||||
key='LEAVESCREEN',
|
||||
label='Go back',
|
||||
auto_width=true,
|
||||
on_activate=self:callback('on_cancel'),
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=0, b=0},
|
||||
key='KEYBOARD_CURSOR_RIGHT_FAST',
|
||||
key_sep=' : ',
|
||||
label='Use one',
|
||||
auto_width=true,
|
||||
on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end,
|
||||
},
|
||||
widgets.Label{
|
||||
frame={l=6, b=0, w=5},
|
||||
text_pen=COLOR_LIGHTGREEN,
|
||||
text='Right',
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={l=23, b=0},
|
||||
key='KEYBOARD_CURSOR_LEFT_FAST',
|
||||
key_sep=' : ',
|
||||
label='Use one fewer',
|
||||
auto_width=true,
|
||||
on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end,
|
||||
},
|
||||
widgets.Label{
|
||||
frame={l=29, b=0, w=4},
|
||||
text_pen=COLOR_LIGHTGREEN,
|
||||
text='Left',
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
-- resort and restore selection
|
||||
function ItemSelection:on_sort(sort_fn)
|
||||
local flist = self.subviews.flist
|
||||
local saved_filter = flist:getFilter()
|
||||
flist:setFilter('')
|
||||
flist:setChoices(self:get_choices(sort_fn), flist:getSelected())
|
||||
flist:setFilter(saved_filter)
|
||||
end
|
||||
|
||||
local function make_search_key(str)
|
||||
local out = ''
|
||||
for c in str:gmatch("[%w%s]") do
|
||||
out = out .. c
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function ItemSelection:get_choices(sort_fn)
|
||||
local item_ids = require('plugins.buildingplan').getAvailableItems(uibs.building_type,
|
||||
uibs.building_subtype, uibs.custom_type, self.index-1)
|
||||
local buckets = {}
|
||||
for _,item_id in ipairs(item_ids) do
|
||||
local item = df.item.find(item_id)
|
||||
if not item then goto continue end
|
||||
local desc = dfhack.items.getDescription(item, 0, true)
|
||||
if buckets[desc] then
|
||||
local bucket = buckets[desc]
|
||||
table.insert(bucket.data.item_ids, item_id)
|
||||
bucket.data.quantity = bucket.data.quantity + 1
|
||||
else
|
||||
local entry = {
|
||||
search_key=make_search_key(desc),
|
||||
icon=self:callback('get_entry_icon', item_id),
|
||||
data={
|
||||
item_ids={item_id},
|
||||
item_type=item:getType(),
|
||||
item_subtype=item:getSubtype(),
|
||||
quantity=1,
|
||||
quality=item:getQuality(),
|
||||
selected=0,
|
||||
},
|
||||
}
|
||||
buckets[desc] = entry
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
local choices = {}
|
||||
for desc,choice in pairs(buckets) do
|
||||
local data = choice.data
|
||||
choice.text = {
|
||||
{width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end},
|
||||
{gap=2, text=desc},
|
||||
}
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
table.sort(choices, sort_fn)
|
||||
return choices
|
||||
end
|
||||
|
||||
function ItemSelection:increment_group(idx, choice)
|
||||
local data = choice.data
|
||||
if self.quantity <= self.num_selected then return false end
|
||||
if data.selected >= data.quantity then return false end
|
||||
data.selected = data.selected + 1
|
||||
self.num_selected = self.num_selected + 1
|
||||
local item_id = data.item_ids[data.selected]
|
||||
self.selected_set[item_id] = true
|
||||
return true
|
||||
end
|
||||
|
||||
function ItemSelection:decrement_group(idx, choice)
|
||||
local data = choice.data
|
||||
if data.selected <= 0 then return false end
|
||||
local item_id = data.item_ids[data.selected]
|
||||
self.selected_set[item_id] = nil
|
||||
self.num_selected = self.num_selected - 1
|
||||
data.selected = data.selected - 1
|
||||
return true
|
||||
end
|
||||
|
||||
function ItemSelection:toggle_group(idx, choice)
|
||||
local data = choice.data
|
||||
if data.selected > 0 then
|
||||
while self:decrement_group(idx, choice) do end
|
||||
else
|
||||
while self:increment_group(idx, choice) do end
|
||||
end
|
||||
end
|
||||
|
||||
function ItemSelection:get_entry_icon(item_id)
|
||||
return self.selected_set[item_id] and pens.SELECTED_ITEM_PEN or nil
|
||||
end
|
||||
|
||||
local function track_recently_used(choices)
|
||||
-- use same set for all subtypes
|
||||
local tracker = ensure_key(recently_used, uibs.building_type)
|
||||
for _,choice in ipairs(choices) do
|
||||
local data = choice.data
|
||||
if data.selected <= 0 then goto continue end
|
||||
local key = choice.search_key
|
||||
local recent_set = ensure_key(tracker, 'set')
|
||||
local recent_list = ensure_key(tracker, 'list')
|
||||
if recent_set[key] then
|
||||
if recent_list[#recent_list] ~= key then
|
||||
for i,v in ipairs(recent_list) do
|
||||
if v == key then
|
||||
table.remove(recent_list, i)
|
||||
table.insert(recent_list, key)
|
||||
break
|
||||
end
|
||||
end
|
||||
tracker.set = utils.invert(recent_list)
|
||||
end
|
||||
else
|
||||
-- only keep most recent 10
|
||||
if #recent_list >= 10 then
|
||||
-- remove least recently used from list and set
|
||||
recent_set[table.remove(recent_list, 1)] = nil
|
||||
end
|
||||
table.insert(recent_list, key)
|
||||
recent_set[key] = #recent_list
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
function ItemSelection:submit()
|
||||
local selected_items = {}
|
||||
for item_id in pairs(self.selected_set) do
|
||||
table.insert(selected_items, item_id)
|
||||
end
|
||||
if #selected_items > 0 then
|
||||
track_recently_used(self.subviews.flist:getChoices())
|
||||
end
|
||||
self.on_submit(selected_items)
|
||||
end
|
||||
|
||||
function ItemSelection:onInput(keys)
|
||||
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
|
||||
self.on_cancel()
|
||||
return true
|
||||
elseif keys._MOUSE_L_DOWN then
|
||||
local list = self.subviews.flist.list
|
||||
local idx = list:getIdxUnderMouse()
|
||||
if idx then
|
||||
list:setSelected(idx)
|
||||
local modstate = dfhack.internal.getModstate()
|
||||
if modstate & 2 > 0 then -- ctrl
|
||||
local choice = list:getChoices()[idx]
|
||||
if modstate & 1 > 0 then -- shift
|
||||
self:decrement_group(idx, choice)
|
||||
else
|
||||
self:increment_group(idx, choice)
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return ItemSelection.super.onInput(self, keys)
|
||||
end
|
||||
|
||||
ItemSelectionScreen = defclass(ItemSelectionScreen, gui.ZScreen)
|
||||
ItemSelectionScreen.ATTRS {
|
||||
focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection',
|
||||
force_pause=true,
|
||||
pass_movement_keys=true,
|
||||
pass_pause=false,
|
||||
pass_mouse_clicks=false,
|
||||
defocusable=false,
|
||||
index=DEFAULT_NIL,
|
||||
desc=DEFAULT_NIL,
|
||||
quantity=DEFAULT_NIL,
|
||||
on_submit=DEFAULT_NIL,
|
||||
on_cancel=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
function ItemSelectionScreen:init()
|
||||
self:addviews{
|
||||
ItemSelection{
|
||||
index=self.index,
|
||||
desc=self.desc,
|
||||
quantity=self.quantity,
|
||||
on_submit=self.on_submit,
|
||||
on_cancel=self.on_cancel,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
return _ENV
|
@ -0,0 +1,31 @@
|
||||
local _ENV = mkmodule('plugins.buildingplan.pens')
|
||||
|
||||
GOOD_TILE_PEN, BAD_TILE_PEN = nil, nil
|
||||
VERT_TOP_PEN, VERT_MID_PEN, VERT_BOT_PEN = nil, nil, nil
|
||||
BUTTON_START_PEN, BUTTON_END_PEN = nil, nil
|
||||
SELECTED_ITEM_PEN = nil
|
||||
|
||||
local to_pen = dfhack.pen.parse
|
||||
|
||||
local tp = function(base, offset)
|
||||
if base == -1 then return nil end
|
||||
return base + offset
|
||||
end
|
||||
|
||||
function reload_pens()
|
||||
GOOD_TILE_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)}
|
||||
BAD_TILE_PEN = to_pen{ch='X', fg=COLOR_RED, tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)}
|
||||
|
||||
local tb_texpos = dfhack.textures.getThinBordersTexposStart()
|
||||
VERT_TOP_PEN = to_pen{tile=tp(tb_texpos, 10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK}
|
||||
VERT_MID_PEN = to_pen{tile=tp(tb_texpos, 4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK}
|
||||
VERT_BOT_PEN = to_pen{tile=tp(tb_texpos, 11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK}
|
||||
|
||||
local cp_texpos = dfhack.textures.getControlPanelTexposStart()
|
||||
BUTTON_START_PEN = to_pen{tile=tp(cp_texpos, 13), ch='[', fg=COLOR_YELLOW}
|
||||
BUTTON_END_PEN = to_pen{tile=tp(cp_texpos, 15), ch=']', fg=COLOR_YELLOW}
|
||||
SELECTED_ITEM_PEN = to_pen{tile=tp(cp_texpos, 9), ch=string.char(251), fg=COLOR_YELLOW}
|
||||
end
|
||||
reload_pens()
|
||||
|
||||
return _ENV
|
@ -0,0 +1,734 @@
|
||||
local _ENV = mkmodule('plugins.buildingplan.planneroverlay')
|
||||
|
||||
local itemselection = require('plugins.buildingplan.itemselection')
|
||||
local filterselection = require('plugins.buildingplan.filterselection')
|
||||
local gui = require('gui')
|
||||
local guidm = require('gui.dwarfmode')
|
||||
local overlay = require('plugins.overlay')
|
||||
local pens = require('plugins.buildingplan.pens')
|
||||
local utils = require('utils')
|
||||
local widgets = require('gui.widgets')
|
||||
require('dfhack.buildings')
|
||||
|
||||
local uibs = df.global.buildreq
|
||||
|
||||
reset_counts_flag = false
|
||||
|
||||
local function get_cur_filters()
|
||||
return dfhack.buildings.getFiltersByType({}, uibs.building_type,
|
||||
uibs.building_subtype, uibs.custom_type)
|
||||
end
|
||||
|
||||
local function is_choosing_area()
|
||||
return uibs.selection_pos.x >= 0
|
||||
end
|
||||
|
||||
local function get_cur_area_dims(placement_data)
|
||||
if not placement_data and not is_choosing_area() then return 1, 1, 1 end
|
||||
local selection_pos = placement_data and placement_data.p1 or uibs.selection_pos
|
||||
local pos = placement_data and placement_data.p2 or uibs.pos
|
||||
return math.abs(selection_pos.x - pos.x) + 1,
|
||||
math.abs(selection_pos.y - pos.y) + 1,
|
||||
math.abs(selection_pos.z - pos.z) + 1
|
||||
end
|
||||
|
||||
local function get_quantity(filter, hollow, placement_data)
|
||||
local quantity = filter.quantity or 1
|
||||
local dimx, dimy, dimz = get_cur_area_dims(placement_data)
|
||||
if quantity < 1 then
|
||||
return (((dimx * dimy) // 4) + 1) * dimz
|
||||
end
|
||||
if hollow and dimx > 2 and dimy > 2 then
|
||||
return quantity * (2*dimx + 2*dimy - 4) * dimz
|
||||
end
|
||||
return quantity * dimx * dimy * dimz
|
||||
end
|
||||
|
||||
local function cur_building_has_no_area()
|
||||
if uibs.building_type == df.building_type.Construction then return false end
|
||||
local filters = dfhack.buildings.getFiltersByType({},
|
||||
uibs.building_type, uibs.building_subtype, uibs.custom_type)
|
||||
-- this works because all variable-size buildings have either no item
|
||||
-- filters or a quantity of -1 for their first (and only) item
|
||||
return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0)
|
||||
end
|
||||
|
||||
local function is_plannable()
|
||||
return get_cur_filters() and
|
||||
not (uibs.building_type == df.building_type.Construction
|
||||
and uibs.building_subtype == df.construction_type.TrackNSEW)
|
||||
end
|
||||
|
||||
local function is_construction()
|
||||
return uibs.building_type == df.building_type.Construction
|
||||
end
|
||||
|
||||
local function is_stairs()
|
||||
return is_construction()
|
||||
and uibs.building_subtype == df.construction_type.UpDownStair
|
||||
end
|
||||
|
||||
local direction_panel_frame = {t=4, h=13, w=46, r=28}
|
||||
|
||||
local direction_panel_types = utils.invert{
|
||||
df.building_type.Bridge,
|
||||
df.building_type.ScrewPump,
|
||||
df.building_type.WaterWheel,
|
||||
df.building_type.AxleHorizontal,
|
||||
df.building_type.Rollers,
|
||||
}
|
||||
|
||||
local function has_direction_panel()
|
||||
return direction_panel_types[uibs.building_type]
|
||||
or (uibs.building_type == df.building_type.Trap
|
||||
and uibs.building_subtype == df.trap_type.TrackStop)
|
||||
end
|
||||
|
||||
local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28}
|
||||
|
||||
local function has_pressure_plate_panel()
|
||||
return uibs.building_type == df.building_type.Trap
|
||||
and uibs.building_subtype == df.trap_type.PressurePlate
|
||||
end
|
||||
|
||||
local function is_over_options_panel()
|
||||
local frame = nil
|
||||
if has_direction_panel() then
|
||||
frame = direction_panel_frame
|
||||
elseif has_pressure_plate_panel() then
|
||||
frame = pressure_plate_panel_frame
|
||||
else
|
||||
return false
|
||||
end
|
||||
local v = widgets.Widget{frame=frame}
|
||||
local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize())
|
||||
v:updateLayout(gui.ViewRect{rect=rect})
|
||||
return v:getMousePos()
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- ItemLine
|
||||
--
|
||||
|
||||
ItemLine = defclass(ItemLine, widgets.Panel)
|
||||
ItemLine.ATTRS{
|
||||
idx=DEFAULT_NIL,
|
||||
is_selected_fn=DEFAULT_NIL,
|
||||
is_hollow_fn=DEFAULT_NIL,
|
||||
on_select=DEFAULT_NIL,
|
||||
on_filter=DEFAULT_NIL,
|
||||
on_clear_filter=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
function ItemLine:init()
|
||||
self.frame.h = 1
|
||||
self.visible = function() return #get_cur_filters() >= self.idx end
|
||||
self:addviews{
|
||||
widgets.Label{
|
||||
frame={t=0, l=0},
|
||||
text='*',
|
||||
auto_width=true,
|
||||
visible=self.is_selected_fn,
|
||||
},
|
||||
widgets.Label{
|
||||
frame={t=0, l=25},
|
||||
text={
|
||||
{tile=pens.BUTTON_START_PEN},
|
||||
{gap=6, tile=pens.BUTTON_END_PEN},
|
||||
},
|
||||
auto_width=true,
|
||||
on_click=function() self.on_filter(self.idx) end,
|
||||
},
|
||||
widgets.Label{
|
||||
frame={t=0, l=33},
|
||||
text={
|
||||
{tile=pens.BUTTON_START_PEN},
|
||||
{gap=1, tile=pens.BUTTON_END_PEN},
|
||||
},
|
||||
auto_width=true,
|
||||
on_click=function() self.on_clear_filter(self.idx) end,
|
||||
},
|
||||
widgets.Label{
|
||||
frame={t=0, l=2},
|
||||
text={
|
||||
{width=21, text=self:callback('get_item_line_text')},
|
||||
{gap=3, text='filter', pen=COLOR_GREEN},
|
||||
{gap=2, text='x', pen=self:callback('get_x_pen')},
|
||||
{gap=3, text=function() return self.note end,
|
||||
pen=function() return self.note_pen end},
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function ItemLine:reset()
|
||||
self.desc = nil
|
||||
self.available = nil
|
||||
end
|
||||
|
||||
function ItemLine:onInput(keys)
|
||||
if keys._MOUSE_L_DOWN and self:getMousePos() then
|
||||
self.on_select(self.idx)
|
||||
end
|
||||
return ItemLine.super.onInput(self, keys)
|
||||
end
|
||||
|
||||
function ItemLine:get_x_pen()
|
||||
return require('plugins.buildingplan').hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) and
|
||||
COLOR_GREEN or COLOR_GREY
|
||||
end
|
||||
|
||||
function ItemLine:get_item_line_text()
|
||||
local idx = self.idx
|
||||
local filter = get_cur_filters()[idx]
|
||||
local quantity = get_quantity(filter, self.is_hollow_fn())
|
||||
|
||||
local buildingplan = require('plugins.buildingplan')
|
||||
self.desc = self.desc or buildingplan.get_desc(filter)
|
||||
|
||||
self.available = self.available or buildingplan.countAvailableItems(
|
||||
uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1)
|
||||
if self.available >= quantity then
|
||||
self.note_pen = COLOR_GREEN
|
||||
self.note = 'Available now'
|
||||
else
|
||||
self.note_pen = COLOR_YELLOW
|
||||
self.note = 'Will link later'
|
||||
end
|
||||
|
||||
return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's')
|
||||
end
|
||||
|
||||
function ItemLine:reduce_quantity(used_quantity)
|
||||
if not self.available then return end
|
||||
local filter = get_cur_filters()[self.idx]
|
||||
used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn())
|
||||
self.available = math.max(0, self.available - used_quantity)
|
||||
end
|
||||
|
||||
local function get_placement_errors()
|
||||
local out = ''
|
||||
for _,str in ipairs(uibs.errors) do
|
||||
if #out > 0 then out = out .. NEWLINE end
|
||||
out = out .. str.value
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- PlannerOverlay
|
||||
--
|
||||
|
||||
PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget)
|
||||
PlannerOverlay.ATTRS{
|
||||
default_pos={x=5,y=9},
|
||||
default_enabled=true,
|
||||
viewscreens='dwarfmode/Building/Placement',
|
||||
frame={w=56, h=20},
|
||||
}
|
||||
|
||||
function PlannerOverlay:init()
|
||||
self.selected = 1
|
||||
|
||||
local main_panel = widgets.Panel{
|
||||
view_id='main',
|
||||
frame={t=0, l=0, r=0, h=14},
|
||||
frame_style=gui.MEDIUM_FRAME,
|
||||
frame_background=gui.CLEAR_PEN,
|
||||
}
|
||||
|
||||
local function make_is_selected_fn(idx)
|
||||
return function() return self.selected == idx end
|
||||
end
|
||||
|
||||
local function on_select_fn(idx)
|
||||
self.selected = idx
|
||||
end
|
||||
|
||||
local function is_hollow_fn()
|
||||
return self.subviews.hollow:getOptionValue()
|
||||
end
|
||||
|
||||
local buildingplan = require('plugins.buildingplan')
|
||||
|
||||
main_panel:addviews{
|
||||
widgets.Label{
|
||||
frame={},
|
||||
auto_width=true,
|
||||
text='No items required.',
|
||||
visible=function() return #get_cur_filters() == 0 end,
|
||||
},
|
||||
ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1,
|
||||
is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn,
|
||||
on_select=on_select_fn, on_filter=self:callback('set_filter'),
|
||||
on_clear_filter=self:callback('clear_filter')},
|
||||
ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2,
|
||||
is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn,
|
||||
on_select=on_select_fn, on_filter=self:callback('set_filter'),
|
||||
on_clear_filter=self:callback('clear_filter')},
|
||||
ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3,
|
||||
is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn,
|
||||
on_select=on_select_fn, on_filter=self:callback('set_filter'),
|
||||
on_clear_filter=self:callback('clear_filter')},
|
||||
ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4,
|
||||
is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn,
|
||||
on_select=on_select_fn, on_filter=self:callback('set_filter'),
|
||||
on_clear_filter=self:callback('clear_filter')},
|
||||
widgets.CycleHotkeyLabel{
|
||||
view_id='hollow',
|
||||
frame={t=3, l=4},
|
||||
key='CUSTOM_H',
|
||||
label='Hollow area:',
|
||||
visible=is_construction,
|
||||
options={
|
||||
{label='No', value=false},
|
||||
{label='Yes', value=true},
|
||||
},
|
||||
},
|
||||
widgets.CycleHotkeyLabel{
|
||||
view_id='stairs_top_subtype',
|
||||
frame={t=4, l=4},
|
||||
key='CUSTOM_R',
|
||||
label='Top Stair Type: ',
|
||||
visible=is_stairs,
|
||||
options={
|
||||
{label='Auto', value='auto'},
|
||||
{label='UpDown', value=df.construction_type.UpDownStair},
|
||||
{label='Down', value=df.construction_type.DownStair},
|
||||
},
|
||||
},
|
||||
widgets.CycleHotkeyLabel {
|
||||
view_id='stairs_bottom_subtype',
|
||||
frame={t=5, l=4},
|
||||
key='CUSTOM_B',
|
||||
label='Bottom Stair Type: ',
|
||||
visible=is_stairs,
|
||||
options={
|
||||
{label='Auto', value='auto'},
|
||||
{label='UpDown', value=df.construction_type.UpDownStair},
|
||||
{label='Up', value=df.construction_type.UpStair},
|
||||
},
|
||||
},
|
||||
widgets.Label{
|
||||
frame={b=3, l=17},
|
||||
text={
|
||||
'Selected area: ',
|
||||
{text=function()
|
||||
return ('%dx%dx%d'):format(get_cur_area_dims(self.saved_placement))
|
||||
end
|
||||
},
|
||||
},
|
||||
visible=function()
|
||||
return not cur_building_has_no_area() and (self.saved_placement or is_choosing_area())
|
||||
end,
|
||||
},
|
||||
widgets.Panel{
|
||||
visible=function() return #get_cur_filters() > 0 end,
|
||||
subviews={
|
||||
widgets.HotkeyLabel{
|
||||
frame={b=1, l=0},
|
||||
key='STRING_A042',
|
||||
auto_width=true,
|
||||
enabled=function() return #get_cur_filters() > 1 end,
|
||||
on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end,
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={b=1, l=1},
|
||||
key='STRING_A047',
|
||||
label='Prev/next item',
|
||||
auto_width=true,
|
||||
enabled=function() return #get_cur_filters() > 1 end,
|
||||
on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end,
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={b=1, l=21},
|
||||
key='CUSTOM_F',
|
||||
label='Set filter',
|
||||
auto_width=true,
|
||||
on_activate=function() self:set_filter(self.selected) end,
|
||||
},
|
||||
widgets.HotkeyLabel{
|
||||
frame={b=1, l=37},
|
||||
key='CUSTOM_X',
|
||||
label='Clear filter',
|
||||
auto_width=true,
|
||||
on_activate=function() self:clear_filter(self.selected) end,
|
||||
enabled=function()
|
||||
return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1)
|
||||
end
|
||||
},
|
||||
widgets.CycleHotkeyLabel{
|
||||
view_id='choose',
|
||||
frame={b=0, l=0, w=25},
|
||||
key='CUSTOM_I',
|
||||
label='Choose from items:',
|
||||
options={{label='Yes', value=true},
|
||||
{label='No', value=false}},
|
||||
initial_option=false,
|
||||
enabled=function()
|
||||
for idx = 1,4 do
|
||||
if (self.subviews['item'..idx].available or 0) > 0 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end,
|
||||
},
|
||||
widgets.CycleHotkeyLabel{
|
||||
view_id='safety',
|
||||
frame={b=0, l=29, w=25},
|
||||
key='CUSTOM_G',
|
||||
label='Building safety:',
|
||||
options={
|
||||
{label='Any', value=0},
|
||||
{label='Magma', value=2, pen=COLOR_RED},
|
||||
{label='Fire', value=1, pen=COLOR_LIGHTRED},
|
||||
},
|
||||
initial_option=0,
|
||||
on_change=function(heat)
|
||||
buildingplan.setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat)
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local error_panel = widgets.ResizingPanel{
|
||||
view_id='errors',
|
||||
frame={t=14, l=0, r=0},
|
||||
frame_style=gui.MEDIUM_FRAME,
|
||||
frame_background=gui.CLEAR_PEN,
|
||||
}
|
||||
|
||||
error_panel:addviews{
|
||||
widgets.WrappedLabel{
|
||||
frame={t=0, l=0, r=0},
|
||||
text_pen=COLOR_LIGHTRED,
|
||||
text_to_wrap=get_placement_errors,
|
||||
visible=function() return #uibs.errors > 0 end,
|
||||
},
|
||||
widgets.Label{
|
||||
frame={t=0, l=0, r=0},
|
||||
text_pen=COLOR_GREEN,
|
||||
text='OK to build',
|
||||
visible=function() return #uibs.errors == 0 end,
|
||||
},
|
||||
}
|
||||
|
||||
self:addviews{
|
||||
main_panel,
|
||||
error_panel,
|
||||
}
|
||||
end
|
||||
|
||||
function PlannerOverlay:reset()
|
||||
self.subviews.item1:reset()
|
||||
self.subviews.item2:reset()
|
||||
self.subviews.item3:reset()
|
||||
self.subviews.item4:reset()
|
||||
reset_counts_flag = false
|
||||
end
|
||||
|
||||
function PlannerOverlay:set_filter(idx)
|
||||
filterselection.FilterSelectionScreen{index=idx, desc=require('plugins.buildingplan').get_desc(get_cur_filters()[idx])}:show()
|
||||
end
|
||||
|
||||
function PlannerOverlay:clear_filter(idx)
|
||||
clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1)
|
||||
end
|
||||
|
||||
local function get_placement_data()
|
||||
local pos = uibs.pos
|
||||
local direction = uibs.direction
|
||||
local width, height, depth = get_cur_area_dims()
|
||||
local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize(
|
||||
width, height, uibs.building_type, uibs.building_subtype,
|
||||
uibs.custom_type, direction)
|
||||
-- get the upper-left corner of the building/area at min z-level
|
||||
local has_selection = is_choosing_area()
|
||||
local start_pos = xyz2pos(
|
||||
has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2,
|
||||
has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2,
|
||||
has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z
|
||||
)
|
||||
if uibs.building_type == df.building_type.ScrewPump then
|
||||
if direction == df.screw_pump_direction.FromSouth then
|
||||
start_pos.y = start_pos.y + 1
|
||||
elseif direction == df.screw_pump_direction.FromEast then
|
||||
start_pos.x = start_pos.x + 1
|
||||
end
|
||||
end
|
||||
local min_x, max_x = start_pos.x, start_pos.x
|
||||
local min_y, max_y = start_pos.y, start_pos.y
|
||||
local min_z, max_z = start_pos.z, start_pos.z
|
||||
if adjusted_width == 1 and adjusted_height == 1
|
||||
and (width > 1 or height > 1 or depth > 1) then
|
||||
max_x = min_x + width - 1
|
||||
max_y = min_y + height - 1
|
||||
max_z = math.max(uibs.selection_pos.z, pos.z)
|
||||
end
|
||||
return {
|
||||
p1=xyz2pos(min_x, min_y, min_z),
|
||||
p2=xyz2pos(max_x, max_y, max_z),
|
||||
width=adjusted_width,
|
||||
height=adjusted_height
|
||||
}
|
||||
end
|
||||
|
||||
function PlannerOverlay:save_placement()
|
||||
self.saved_placement = get_placement_data()
|
||||
if (uibs.selection_pos:isValid()) then
|
||||
self.saved_selection_pos_valid = true
|
||||
self.saved_selection_pos = copyall(uibs.selection_pos)
|
||||
self.saved_pos = copyall(uibs.pos)
|
||||
uibs.selection_pos:clear()
|
||||
else
|
||||
self.saved_selection_pos = copyall(self.saved_placement.p1)
|
||||
self.saved_pos = copyall(self.saved_placement.p2)
|
||||
self.saved_pos.x = self.saved_pos.x + self.saved_placement.width - 1
|
||||
self.saved_pos.y = self.saved_pos.y + self.saved_placement.height - 1
|
||||
end
|
||||
end
|
||||
|
||||
function PlannerOverlay:restore_placement()
|
||||
if self.saved_selection_pos_valid then
|
||||
uibs.selection_pos = self.saved_selection_pos
|
||||
self.saved_selection_pos_valid = nil
|
||||
else
|
||||
uibs.selection_pos:clear()
|
||||
end
|
||||
self.saved_selection_pos = nil
|
||||
self.saved_pos = nil
|
||||
local placement_data = self.saved_placement
|
||||
self.saved_placement = nil
|
||||
return placement_data
|
||||
end
|
||||
|
||||
function PlannerOverlay:onInput(keys)
|
||||
if not is_plannable() then return false end
|
||||
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
|
||||
if uibs.selection_pos:isValid() then
|
||||
uibs.selection_pos:clear()
|
||||
return true
|
||||
end
|
||||
self.selected = 1
|
||||
self.subviews.hollow:setOption(false)
|
||||
self.subviews.choose:setOption(false)
|
||||
self:reset()
|
||||
reset_counts_flag = true
|
||||
return false
|
||||
end
|
||||
if PlannerOverlay.super.onInput(self, keys) then
|
||||
return true
|
||||
end
|
||||
if keys._MOUSE_L_DOWN then
|
||||
if is_over_options_panel() then return false end
|
||||
local detect_rect = copyall(self.frame_rect)
|
||||
detect_rect.height = self.subviews.main.frame_rect.height +
|
||||
self.subviews.errors.frame_rect.height
|
||||
detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1
|
||||
if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect})
|
||||
or self.subviews.errors:getMousePos() then
|
||||
return true
|
||||
end
|
||||
if not is_construction() and #uibs.errors > 0 then return true end
|
||||
if dfhack.gui.getMousePos() then
|
||||
if is_choosing_area() or cur_building_has_no_area() then
|
||||
local filters = get_cur_filters()
|
||||
local num_filters = #filters
|
||||
local choose = self.subviews.choose
|
||||
if choose.enabled() and choose:getOptionValue() then
|
||||
self:save_placement()
|
||||
local is_hollow = self.subviews.hollow:getOptionValue()
|
||||
local chosen_items, active_screens = {}, {}
|
||||
local pending = num_filters
|
||||
df.global.game.main_interface.bottom_mode_selected = -1
|
||||
for idx = num_filters,1,-1 do
|
||||
chosen_items[idx] = {}
|
||||
if (self.subviews['item'..idx].available or 0) > 0 then
|
||||
local filter = filters[idx]
|
||||
active_screens[idx] = itemselection.ItemSelectionScreen{
|
||||
index=idx,
|
||||
desc=require('plugins.buildingplan').get_desc(filter),
|
||||
quantity=get_quantity(filter, is_hollow,
|
||||
self.saved_placement),
|
||||
on_submit=function(items)
|
||||
chosen_items[idx] = items
|
||||
active_screens[idx]:dismiss()
|
||||
active_screens[idx] = nil
|
||||
pending = pending - 1
|
||||
if pending == 0 then
|
||||
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
|
||||
self:place_building(self:restore_placement(), chosen_items)
|
||||
end
|
||||
end,
|
||||
on_cancel=function()
|
||||
for i,scr in pairs(active_screens) do
|
||||
scr:dismiss()
|
||||
end
|
||||
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
|
||||
self:restore_placement()
|
||||
end,
|
||||
}:show()
|
||||
else
|
||||
pending = pending - 1
|
||||
end
|
||||
end
|
||||
else
|
||||
self:place_building(get_placement_data())
|
||||
end
|
||||
return true
|
||||
elseif not is_choosing_area() then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return keys._MOUSE_L or keys.SELECT
|
||||
end
|
||||
|
||||
function PlannerOverlay:render(dc)
|
||||
if not is_plannable() then return end
|
||||
self.subviews.errors:updateLayout()
|
||||
PlannerOverlay.super.render(self, dc)
|
||||
end
|
||||
|
||||
local ONE_BY_ONE = xy2pos(1, 1)
|
||||
|
||||
function PlannerOverlay:onRenderFrame(dc, rect)
|
||||
PlannerOverlay.super.onRenderFrame(self, dc, rect)
|
||||
|
||||
if reset_counts_flag then
|
||||
self:reset()
|
||||
self.subviews.safety:setOption(require('plugins.buildingplan').getHeatSafetyFilter(
|
||||
uibs.building_type, uibs.building_subtype, uibs.custom_type))
|
||||
end
|
||||
|
||||
local selection_pos = self.saved_selection_pos or uibs.selection_pos
|
||||
if not selection_pos or selection_pos.x < 0 then return end
|
||||
|
||||
local pos = self.saved_pos or uibs.pos
|
||||
local bounds = {
|
||||
x1 = math.max(0, math.min(selection_pos.x, pos.x)),
|
||||
x2 = math.min(df.global.world.map.x_count-1, math.max(selection_pos.x, pos.x)),
|
||||
y1 = math.max(0, math.min(selection_pos.y, pos.y)),
|
||||
y2 = math.min(df.global.world.map.y_count-1, math.max(selection_pos.y, pos.y)),
|
||||
}
|
||||
|
||||
local hollow = self.subviews.hollow:getOptionValue()
|
||||
local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN
|
||||
|
||||
local get_pen_fn = is_construction() and function(pos)
|
||||
return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and GOOD_PEN or BAD_PEN
|
||||
end or function()
|
||||
return default_pen
|
||||
end
|
||||
|
||||
local function get_overlay_pen(pos)
|
||||
if not hollow then return get_pen_fn(pos) end
|
||||
if pos.x == bounds.x1 or pos.x == bounds.x2 or
|
||||
pos.y == bounds.y1 or pos.y == bounds.y2 then
|
||||
return get_pen_fn(pos)
|
||||
end
|
||||
return gui.TRANSPARENT_PEN
|
||||
end
|
||||
|
||||
guidm.renderMapOverlay(get_overlay_pen, bounds)
|
||||
end
|
||||
|
||||
function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2)
|
||||
local subtype = uibs.building_subtype
|
||||
if pos.z == corner1.z then
|
||||
local opt = self.subviews.stairs_bottom_subtype:getOptionValue()
|
||||
if opt == 'auto' then
|
||||
local tt = dfhack.maps.getTileType(pos)
|
||||
local shape = df.tiletype.attrs[tt].shape
|
||||
if shape ~= df.tiletype_shape.STAIR_DOWN then
|
||||
subtype = df.construction_type.UpStair
|
||||
end
|
||||
else
|
||||
subtype = opt
|
||||
end
|
||||
elseif pos.z == corner2.z then
|
||||
local opt = self.subviews.stairs_top_subtype:getOptionValue()
|
||||
if opt == 'auto' then
|
||||
local tt = dfhack.maps.getTileType(pos)
|
||||
local shape = df.tiletype.attrs[tt].shape
|
||||
if shape ~= df.tiletype_shape.STAIR_UP then
|
||||
subtype = df.construction_type.DownStair
|
||||
end
|
||||
else
|
||||
subtype = opt
|
||||
end
|
||||
end
|
||||
return subtype
|
||||
end
|
||||
|
||||
function PlannerOverlay:place_building(placement_data, chosen_items)
|
||||
local p1, p2 = placement_data.p1, placement_data.p2
|
||||
local blds = {}
|
||||
local hollow = self.subviews.hollow:getOptionValue()
|
||||
local subtype = uibs.building_subtype
|
||||
for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do
|
||||
if hollow and x ~= p1.x and x ~= p2.x and y ~= p1.y and y ~= p2.y then
|
||||
goto continue
|
||||
end
|
||||
local pos = xyz2pos(x, y, z)
|
||||
if is_stairs() then
|
||||
subtype = self:get_stairs_subtype(pos, p1, p2)
|
||||
end
|
||||
local bld, err = dfhack.buildings.constructBuilding{pos=pos,
|
||||
type=uibs.building_type, subtype=subtype, custom=uibs.custom_type,
|
||||
width=placement_data.width, height=placement_data.height,
|
||||
direction=uibs.direction}
|
||||
if err then
|
||||
-- it's ok if some buildings fail to build
|
||||
goto continue
|
||||
end
|
||||
-- assign fields for the types that need them. we can't pass them all in
|
||||
-- to the call to constructBuilding since attempting to assign unrelated
|
||||
-- fields to building types that don't support them causes errors.
|
||||
for k,v in pairs(bld) do
|
||||
if k == 'friction' then bld.friction = uibs.friction end
|
||||
if k == 'use_dump' then bld.use_dump = uibs.use_dump end
|
||||
if k == 'dump_x_shift' then bld.dump_x_shift = uibs.dump_x_shift end
|
||||
if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_shift end
|
||||
if k == 'speed' then bld.speed = uibs.speed end
|
||||
end
|
||||
table.insert(blds, bld)
|
||||
::continue::
|
||||
end end end
|
||||
local used_quantity = is_construction() and #blds or false
|
||||
self.subviews.item1:reduce_quantity(used_quantity)
|
||||
self.subviews.item2:reduce_quantity(used_quantity)
|
||||
self.subviews.item3:reduce_quantity(used_quantity)
|
||||
self.subviews.item4:reduce_quantity(used_quantity)
|
||||
local buildingplan = require('plugins.buildingplan')
|
||||
for _,bld in ipairs(blds) do
|
||||
-- attach chosen items and reduce job_item quantity
|
||||
if chosen_items then
|
||||
local job = bld.jobs[0]
|
||||
local jitems = job.job_items
|
||||
for idx=1,#get_cur_filters() do
|
||||
local item_ids = chosen_items[idx]
|
||||
while jitems[idx-1].quantity > 0 and #item_ids > 0 do
|
||||
local item_id = item_ids[#item_ids]
|
||||
local item = df.item.find(item_id)
|
||||
if not item then
|
||||
dfhack.printerr(('item no longer available: %d'):format(item_id))
|
||||
break
|
||||
end
|
||||
if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then
|
||||
dfhack.printerr(('cannot attach item: %d'):format(item_id))
|
||||
break
|
||||
end
|
||||
jitems[idx-1].quantity = jitems[idx-1].quantity - 1
|
||||
item_ids[#item_ids] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
buildingplan.addPlannedBuilding(bld)
|
||||
end
|
||||
buildingplan.scheduleCycle()
|
||||
uibs.selection_pos:clear()
|
||||
end
|
||||
|
||||
return _ENV
|
Loading…
Reference in New Issue