Support setting the target area for the siege engine.

develop
Alexander Gavrilov 2012-09-08 13:46:02 +04:00
parent 325e294af2
commit bfa6ed3e08
6 changed files with 390 additions and 75 deletions

@ -46,7 +46,7 @@ function getPanelLayout()
end end
function getCursorPos() function getCursorPos()
if g_cursor ~= -30000 then if g_cursor.x ~= -30000 then
return copyall(g_cursor) return copyall(g_cursor)
end end
end end
@ -167,6 +167,18 @@ function Viewport:isVisible(target,gap)
return self:isVisibleXY(target,gap) and target.z == self.z return self:isVisibleXY(target,gap) and target.z == self.z
end end
function Viewport:tileToScreen(coord)
return xyz2pos(coord.x - self.x1, coord.y - self.y1, coord.z - self.z)
end
function Viewport:getCenter()
return xyz2pos(
math.floor((self.x2+self.x1)/2),
math.floor((self.y2+self.y1)/2),
self.z
)
end
function Viewport:centerOn(target) function Viewport:centerOn(target)
return self:clip( return self:clip(
target.x - math.floor(self.width/2), target.x - math.floor(self.width/2),
@ -253,16 +265,23 @@ function DwarfOverlay:getViewport(old_vp)
end end
end end
function DwarfOverlay:moveCursorTo(cursor,viewport) function DwarfOverlay:moveCursorTo(cursor,viewport,gap)
setCursorPos(cursor) setCursorPos(cursor)
self:getViewport(viewport):reveal(cursor, 5, 0, 10):set() self:zoomViewportTo(cursor,viewport,gap)
end
function DwarfOverlay:zoomViewportTo(target, viewport, gap)
if gap and self:getViewport():isVisible(target, gap) then
return
end
self:getViewport(viewport):reveal(target, 5, 0, 10):set()
end end
function DwarfOverlay:selectBuilding(building,cursor,viewport) function DwarfOverlay:selectBuilding(building,cursor,viewport,gap)
cursor = cursor or utils.getBuildingCenter(building) cursor = cursor or utils.getBuildingCenter(building)
df.global.world.selected_building = building df.global.world.selected_building = building
self:moveCursorTo(cursor, viewport) self:moveCursorTo(cursor, viewport, gap)
end end
function DwarfOverlay:propagateMoveKeys(keys) function DwarfOverlay:propagateMoveKeys(keys)

@ -173,10 +173,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
else if (id == &df::building_trapst::_identity) else if (id == &df::building_trapst::_identity)
{ {
auto trap = (df::building_trapst*)selected; auto trap = (df::building_trapst*)selected;
if (trap->trap_type == trap_type::Lever) { focus += "/" + enum_item_key(trap->trap_type);
focus += "/Lever"; if (trap->trap_type == trap_type::Lever)
jobs = true; jobs = true;
}
} }
else if (ui_building_in_assign && *ui_building_in_assign && else if (ui_building_in_assign && *ui_building_in_assign &&
ui_building_assign_type && ui_building_assign_units && ui_building_assign_type && ui_building_assign_units &&
@ -189,6 +188,8 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
focus += unit ? "/Unit" : "/None"; focus += unit ? "/Unit" : "/None";
} }
} }
else
focus += "/" + enum_item_key(selected->getType());
if (jobs) if (jobs)
{ {

@ -47,6 +47,65 @@ using Screen::Pen;
DFHACK_PLUGIN("siege-engine"); DFHACK_PLUGIN("siege-engine");
/*
* Misc. utils
*/
typedef std::pair<df::coord, df::coord> coord_range;
static void set_range(coord_range *target, df::coord p1, df::coord p2)
{
if (!p1.isValid() || !p2.isValid())
{
*target = coord_range();
}
else
{
target->first.x = std::min(p1.x, p2.x);
target->first.y = std::min(p1.y, p2.y);
target->first.z = std::min(p1.z, p2.z);
target->second.x = std::max(p1.x, p2.x);
target->second.y = std::max(p1.y, p2.y);
target->second.z = std::max(p1.z, p2.z);
}
}
static bool is_range_valid(const coord_range &target)
{
return target.first.isValid() && target.second.isValid();
}
static bool is_in_range(const coord_range &target, df::coord pos)
{
return target.first.isValid() && target.second.isValid() &&
target.first.x <= pos.x && pos.x <= target.second.x &&
target.first.y <= pos.y && pos.y <= target.second.y &&
target.first.z <= pos.z && pos.z <= target.second.z;
}
static std::pair<int, int> get_engine_range(df::building_siegeenginest *bld)
{
if (bld->type == siegeengine_type::Ballista)
return std::make_pair(0, 200);
else
return std::make_pair(30, 100);
}
static void orient_engine(df::building_siegeenginest *bld, df::coord target)
{
int dx = target.x - bld->centerx;
int dy = target.y - bld->centery;
if (abs(dx) > abs(dy))
bld->facing = (dx > 0) ?
df::building_siegeenginest::Right :
df::building_siegeenginest::Left;
else
bld->facing = (dy > 0) ?
df::building_siegeenginest::Down :
df::building_siegeenginest::Up;
}
/* /*
* Configuration management * Configuration management
*/ */
@ -55,17 +114,10 @@ static bool enable_plugin();
struct EngineInfo { struct EngineInfo {
int id; int id;
df::coord target_min, target_max; coord_range target;
bool hasTarget() { bool hasTarget() { return is_range_valid(target); }
return target_min.isValid() && target_max.isValid(); bool onTarget(df::coord pos) { return is_in_range(target, pos); }
}
bool onTarget(df::coord pos) {
return hasTarget() &&
target_min.x <= pos.x && pos.x <= target_max.x &&
target_min.y <= pos.y && pos.y <= target_max.y &&
target_min.z <= pos.z && pos.z <= target_max.z;
}
}; };
static std::map<df::building*, EngineInfo> engines; static std::map<df::building*, EngineInfo> engines;
@ -98,8 +150,8 @@ static void load_engines()
{ {
auto engine = find_engine(df::building::find(it->ival(0)), true); auto engine = find_engine(df::building::find(it->ival(0)), true);
if (!engine) continue; if (!engine) continue;
engine->target_min = df::coord(it->ival(1), it->ival(2), it->ival(3)); engine->target.first = df::coord(it->ival(1), it->ival(2), it->ival(3));
engine->target_max = df::coord(it->ival(4), it->ival(5), it->ival(6)); engine->target.second = df::coord(it->ival(4), it->ival(5), it->ival(6));
} }
} }
@ -109,10 +161,10 @@ static int getTargetArea(lua_State *L)
if (!bld) luaL_argerror(L, 1, "null building"); if (!bld) luaL_argerror(L, 1, "null building");
auto engine = find_engine(bld); auto engine = find_engine(bld);
if (engine && engine->target_min.isValid()) if (engine && engine->hasTarget())
{ {
Lua::Push(L, engine->target_min); Lua::Push(L, engine->target.first);
Lua::Push(L, engine->target_max); Lua::Push(L, engine->target.second);
} }
else else
{ {
@ -128,7 +180,7 @@ static void clearTargetArea(df::building_siegeenginest *bld)
CHECK_NULL_POINTER(bld); CHECK_NULL_POINTER(bld);
if (auto engine = find_engine(bld)) if (auto engine = find_engine(bld))
engine->target_min = engine->target_max = df::coord(); engine->target = coord_range();
auto pworld = Core::getInstance().getWorld(); auto pworld = Core::getInstance().getWorld();
auto key = stl_sprintf("siege-engine/target/%d", bld->id); auto key = stl_sprintf("siege-engine/target/%d", bld->id);
@ -151,13 +203,18 @@ static bool setTargetArea(df::building_siegeenginest *bld, df::coord target_min,
auto engine = find_engine(bld, true); auto engine = find_engine(bld, true);
set_range(&engine->target, target_min, target_max);
entry.ival(0) = bld->id; entry.ival(0) = bld->id;
entry.ival(1) = engine->target_min.x = std::min(target_min.x, target_max.x); entry.ival(1) = engine->target.first.x;
entry.ival(2) = engine->target_min.y = std::min(target_min.y, target_max.y); entry.ival(2) = engine->target.first.y;
entry.ival(3) = engine->target_min.z = std::min(target_min.z, target_max.z); entry.ival(3) = engine->target.first.z;
entry.ival(4) = engine->target_max.x = std::max(target_min.x, target_max.x); entry.ival(4) = engine->target.second.x;
entry.ival(5) = engine->target_max.y = std::max(target_min.y, target_max.y); entry.ival(5) = engine->target.second.y;
entry.ival(6) = engine->target_max.z = std::max(target_min.z, target_max.z); entry.ival(6) = engine->target.second.z;
df::coord sum = target_min + target_max;
orient_engine(bld, df::coord(sum.x/2, sum.y/2, sum.z/2));
return true; return true;
} }
@ -267,38 +324,45 @@ struct PathMetrics {
} }
}; };
void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d ltop, df::coord2d size) static std::string getTileStatus(df::building_siegeenginest *bld, df::coord tile_pos)
{ {
CHECK_NULL_POINTER(bld); df::coord origin(bld->centerx, bld->centery, bld->z);
auto fire_range = get_engine_range(bld);
df::coord origin = df::coord(bld->centerx, bld->centery, bld->z); ProjectilePath path(origin, tile_pos);
PathMetrics raytrace(path, tile_pos);
auto engine = find_engine(bld); if (raytrace.hits())
int min_distance, max_distance;
if (bld->type == siegeengine_type::Ballista)
{ {
min_distance = 0; if (raytrace.goal_step >= fire_range.first &&
max_distance = 200; raytrace.goal_step <= fire_range.second)
return "ok";
else
return "out_of_range";
} }
else else
{ return "blocked";
min_distance = 30; }
max_distance = 100;
} static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d ltop, df::coord2d size)
{
CHECK_NULL_POINTER(bld);
df::coord cursor = Gui::getCursorPos(); df::coord origin(bld->centerx, bld->centery, bld->z);
coord_range building_rect(
df::coord(bld->x1, bld->y1, bld->z),
df::coord(bld->x2, bld->y2, bld->z)
);
auto engine = find_engine(bld);
auto fire_range = get_engine_range(bld);
for (int x = 0; x < size.x; x++) for (int x = 0; x < size.x; x++)
{ {
for (int y = 0; y < size.y; y++) for (int y = 0; y < size.y; y++)
{ {
df::coord tile_pos = view + df::coord(x,y,0); df::coord tile_pos = view + df::coord(x,y,0);
if (tile_pos == cursor) if (is_in_range(building_rect, tile_pos))
continue;
if (tile_pos.z == bld->z &&
tile_pos.x >= bld->x1 && tile_pos.x <= bld->x2 &&
tile_pos.y >= bld->y1 && tile_pos.y <= bld->y2)
continue; continue;
Pen cur_tile = Screen::readTile(ltop.x+x, ltop.y+y); Pen cur_tile = Screen::readTile(ltop.x+x, ltop.y+y);
@ -306,22 +370,13 @@ void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d
continue; continue;
ProjectilePath path(origin, tile_pos); ProjectilePath path(origin, tile_pos);
if (path.speed.z != 0 && abs(path.speed.z) != path.divisor) {
path.divisor *= 20;
path.speed.x *= 20;
path.speed.y *= 20;
path.speed.z *= 20;
path.speed.z += 9;
}
PathMetrics raytrace(path, tile_pos); PathMetrics raytrace(path, tile_pos);
int color; int color;
if (raytrace.hits()) if (raytrace.hits())
{ {
if (raytrace.goal_step >= min_distance && if (raytrace.goal_step >= fire_range.first &&
raytrace.goal_step <= max_distance) raytrace.goal_step <= fire_range.second)
color = COLOR_GREEN; color = COLOR_GREEN;
else else
color = COLOR_CYAN; color = COLOR_CYAN;
@ -357,6 +412,7 @@ void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d
DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(clearTargetArea), DFHACK_LUA_FUNCTION(clearTargetArea),
DFHACK_LUA_FUNCTION(setTargetArea), DFHACK_LUA_FUNCTION(setTargetArea),
DFHACK_LUA_FUNCTION(getTileStatus),
DFHACK_LUA_FUNCTION(paintAimScreen), DFHACK_LUA_FUNCTION(paintAimScreen),
DFHACK_LUA_END DFHACK_LUA_END
}; };

@ -0,0 +1,3 @@
-- For killing bugged out gui script screens.
dfhack.screen.dismiss(dfhack.gui.getCurViewscreen())

@ -122,7 +122,7 @@ function MechanismList:onInput(keys)
end end
end end
if not string.find(dfhack.gui.getCurFocus(), 'dwarfmode/QueryBuilding/Some') then if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
qerror("This script requires the main dwarfmode view in 'q' mode") qerror("This script requires the main dwarfmode view in 'q' mode")
end end

@ -6,6 +6,11 @@ local guidm = require 'gui.dwarfmode'
local dlg = require 'gui.dialogs' local dlg = require 'gui.dialogs'
local plugin = require 'plugins.siege-engine' local plugin = require 'plugins.siege-engine'
local wmap = df.global.world.map
-- Globals kept between script calls
last_target_min = last_target_min or nil
last_target_max = last_target_max or nil
SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay) SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
@ -15,9 +20,17 @@ function SiegeEngine:init(building)
self:init_fields{ self:init_fields{
building = building, building = building,
center = utils.getBuildingCenter(building), center = utils.getBuildingCenter(building),
links = {}, selected = 1 links = {}, selected = 1,
} }
guidm.MenuOverlay.init(self) guidm.MenuOverlay.init(self)
self.mode_main = {
render = self:callback 'onRenderBody_main',
input = self:callback 'onInput_main',
}
self.mode_aim = {
render = self:callback 'onRenderBody_aim',
input = self:callback 'onInput_aim',
}
return self return self
end end
@ -26,40 +39,263 @@ function SiegeEngine:onShow()
self.old_cursor = guidm.getCursorPos() self.old_cursor = guidm.getCursorPos()
self.old_viewport = self:getViewport() self.old_viewport = self:getViewport()
self.mode = self.mode_main
self:showCursor(false)
end end
function SiegeEngine:onDestroy() function SiegeEngine:onDestroy()
guidm.setCursorPos(self.old_cursor) self:selectBuilding(self.building, self.old_cursor, self.old_viewport, 10)
self:getViewport(self.old_viewport):set()
end end
function SiegeEngine:onRenderBody(dc) function SiegeEngine:showCursor(enable)
dc:clear() local cursor = guidm.getCursorPos()
dc:seek(1,1):string(utils.getBuildingName(self.building), COLOR_WHITE):newline() 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)
for z = cz,target_max.z do
if plugin.getTileStatus(self.building, xyz2pos(cx,cy,z)) ~= 'blocked' then
cz = z
break
end
end
self:centerViewOn(xyz2pos(cx,cy,cz))
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 view = self:getViewport()
local map = self.df_layout.map local map = self.df_layout.map
local map_dc = gui.Painter.new(map)
plugin.paintAimScreen( plugin.paintAimScreen(
self.building, view:getPos(), self.building, view:getPos(),
xy2pos(map.x1, map.y1), view:getSize() xy2pos(map.x1, map.y1), view:getSize()
) )
dc:newline():newline(1):pen(COLOR_WHITE) if target_min and math.floor(dfhack.getTickCount()/500) % 2 == 0 then
dc:string("Esc", COLOR_LIGHTGREEN):string(": Back, ") paint_target_grid(map_dc, view, self.center, target_min, target_max)
dc:string("Enter", COLOR_LIGHTGREEN):string(": Switch") 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: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
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: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_Z then
self:zoomToTarget()
elseif keys.CUSTOM_X then
plugin.clearTargetArea(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" },
}
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(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 end
function SiegeEngine:onInput(keys) function SiegeEngine:onInput(keys)
if keys.LEAVESCREEN then if self.mode.input(keys) then
--
elseif keys.CUSTOM_C then
self:centerViewOn(self.center)
elseif keys.LEAVESCREEN then
self:dismiss() self:dismiss()
elseif self:simulateCursorMovement(keys, self.center) then
return
end end
end end
if not string.find(dfhack.gui.getCurFocus(), 'dwarfmode/QueryBuilding/Some') then if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/SiegeEngine') then
qerror("This script requires the main dwarfmode view in 'q' mode") qerror("This script requires a siege engine selected in 'q' mode")
end end
local building = df.global.world.selected_building local building = df.global.world.selected_building