Skip to content

Commit

Permalink
Merge pull request #759 from myk002/myk_trade
Browse files Browse the repository at this point in the history
[caravan] advanced trade dialogs
  • Loading branch information
myk002 authored Jul 6, 2023
2 parents 64aff9c + 4e21bcc commit 775c5ee
Show file tree
Hide file tree
Showing 6 changed files with 2,123 additions and 308 deletions.
316 changes: 11 additions & 305 deletions caravan.lua
Original file line number Diff line number Diff line change
@@ -1,317 +1,23 @@
-- Adjusts properties of caravans and provides overlay for enhanced trading
-- Adjusts properties of caravans and provides overlays for enhanced trading
--@ module = true

-- TODO: the category checkbox that indicates whether all items in the category
-- are selected can be incorrect after the overlay adjusts the container
-- selection. the state is in trade.current_type_a_flag, but figuring out which
-- index to modify is non-trivial.

local gui = require('gui')
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')

trader_selected_state = trader_selected_state or {}
broker_selected_state = broker_selected_state or {}
handle_ctrl_click_on_render = handle_ctrl_click_on_render or false
handle_shift_click_on_render = handle_shift_click_on_render or false
local movegoods = reqscript('internal/caravan/movegoods')
local trade = reqscript('internal/caravan/trade')
local tradeagreement = reqscript('internal/caravan/tradeagreement')

dfhack.onStateChange.caravanTradeOverlay = function(code)
if code == SC_WORLD_UNLOADED then
trader_selected_state = {}
broker_selected_state = {}
handle_ctrl_click_on_render = false
handle_shift_click_on_render = false
end
end

local GOODFLAG = {
UNCONTAINED_UNSELECTED = 0,
UNCONTAINED_SELECTED = 1,
CONTAINED_UNSELECTED = 2,
CONTAINED_SELECTED = 3,
CONTAINER_COLLAPSED_UNSELECTED = 4,
CONTAINER_COLLAPSED_SELECTED = 5,
}

local trade = df.global.game.main_interface.trade

local MARGIN_HEIGHT = 26 -- screen height *other* than the list

function set_height(list_index, delta)
trade.i_height[list_index] = trade.i_height[list_index] + delta
if delta >= 0 then return end
_,screen_height = dfhack.screen.getWindowSize()
-- list only increments in three tiles at a time
local page_height = ((screen_height - MARGIN_HEIGHT) // 3) * 3
trade.scroll_position_item[list_index] = math.max(0,
math.min(trade.scroll_position_item[list_index],
trade.i_height[list_index] - page_height))
end

function select_shift_clicked_container_items(new_state, old_state, list_index)
-- if ctrl is also held, collapse the container too
local also_collapse = dfhack.internal.getModifiers().ctrl
local collapsed_item_count, collapsing_container, in_container = 0, false, false
for k, goodflag in ipairs(new_state) do
if in_container then
if goodflag <= GOODFLAG.UNCONTAINED_SELECTED
or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then
break
end

new_state[k] = GOODFLAG.CONTAINED_SELECTED

if collapsing_container then
collapsed_item_count = collapsed_item_count + 1
end
goto continue
end

if goodflag == old_state[k] then goto continue end
local is_container = df.item_binst:is_instance(trade.good[list_index][k])
if not is_container then goto continue end

-- deselect the container itself
if also_collapse or
old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED or
old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_SELECTED then
collapsing_container = goodflag == GOODFLAG.UNCONTAINED_SELECTED
new_state[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED
else
new_state[k] = GOODFLAG.UNCONTAINED_UNSELECTED
end
in_container = true

::continue::
end

if collapsed_item_count > 0 then
set_height(list_index, collapsed_item_count * -3)
end
end

local CTRL_CLICK_STATE_MAP = {
[GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED,
[GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED,
[GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED,
[GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED,
}

-- collapses uncollapsed containers and restores the selection state for the container
-- and contained items
function toggle_ctrl_clicked_containers(new_state, old_state, list_index)
local toggled_item_count, in_container, is_collapsing = 0, false, false
for k, goodflag in ipairs(new_state) do
if in_container then
if goodflag <= GOODFLAG.UNCONTAINED_SELECTED
or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then
break
end
toggled_item_count = toggled_item_count + 1
new_state[k] = old_state[k]
goto continue
end

if goodflag == old_state[k] then goto continue end
local is_contained = goodflag == GOODFLAG.CONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINED_SELECTED
if is_contained then goto continue end
local is_container = df.item_binst:is_instance(trade.good[list_index][k])
if not is_container then goto continue end

new_state[k] = CTRL_CLICK_STATE_MAP[old_state[k]]
in_container = true
is_collapsing = goodflag == GOODFLAG.UNCONTAINED_UNSELECTED or goodflag == GOODFLAG.UNCONTAINED_SELECTED

::continue::
end

if toggled_item_count > 0 then
set_height(list_index, toggled_item_count * 3 * (is_collapsing and -1 or 1))
end
end

function collapseTypes(types_list, list_index)
local type_on_count = 0

for k in ipairs(types_list) do
local type_on = trade.current_type_a_on[list_index][k]
if type_on then
type_on_count = type_on_count + 1
end
types_list[k] = false
trade.trader_selected_state = {}
trade.broker_selected_state = {}
trade.handle_ctrl_click_on_render = false
trade.handle_shift_click_on_render = false
end

trade.i_height[list_index] = type_on_count * 3
trade.scroll_position_item[list_index] = 0
end

function collapseAllTypes()
collapseTypes(trade.current_type_a_expanded[0], 0)
collapseTypes(trade.current_type_a_expanded[1], 1)
end

function collapseContainers(item_list, list_index)
local num_items_collapsed = 0
for k, goodflag in ipairs(item_list) do
if goodflag == GOODFLAG.CONTAINED_UNSELECTED
or goodflag == GOODFLAG.CONTAINED_SELECTED then
goto continue
end

local item = trade.good[list_index][k]
local is_container = df.item_binst:is_instance(item)
if not is_container then goto continue end

local collapsed_this_container = false
if goodflag == GOODFLAG.UNCONTAINED_SELECTED then
item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED
collapsed_this_container = true
elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then
item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED
collapsed_this_container = true
end

if collapsed_this_container then
num_items_collapsed = num_items_collapsed + #dfhack.items.getContainedItems(item)
end
::continue::
end

if num_items_collapsed > 0 then
set_height(list_index, num_items_collapsed * -3)
end
end

function collapseAllContainers()
collapseContainers(trade.goodflag[0], 0)
collapseContainers(trade.goodflag[1], 1)
end

function collapseEverything()
collapseAllContainers()
collapseAllTypes()
end

function copyGoodflagState()
trader_selected_state = copyall(trade.goodflag[0])
broker_selected_state = copyall(trade.goodflag[1])
end

CaravanTradeOverlay = defclass(CaravanTradeOverlay, overlay.OverlayWidget)
CaravanTradeOverlay.ATTRS{
default_pos={x=-3,y=-12},
default_enabled=true,
viewscreens='dwarfmode/Trade',
frame={w=27, h=13},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}

function CaravanTradeOverlay:init()
self:addviews{
widgets.Label{
frame={t=0, l=0},
text={
{text='Shift+Click checkbox', pen=COLOR_LIGHTGREEN}, ':',
NEWLINE,
' select items inside bin',
},
},
widgets.Label{
frame={t=3, l=0},
text={
{text='Ctrl+Click checkbox', pen=COLOR_LIGHTGREEN}, ':',
NEWLINE,
' collapse/expand bin',
},
},
widgets.HotkeyLabel{
frame={t=6, l=0},
label='collapse bins',
key='CUSTOM_CTRL_C',
on_activate=collapseAllContainers,
},
widgets.HotkeyLabel{
frame={t=7, l=0},
label='collapse all',
key='CUSTOM_CTRL_X',
on_activate=collapseEverything,
},
widgets.Label{
frame={t=9, l=0},
text = 'Shift+Scroll',
text_pen=COLOR_LIGHTGREEN,
},
widgets.Label{
frame={t=9, l=12},
text = ': fast scroll',
},
}
end

-- do our alterations *after* the vanilla response to the click has registered. otherwise
-- it's very difficult to figure out which item has been clicked
function CaravanTradeOverlay:onRenderBody(dc)
if handle_shift_click_on_render then
handle_shift_click_on_render = false
select_shift_clicked_container_items(trade.goodflag[0], trader_selected_state, 0)
select_shift_clicked_container_items(trade.goodflag[1], broker_selected_state, 1)
elseif handle_ctrl_click_on_render then
handle_ctrl_click_on_render = false
toggle_ctrl_clicked_containers(trade.goodflag[0], trader_selected_state, 0)
toggle_ctrl_clicked_containers(trade.goodflag[1], broker_selected_state, 1)
end
end

function CaravanTradeOverlay:onInput(keys)
if CaravanTradeOverlay.super.onInput(self, keys) then return true end

if keys._MOUSE_L_DOWN then
if dfhack.internal.getModifiers().shift then
handle_shift_click_on_render = true
copyGoodflagState()
elseif dfhack.internal.getModifiers().ctrl then
handle_ctrl_click_on_render = true
copyGoodflagState()
end
end
end

DiplomacyOverlay = defclass(DiplomacyOverlay, overlay.OverlayWidget)
DiplomacyOverlay.ATTRS{
default_pos={x=45,y=-6},
default_enabled=true,
viewscreens='dwarfmode/Diplomacy/Requests',
frame={w=25, h=3},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}

local diplomacy = df.global.game.main_interface.diplomacy
local function diplomacy_toggle_cat()
local priority_idx = diplomacy.taking_requests_tablist[diplomacy.taking_requests_selected_tab]
local priority = diplomacy.environment.meeting.sell_requests.priority[priority_idx]
if #priority == 0 then return end
local target_val = priority[0] == 0 and 4 or 0
for i in ipairs(priority) do
priority[i] = target_val
end
end

function DiplomacyOverlay:init()
self:addviews{
widgets.HotkeyLabel{
frame={t=0, l=0},
label='Select all/none',
key='CUSTOM_CTRL_A',
on_activate=diplomacy_toggle_cat,
},
}
end

OVERLAY_WIDGETS = {
trade=CaravanTradeOverlay,
diplomacy=DiplomacyOverlay,
trade=trade.TradeOverlay,
tradeagreement=tradeagreement.TradeAgreementOverlay,
movegoods=movegoods.MoveGoodsOverlay,
}

INTERESTING_FLAGS = {
Expand Down
34 changes: 31 additions & 3 deletions docs/caravan.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,14 @@ Examples
``caravan unload``
Fix a caravan that got spooked by wildlife and refuses to fully unload.

Overlay
-------
Overlays
--------

Additional functionality is provided on the various trade-related screens via
`overlay` widgets.

Additional functionality is provided when the trade screen is open via an `overlay` widget:
Trade screen
````````````

- ``Shift+Click checkbox``: Select all items inside a bin without selecting the
bin itself
Expand All @@ -66,3 +70,27 @@ vanilla game when you hold shift while scrolling (this works everywhere).
You can turn the overlay on and off in `gui/control-panel`, or you can
reposition it to your liking with `gui/overlay`. The overlay is named
``caravan.tradeScreenExtension``.

Bring item to depot
```````````````````

When the trade depot is selected, a button appears to bring up the DFHack
enhanced move trade goods screen. You'll get a searchable, sortable list of all
your tradeable items, with hotkeys to quickly select or deselect all visible
items.

There are filter sliders for selecting items of various condition levels and
quality. For example, you can quickly trade all your tattered, frayed, and worn
clothing by setting the condition slider to include from tattered to worn, then
hitting Ctrl-V to select all.

Click on an item and shift-click on a second item to toggle all items between
the two that you clicked on. If the one that you shift-clicked on was selected,
the range of items will be deselected. If the one you shift-clicked on was not
selected, then the range of items will be selected.

Trade agreement
```````````````

A small panel is shown with a hotkey (``Ctrl-A``) for selecting all/none in the
currently shown category.
Loading

0 comments on commit 775c5ee

Please sign in to comment.