Implement a slightly more sensible aiming AI in siege engine.

develop
Alexander Gavrilov 2012-09-17 14:45:22 +04:00
parent c1e20c6f05
commit f2fde21b10
2 changed files with 270 additions and 14 deletions

@ -112,7 +112,7 @@ static bool is_in_range(const coord_range &target, df::coord pos)
static std::pair<int, int> get_engine_range(df::building_siegeenginest *bld)
{
if (bld->type == siegeengine_type::Ballista)
return std::make_pair(0, 200);
return std::make_pair(1, 200);
else
return std::make_pair(30, 100);
}
@ -291,7 +291,7 @@ static EngineInfo *find_engine(df::building *bld, bool create = false)
);
obj->is_catapult = (ebld->type == siegeengine_type::Catapult);
obj->proj_speed = 2;
obj->hit_delay = 3;
obj->hit_delay = obj->is_catapult ? 2 : -1;
obj->fire_range = get_engine_range(ebld);
obj->ammo_vector_id = job_item_vector_id::BOULDER;
@ -1107,6 +1107,9 @@ struct UnitPath {
float time = unit->counters.job_counter+0.5f;
float speed = Units::computeMovementSpeed(unit)/100.0f;
if (unit->counters.unconscious > 0)
time += unit->counters.unconscious;
for (size_t i = 0; i < upath.size(); i++)
{
df::coord new_pos = upath[i];
@ -1282,6 +1285,74 @@ static int proposeUnitHits(lua_State *L)
return 1;
}
static int computeNearbyWeight(lua_State *L)
{
auto engine = find_engine(L, 1);
luaL_checktype(L, 2, LUA_TTABLE);
luaL_checktype(L, 3, LUA_TTABLE);
const char *fname = luaL_optstring(L, 4, "nearby_weight");
std::vector<UnitPath*> units;
std::vector<float> weights;
lua_pushnil(L);
while (lua_next(L, 3))
{
df::unit *unit;
if (lua_isnumber(L, -2))
unit = df::unit::find(lua_tointeger(L, -2));
else
unit = Lua::CheckDFObject<df::unit>(L, -2);
if (!unit)
continue;
units.push_back(UnitPath::get(unit));
weights.push_back(lua_tonumber(L, -1));
lua_pop(L, 1);
}
lua_pushnil(L);
while (lua_next(L, 2))
{
Lua::StackUnwinder frame(L, 1);
lua_getfield(L, frame[1], "unit");
df::unit *unit = Lua::CheckDFObject<df::unit>(L, -1);
lua_getfield(L, frame[1], "time");
float time = luaL_checknumber(L, lua_gettop(L));
df::coord pos;
lua_getfield(L, frame[1], "pos");
if (lua_isnil(L, -1))
{
if (!unit) luaL_error(L, "either unit or pos is required");
pos = UnitPath::get(unit)->posAtTime(time);
}
else
Lua::CheckDFAssign(L, &pos, -1);
float sum = 0.0f;
for (size_t i = 0; i < units.size(); i++)
{
if (units[i]->unit == unit)
continue;
auto diff = units[i]->posAtTime(time) - pos;
float dist = 1 + sqrtf(diff.x*diff.x + diff.y*diff.y + diff.z*diff.z);
sum += weights[i]/(dist*dist);
}
lua_pushnumber(L, sum);
lua_setfield(L, frame[1], fname);
}
return 0;
}
/*
* Projectile hook
*/
@ -1698,6 +1769,7 @@ DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(traceUnitPath),
DFHACK_LUA_COMMAND(unitPosAtTime),
DFHACK_LUA_COMMAND(proposeUnitHits),
DFHACK_LUA_COMMAND(computeNearbyWeight),
DFHACK_LUA_END
};

@ -8,37 +8,221 @@ local _ENV = mkmodule('plugins.siege-engine')
* clearTargetArea(building)
* setTargetArea(building, point1, point2) -> true/false
--]]
* isLinkedToPile(building,pile) -> true/false
* getStockpileLinks(building) -> {pile}
* addStockpileLink(building,pile) -> true/false
* removeStockpileLink(building,pile) -> true/false
* saveWorkshopProfile(building) -> profile
* getAmmoItem(building) -> item_type
* setAmmoItem(building,item_type) -> true/false
* isPassableTile(pos) -> true/false
* isTreeTile(pos) -> true/false
* isTargetableTile(pos) -> true/false
* getTileStatus(building,pos) -> 'invalid/ok/out_of_range/blocked/semiblocked'
* paintAimScreen(building,view_pos_xyz,left_top_xy,size_xy)
* canTargetUnit(unit) -> true/false
proj_info = { target = pos, [delta = float/pos], [factor = int] }
* projPosAtStep(building,proj_info,step) -> pos
* projPathMetrics(building,proj_info) -> {
hit_type = 'wall/floor/ceiling/map_edge/tree',
collision_step = int,
collision_z_step = int,
goal_distance = int,
goal_step = int/nil,
goal_z_step = int/nil,
status = 'ok/out_of_range/blocked'
}
* adjustToTarget(building,pos) -> pos,ok=true/false
* traceUnitPath(unit) -> { {x=int,y=int,z=int[,from=time][,to=time]} }
* unitPosAtTime(unit, time) -> pos
* proposeUnitHits(building) -> { {
pos=pos, unit=unit, time=float, dist=int,
[lmargin=float,] [rmargin=float,]
} }
* computeNearbyWeight(building,hits,{[id/unit]=score}[,fname])
]]
Z_STEP_COUNT = 15
Z_STEP = 1/31
function getMetrics(engine, path)
path.metrics = path.metrics or projPathMetrics(engine, path)
return path.metrics
end
function findShotHeight(engine, target)
local path = { target = target, delta = 0.0 }
if projPathMetrics(engine, path).goal_step then
if getMetrics(engine, path).goal_step then
return path
end
for i = 1,Z_STEP_COUNT do
path.delta = i*Z_STEP
if projPathMetrics(engine, path).goal_step then
return path
local tpath = { target = target, delta = Z_STEP_COUNT*Z_STEP }
if getMetrics(engine, tpath).goal_step then
for i = 1,Z_STEP_COUNT-1 do
path = { target = target, delta = i*Z_STEP }
if getMetrics(engine, path).goal_step then
return path
end
end
return tpath
end
tpath = { target = target, delta = -Z_STEP_COUNT*Z_STEP }
if getMetrics(engine, tpath).goal_step then
for i = 1,Z_STEP_COUNT-1 do
path = { target = target, delta = -i*Z_STEP }
if getMetrics(engine, path).goal_step then
return path
end
end
return tpath
end
end
function findReachableTargets(engine, targets)
local reachable = {}
for _,tgt in ipairs(targets) do
tgt.path = findShotHeight(engine, tgt.pos)
if tgt.path then
table.insert(reachable, tgt)
end
end
return reachable
end
recent_targets = recent_targets or {}
if dfhack.is_core_context then
dfhack.onStateChange[_ENV] = function(code)
if code == SC_MAP_LOADED then
recent_targets = {}
end
end
end
function saveRecent(unit)
local id = unit.id
local tgt = recent_targets
tgt[id] = (tgt[id] or 0) + 1
dfhack.timeout(3, 'days', function()
tgt[id] = math.max(0, tgt[id]-1)
end)
end
function getBaseUnitWeight(unit)
if dfhack.units.isCitizen(unit) then
return -10
elseif unit.flags1.diplomat or unit.flags1.merchant then
return -2
elseif unit.flags1.tame and unit.civ_id == df.global.ui.civ_id then
return -1
else
local rv = 1
if unit.flags1.marauder then rv = rv + 0.5 end
if unit.flags1.active_invader then rv = rv + 1 end
if unit.flags1.invader_origin then rv = rv + 1 end
if unit.flags1.invades then rv = rv + 1 end
if unit.flags1.hidden_ambusher then rv = rv + 1 end
return rv
end
end
function getUnitWeight(unit)
local base = getBaseUnitWeight(unit)
return base * math.pow(0.7, recent_targets[unit.id] or 0)
end
function unitWeightCache()
local cache = {}
return cache, function(unit)
local id = unit.id
cache[id] = cache[id] or getUnitWeight(unit)
return cache[id]
end
end
function scoreTargets(engine, reachable)
local ucache, get_weight = unitWeightCache()
for _,tgt in ipairs(reachable) do
tgt.score = get_weight(tgt.unit)
if tgt.lmargin and tgt.lmargin < 3 then
tgt.score = tgt.score * tgt.lmargin / 3
end
if tgt.rmargin and tgt.rmargin < 3 then
tgt.score = tgt.score * tgt.rmargin / 3
end
end
computeNearbyWeight(engine, reachable, ucache)
for _,tgt in ipairs(reachable) do
tgt.score = (tgt.score + tgt.nearby_weight*0.7) * math.pow(0.995, tgt.time/3)
end
table.sort(reachable, function(a,b)
return a.score > b.score or (a.score == b.score and a.time < b.time)
end)
end
function pickUniqueTargets(reachable)
local unique = {}
path.delta = -i*Z_STEP
if projPathMetrics(engine, path).goal_step then
return path
if #reachable > 0 then
local pos_table = {}
local first_score = reachable[1].score
for i,tgt in ipairs(reachable) do
if tgt.score < 0 or tgt.score < 0.1*first_score then
break
end
local x,y,z = pos2xyz(tgt.pos)
local key = x..':'..y..':'..z
if pos_table[key] then
table.insert(pos_table[key].units, tgt.unit)
else
table.insert(unique, tgt)
pos_table[key] = tgt
tgt.units = { tgt.unit }
end
end
end
return unique
end
function doAimProjectile(engine, item, target_min, target_max, skill)
print(item, df.skill_rating[skill])
local targets = proposeUnitHits(engine)
if #targets > 0 then
local rnd = math.random(#targets)
return findShotHeight(engine, targets[rnd].pos)
local reachable = findReachableTargets(engine, targets)
scoreTargets(engine, reachable)
local unique = pickUniqueTargets(reachable)
if #unique > 0 then
local cnt = math.max(math.min(#unique,5), math.min(10, math.floor(#unique/2)))
local rnd = math.random(cnt)
for _,u in ipairs(unique[rnd].units) do
saveRecent(u)
end
return unique[rnd].path
end
end