diff options
Diffstat (limited to 'scripts/gui/siege-engine.lua')
| -rw-r--r-- | scripts/gui/siege-engine.lua | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua new file mode 100644 index 00000000..f460bb21 --- /dev/null +++ b/scripts/gui/siege-engine.lua @@ -0,0 +1,494 @@ +-- Front-end for the siege engine plugin. + +local utils = require 'utils' +local gui = require 'gui' +local guidm = require 'gui.dwarfmode' +local dlg = require 'gui.dialogs' + +local plugin = require 'plugins.siege-engine' +local wmap = df.global.world.map + +local LEGENDARY = df.skill_rating.Legendary + +-- Globals kept between script calls +last_target_min = last_target_min or nil +last_target_max = last_target_max or nil + +local item_choices = { + { caption = 'boulders (default)', item_type = df.item_type.BOULDER }, + { caption = 'blocks', item_type = df.item_type.BLOCKS }, + { caption = 'weapons', item_type = df.item_type.WEAPON }, + { caption = 'trap components', item_type = df.item_type.TRAPCOMP }, + { caption = 'bins', item_type = df.item_type.BIN }, + { caption = 'barrels', item_type = df.item_type.BARREL }, + { caption = 'cages', item_type = df.item_type.CAGE }, + { caption = 'anything', item_type = -1 }, +} + +local item_choice_idx = {} +for i,v in ipairs(item_choices) do + item_choice_idx[v.item_type] = i +end + +SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay) + +SiegeEngine.focus_path = 'siege-engine' + +SiegeEngine.ATTRS{ building = DEFAULT_NIL } + +function SiegeEngine:init() + self:assign{ + center = utils.getBuildingCenter(self.building), + selected_pile = 1, + mode_main = { + render = self:callback 'onRenderBody_main', + input = self:callback 'onInput_main', + }, + mode_aim = { + render = self:callback 'onRenderBody_aim', + input = self:callback 'onInput_aim', + }, + mode_pile = { + render = self:callback 'onRenderBody_pile', + input = self:callback 'onInput_pile', + } + } +end + +function SiegeEngine:onShow() + SiegeEngine.super.onShow(self) + + self.old_cursor = guidm.getCursorPos() + self.old_viewport = self:getViewport() + + self.mode = self.mode_main + self:showCursor(false) +end + +function SiegeEngine:onDestroy() + if self.save_profile then + plugin.saveWorkshopProfile(self.building) + end + if not self.no_select_building then + self:selectBuilding(self.building, self.old_cursor, self.old_viewport, 10) + end +end + +function SiegeEngine:onGetSelectedBuilding() + return df.global.world.selected_building +end + +function SiegeEngine:showCursor(enable) + local cursor = guidm.getCursorPos() + if cursor and not enable then + self.cursor = cursor + self.target_select_first = nil + guidm.clearCursorPos() + elseif not cursor and enable then + local view = self:getViewport() + cursor = self.cursor + if not cursor or not view:isVisible(cursor) then + cursor = view:getCenter() + end + self.cursor = nil + guidm.setCursorPos(cursor) + end +end + +function SiegeEngine:centerViewOn(pos) + local cursor = guidm.getCursorPos() + if cursor then + guidm.setCursorPos(pos) + else + self.cursor = pos + end + self:getViewport():centerOn(pos):set() +end + +function SiegeEngine:zoomToTarget() + local target_min, target_max = plugin.getTargetArea(self.building) + if target_min then + local cx = math.floor((target_min.x + target_max.x)/2) + local cy = math.floor((target_min.y + target_max.y)/2) + local cz = math.floor((target_min.z + target_max.z)/2) + local pos = plugin.adjustToTarget(self.building, xyz2pos(cx,cy,cz)) + self:centerViewOn(pos) + end +end + +function paint_target_grid(dc, view, origin, p1, p2) + local r1, sz, r2 = guidm.getSelectionRange(p1, p2) + + if view.z < r1.z or view.z > r2.z then + return + end + + local p1 = view:tileToScreen(r1) + local p2 = view:tileToScreen(r2) + local org = view:tileToScreen(origin) + dc:pen{ fg = COLOR_CYAN, bg = COLOR_CYAN, ch = '+', bold = true } + + -- Frame + dc:fill(p1.x,p1.y,p1.x,p2.y) + dc:fill(p1.x,p1.y,p2.x,p1.y) + dc:fill(p2.x,p1.y,p2.x,p2.y) + dc:fill(p1.x,p2.y,p2.x,p2.y) + + -- Grid + local gxmin = org.x+10*math.ceil((p1.x-org.x)/10) + local gxmax = org.x+10*math.floor((p2.x-org.x)/10) + local gymin = org.y+10*math.ceil((p1.y-org.y)/10) + local gymax = org.y+10*math.floor((p2.y-org.y)/10) + for x = gxmin,gxmax,10 do + for y = gymin,gymax,10 do + dc:fill(p1.x,y,p2.x,y) + dc:fill(x,p1.y,x,p2.y) + end + end +end + +function SiegeEngine:renderTargetView(target_min, target_max) + local view = self:getViewport() + local map = self.df_layout.map + local map_dc = gui.Painter.new(map) + + plugin.paintAimScreen( + self.building, view:getPos(), + xy2pos(map.x1, map.y1), view:getSize() + ) + + if target_min and math.floor(dfhack.getTickCount()/500) % 2 == 0 then + paint_target_grid(map_dc, view, self.center, target_min, target_max) + end + + local cursor = guidm.getCursorPos() + if cursor then + local cx, cy, cz = pos2xyz(view:tileToScreen(cursor)) + if cz == 0 then + map_dc:seek(cx,cy):char('X', COLOR_YELLOW) + end + end +end + +function SiegeEngine:scrollPiles(delta) + local links = plugin.getStockpileLinks(self.building) + if links then + self.selected_pile = 1+(self.selected_pile+delta-1) % #links + return links[self.selected_pile] + end +end + +function SiegeEngine:renderStockpiles(dc, links, nlines) + local idx = (self.selected_pile-1) % #links + local page = math.floor(idx/nlines) + for i = page*nlines,math.min(#links,(page+1)*nlines)-1 do + local color = COLOR_BROWN + if i == idx then + color = COLOR_YELLOW + end + dc:newline(2):string(utils.getBuildingName(links[i+1]), color) + end +end + +function SiegeEngine:onRenderBody_main(dc) + dc:newline(1):pen(COLOR_WHITE):string("Target: ") + + local target_min, target_max = plugin.getTargetArea(self.building) + if target_min then + dc:string( + (target_max.x-target_min.x+1).."x".. + (target_max.y-target_min.y+1).."x".. + (target_max.z-target_min.z+1).." Rect" + ) + else + dc:string("None (default)") + end + + dc:newline(3):string("r",COLOR_LIGHTGREEN):string(": Rectangle") + if last_target_min then + dc:string(", "):string("p",COLOR_LIGHTGREEN):string(": Paste") + end + dc:newline(3) + if target_min then + dc:string("x",COLOR_LIGHTGREEN):string(": Clear, ") + dc:string("z",COLOR_LIGHTGREEN):string(": Zoom") + end + + dc:newline():newline(1) + if self.building.type == df.siegeengine_type.Ballista then + dc:string("Uses ballista arrows") + else + local item = plugin.getAmmoItem(self.building) + dc:string("u",COLOR_LIGHTGREEN):string(": Use ") + if item_choice_idx[item] then + dc:string(item_choices[item_choice_idx[item]].caption) + else + dc:string(df.item_type[item]) + end + end + + dc:newline():newline(1) + dc:string("t",COLOR_LIGHTGREEN):string(": Take from stockpile"):newline(3) + local links = plugin.getStockpileLinks(self.building) + local bottom = dc.height - 5 + if links then + dc:string("d",COLOR_LIGHTGREEN):string(": Delete, ") + dc:string("o",COLOR_LIGHTGREEN):string(": Zoom"):newline() + self:renderStockpiles(dc, links, bottom-2-dc:localY()) + dc:newline():newline() + end + + local prof = self.building:getWorkshopProfile() or {} + dc:seek(1,math.max(dc:localY(),19)):string('ghjk',COLOR_LIGHTGREEN)dc:string(': ') + dc:string(df.skill_rating.attrs[prof.min_level or 0].caption):string('-') + dc:string(df.skill_rating.attrs[math.min(LEGENDARY,prof.max_level or 3000)].caption) + dc:newline():newline() + + if self.target_select_first then + self:renderTargetView(self.target_select_first, guidm.getCursorPos()) + else + self:renderTargetView(target_min, target_max) + end +end + +function SiegeEngine:setTargetArea(p1, p2) + self.target_select_first = nil + + if not plugin.setTargetArea(self.building, p1, p2) then + dlg.showMessage( + 'Set Target Area', + 'Could not set the target area', COLOR_LIGHTRED + ) + else + last_target_min = p1 + last_target_max = p2 + end +end + +function SiegeEngine:setAmmoItem(choice) + if self.building.type == df.siegeengine_type.Ballista then + return + end + + if not plugin.setAmmoItem(self.building, choice.item_type) then + dlg.showMessage( + 'Set Ammo Item', + 'Could not set the ammo item', COLOR_LIGHTRED + ) + end +end + +function SiegeEngine:onInput_main(keys) + if keys.CUSTOM_R then + self:showCursor(true) + self.target_select_first = nil + self.mode = self.mode_aim + elseif keys.CUSTOM_P and last_target_min then + self:setTargetArea(last_target_min, last_target_max) + elseif keys.CUSTOM_U then + local item = plugin.getAmmoItem(self.building) + local idx = 1 + (item_choice_idx[item] or 0) % #item_choices + self:setAmmoItem(item_choices[idx]) + elseif keys.CUSTOM_Z then + self:zoomToTarget() + elseif keys.CUSTOM_X then + plugin.clearTargetArea(self.building) + elseif keys.SECONDSCROLL_UP then + self:scrollPiles(-1) + elseif keys.SECONDSCROLL_DOWN then + self:scrollPiles(1) + elseif keys.CUSTOM_D then + local pile = self:scrollPiles(0) + if pile then + plugin.removeStockpileLink(self.building, pile) + end + elseif keys.CUSTOM_O then + local pile = self:scrollPiles(0) + if pile then + self:centerViewOn(utils.getBuildingCenter(pile)) + end + elseif keys.CUSTOM_T then + self:showCursor(true) + self.mode = self.mode_pile + self:sendInputToParent('CURSOR_DOWN_Z') + self:sendInputToParent('CURSOR_UP_Z') + elseif keys.CUSTOM_G then + local prof = plugin.saveWorkshopProfile(self.building) + prof.min_level = math.max(0, prof.min_level-1) + plugin.saveWorkshopProfile(self.building) + elseif keys.CUSTOM_H then + local prof = plugin.saveWorkshopProfile(self.building) + prof.min_level = math.min(LEGENDARY, prof.min_level+1) + plugin.saveWorkshopProfile(self.building) + elseif keys.CUSTOM_J then + local prof = plugin.saveWorkshopProfile(self.building) + prof.max_level = math.max(0, math.min(LEGENDARY,prof.max_level)-1) + plugin.saveWorkshopProfile(self.building) + elseif keys.CUSTOM_K then + local prof = plugin.saveWorkshopProfile(self.building) + prof.max_level = math.min(LEGENDARY, prof.max_level+1) + if prof.max_level >= LEGENDARY then prof.max_level = 3000 end + plugin.saveWorkshopProfile(self.building) + elseif self:simulateViewScroll(keys) then + self.cursor = nil + else + return false + end + return true +end + +local status_table = { + ok = { pen = COLOR_GREEN, msg = "Target accessible" }, + out_of_range = { pen = COLOR_CYAN, msg = "Target out of range" }, + blocked = { pen = COLOR_RED, msg = "Target obstructed" }, + semi_blocked = { pen = COLOR_BROWN, msg = "Partially obstructed" }, +} + +function SiegeEngine:onRenderBody_aim(dc) + local cursor = guidm.getCursorPos() + local first = self.target_select_first + + dc:newline(1):string('Select target rectangle'):newline() + + local info = status_table[plugin.getTileStatus(self.building, cursor)] + if info then + dc:newline(2):string(info.msg, info.pen) + else + dc:newline(2):string('ERROR', COLOR_RED) + end + + dc:newline():newline(1):string("Enter",COLOR_LIGHTGREEN) + if first then + dc:string(": Finish rectangle") + else + dc:string(": Start rectangle") + end + dc:newline() + + local target_min, target_max = plugin.getTargetArea(self.building) + if target_min then + dc:newline(1):string("z",COLOR_LIGHTGREEN):string(": Zoom to current target") + end + + if first then + self:renderTargetView(first, cursor) + else + local target_min, target_max = plugin.getTargetArea(self.building) + self:renderTargetView(target_min, target_max) + end +end + +function SiegeEngine:onInput_aim(keys) + if keys.SELECT then + local cursor = guidm.getCursorPos() + if self.target_select_first then + self:setTargetArea(self.target_select_first, cursor) + + self.mode = self.mode_main + self:showCursor(false) + else + self.target_select_first = cursor + end + elseif keys.CUSTOM_Z then + self:zoomToTarget() + elseif keys.LEAVESCREEN then + self.mode = self.mode_main + self:showCursor(false) + elseif self:simulateCursorMovement(keys) then + self.cursor = nil + else + return false + end + return true +end + +function SiegeEngine:onRenderBody_pile(dc) + dc:newline(1):string('Select pile to take from'):newline():newline(2) + + local sel = df.global.world.selected_building + + if df.building_stockpilest:is_instance(sel) then + dc:string(utils.getBuildingName(sel), COLOR_GREEN):newline():newline(1) + + if plugin.isLinkedToPile(self.building, sel) then + dc:string("Already taking from here"):newline():newline(2) + dc:string("d", COLOR_LIGHTGREEN):string(": Delete link") + else + dc:string("Enter",COLOR_LIGHTGREEN):string(": Take from this pile") + end + elseif sel then + dc:string(utils.getBuildingName(sel), COLOR_DARKGREY) + dc:newline():newline(1) + dc:string("Not a stockpile",COLOR_LIGHTRED) + else + dc:string("No building selected", COLOR_DARKGREY) + end +end + +function SiegeEngine:onInput_pile(keys) + if keys.SELECT then + local sel = df.global.world.selected_building + if df.building_stockpilest:is_instance(sel) + and not plugin.isLinkedToPile(self.building, sel) then + plugin.addStockpileLink(self.building, sel) + + df.global.world.selected_building = self.building + self.mode = self.mode_main + self:showCursor(false) + end + elseif keys.CUSTOM_D then + local sel = df.global.world.selected_building + if df.building_stockpilest:is_instance(sel) then + plugin.removeStockpileLink(self.building, sel) + end + elseif keys.LEAVESCREEN then + df.global.world.selected_building = self.building + self.mode = self.mode_main + self:showCursor(false) + elseif self:propagateMoveKeys(keys) then + -- + else + return false + end + return true +end + +function SiegeEngine:onRenderBody(dc) + dc:clear() + dc:seek(1,1):pen(COLOR_WHITE):string(utils.getBuildingName(self.building)):newline() + + self.mode.render(dc) + + dc:seek(1, math.max(dc:localY(), 21)):pen(COLOR_WHITE) + dc:string("ESC", COLOR_LIGHTGREEN):string(": Back, ") + dc:string("c", COLOR_LIGHTGREEN):string(": Recenter") +end + +function SiegeEngine:onInput(keys) + if self.mode.input(keys) then + -- + elseif keys.CUSTOM_C then + self:centerViewOn(self.center) + elseif keys.LEAVESCREEN then + self:dismiss() + elseif keys.LEAVESCREEN_ALL then + self:dismiss() + self.no_select_building = true + guidm.clearCursorPos() + df.global.ui.main.mode = df.ui_sidebar_mode.Default + df.global.world.selected_building = nil + end +end + +if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/SiegeEngine') then + qerror("This script requires a siege engine selected in 'q' mode") +end + +local building = df.global.world.selected_building + +if not df.building_siegeenginest:is_instance(building) then + qerror("A siege engine must be selected") +end + +local list = SiegeEngine{ building = building } +list:show() |
