dfhack/plugins/lua/siege-engine.lua

313 lines
8.2 KiB
Lua

local _ENV = mkmodule('plugins.siege-engine')
--[[
Native functions:
* getTargetArea(building) -> point1, point2
* 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])
]]
local utils = require 'utils'
Z_STEP_COUNT = 15
Z_STEP = 1/31
ANNOUNCEMENT_FLAGS = {
UNIT_COMBAT_REPORT = true
}
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 getMetrics(engine, path).goal_step then
return path
end
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)
local flags1 = unit.flags1
local rv = 1
if unit.mood == df.mood_type.Berserk
or dfhack.units.isOpposedToLife(unit)
or dfhack.units.isCrazed(unit)
then
rv = rv + 1
else
if dfhack.units.isCitizen(unit) then
return -30
elseif flags1.diplomat or flags1.merchant or flags1.forest then
return -5
elseif flags1.tame and unit.civ_id == df.global.ui.civ_id then
return -1
end
end
if flags1.marauder then rv = rv + 0.5 end
if flags1.active_invader then rv = rv + 1 end
if flags1.invader_origin then rv = rv + 1 end
if flags1.invades then rv = rv + 1 end
if flags1.hidden_ambusher then rv = rv + 1 end
if unit.counters.unconscious > 0 then
rv = rv * 0.3
elseif unit.job.hunt_target then
rv = rv * 3
elseif unit.job.destroy_target then
rv = rv * 2
elseif unit.relationship_ids.GroupLeader < 0 and not flags1.rider then
rv = rv * 1.5
end
return rv
end
function getUnitWeight(unit)
local base = getBaseUnitWeight(unit)
return base * (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) * (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 = {}
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 describeUnit(unit)
local desc = dfhack.units.getProfessionName(unit)
local name = dfhack.units.getVisibleName(unit)
if name.has_name then
return desc .. ' ' .. dfhack.TranslateName(name)
end
return desc
end
function produceCombatReport(operator, item, target)
local msg = describeUnit(operator) .. ' launches ' ..
utils.getItemDescriptionPrefix(item) ..
utils.getItemDescription(item) ..
' at '
local pos = operator.pos
if target then
local units = target.units
for i,v in ipairs(units) do
if i > 1 then
if i < #units then
msg = msg .. ', '
elseif i > 2 then
msg = msg .. ', and '
else
msg = msg .. ' and '
end
end
msg = msg .. describeUnit(v)
end
msg = msg .. '!'
pos = target.pos
else
msg = msg .. 'the target area.'
end
local id = dfhack.gui.makeAnnouncement(
df.announcement_type.COMBAT_STRIKE_DETAILS,
ANNOUNCEMENT_FLAGS, pos, msg, COLOR_CYAN, true
)
dfhack.gui.addCombatReport(operator, df.unit_report_type.Hunting, id)
if target then
for i,v in ipairs(target.units) do
dfhack.gui.addCombatReportAuto(v, ANNOUNCEMENT_FLAGS, id)
end
end
end
function doAimProjectile(engine, item, target_min, target_max, operator, skill)
--print(item, df.skill_rating[skill])
local targets = proposeUnitHits(engine)
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)
local target = unique[rnd]
for _,u in ipairs(target.units) do
saveRecent(u)
end
produceCombatReport(operator, item, target)
return target.path
end
produceCombatReport(operator, item, nil)
end
return _ENV