wesnoth/data/lua/wml/find_path.lua
Steve Cotton 06dd9a140c Add [find_path] option "nearest_by", and simple_find_path test
Adding this is issue 2 of #4177, changing the behavior when [find_path]
is given a SLF which matches multiple hexes.

The map and tests here should be easy enough for manually editing them. It
duplicates some of the functionality of the existing characterize_pathfinding
tests, however those tests need their expected values to be calculated and
can't be changed by hand.

'''nearest_by''': {DevFeature1.15|2} possible values "movement_cost"
(default), "steps", "hexes". If the [destination] SLF matches multiple hexes,
the one that would need the least movement points to reach may not be the one
that's closest as measured by '''hexes''', or closest as measured by steps,
from the starting point.

Behavior in 1.14 depended on which hex was checked first.
2019-09-11 11:17:56 +02:00

174 lines
6.3 KiB
Lua

local helper = wesnoth.require "helper"
local utils = wesnoth.require "wml-utils"
function wesnoth.wml_actions.find_path(cfg)
local filter_unit = wml.get_child(cfg, "traveler") or helper.wml_error("[find_path] missing required [traveler] tag")
-- only the first unit matching
local unit = wesnoth.get_units(filter_unit)[1] or helper.wml_error("[find_path]'s filter didn't match any unit")
local filter_location = wml.get_child(cfg, "destination") or helper.wml_error( "[find_path] missing required [destination] tag" )
-- support for $this_unit
local this_unit = utils.start_var_scope("this_unit")
wml.variables["this_unit"] = nil -- clearing this_unit
wml.variables["this_unit"] = unit.__cfg -- cfg field needed
local variable = cfg.variable or "path"
local ignore_units = false
local ignore_teleport = false
if cfg.check_zoc == false then --if we do not want to check the ZoCs, we must ignore units
ignore_units = true
end
if cfg.check_teleport == false then --if we do not want to check teleport, we must ignore it
ignore_teleport = true
end
local allow_multiple_turns = cfg.allow_multiple_turns
local viewing_side
local nearest_by_cost = true
local nearest_by_distance = false
local nearest_by_steps = false
if (cfg.nearest_by or "movement_cost") == "hexes" then
nearest_by_cost = false
nearest_by_distance = true
nearest_by_steps = false
elseif (cfg.nearest_by or "movement_cost") == "steps" then
nearest_by_cost = false
nearest_by_distance = false
nearest_by_steps = true
end
if not cfg.check_visibility then viewing_side = 0 end -- if check_visiblity then shroud is taken in account
-- only the first location with the lowest distance and lowest movement cost will match.
local locations = wesnoth.get_locations(filter_location)
local max_cost = nil
if not allow_multiple_turns then max_cost = unit.moves end --to avoid wrong calculation on already moved units
local current_distance, current_cost, current_steps = math.huge, math.huge, math.huge
local current_location = {}
local width,heigth = wesnoth.get_map_size() -- data for test below
for index, location in ipairs(locations) do
-- we test if location passed to pathfinder is invalid (border);
-- if it is, do not use it, and continue the cycle
if location[1] == 0 or location[1] == ( width + 1 ) or location[2] == 0 or location[2] == ( heigth + 1 ) then
else
local distance = wesnoth.map.distance_between ( unit.x, unit.y, location[1], location[2] )
-- if we pass an unreachable location then an empty path and high value cost will be returned
local path, cost = wesnoth.find_path( unit, location[1], location[2], { max_cost = max_cost, ignore_units = ignore_units, ignore_teleport = ignore_teleport, viewing_side = viewing_side } )
if #path == 0 or cost >= 42424241 then
-- it's not a reachable hex. 42424242 is the high value returned for unwalkable or busy terrains
else
local steps = #path
local is_better = false
if nearest_by_cost and cost < current_cost then
is_better = true
elseif nearest_by_distance and distance < current_distance then
is_better = true
elseif nearest_by_steps and steps < current_steps then
is_better = true
elseif cost == current_cost and distance == current_distance and steps == current_steps then
-- the two options are equivalent. Treating this as not-better probably creates a bias for
-- choosing the north-west option, treating it as better probably biases to south-east.
-- Choosing false is more likely to match the option that 1.14 would choose.
is_better = false
elseif cost <= current_cost and distance <= current_distance and steps <= current_steps then
is_better = true
end
if is_better then
current_distance = distance
current_cost = cost
current_steps = steps
current_location = location
end
end
end
end
if #current_location == 0 then
-- either no matching locations, or only inaccessible matching locations (maybe enemy units are there)
if #locations == 0 then
wesnoth.message("WML warning","[find_path]'s filter didn't match any location")
end
wml.variables[tostring(variable)] = { hexes = 0 } -- set only hexes, nil all other values
else
local path, cost = wesnoth.find_path(
unit,
current_location[1], current_location[2],
{
max_cost = max_cost,
ignore_units = ignore_units,
ignore_teleport = ignore_teleport,
viewing_side = viewing_side
})
local turns
if cost == 0 then -- if location is the same, of course it doesn't cost any MP
turns = 0
else
turns = math.ceil( ( ( cost - unit.moves ) / unit.max_moves ) + 1 )
end
if cost >= 42424241 then -- it's the high value returned for unwalkable or busy terrains
wml.variables[tostring(variable)] = { hexes = 0 } -- set only length, nil all other values
-- support for $this_unit
wml.variables["this_unit"] = nil -- clearing this_unit
utils.end_var_scope("this_unit", this_unit)
return end
if not allow_multiple_turns and turns > 1 then -- location cannot be reached in one turn
wml.variables[tostring(variable)] = { hexes = 0 }
-- support for $this_unit
wml.variables["this_unit"] = nil -- clearing this_unit
utils.end_var_scope("this_unit", this_unit)
return end -- skip the cycles below
wml.variables[tostring( variable )] =
{
hexes = current_distance,
from_x = unit.x, from_y = unit.y,
to_x = current_location[1], to_y = current_location[2],
movement_cost = cost,
required_turns = turns
}
for index, path_loc in ipairs(path) do
local sub_path, sub_cost = wesnoth.find_path(
unit,
path_loc[1],
path_loc[2],
{
max_cost = max_cost,
ignore_units = ignore_units,
ignore_teleport = ignore_teleport,
viewing_side = viewing_side
} )
local sub_turns
if sub_cost == 0 then
sub_turns = 0
else
sub_turns = math.ceil( ( ( sub_cost - unit.moves ) / unit.max_moves ) + 1 )
end
wml.variables[string.format( "%s.step[%d]", variable, index - 1 )] =
{ -- this structure takes less space in the inspection window
x = path_loc[1], y = path_loc[2],
terrain = wesnoth.get_terrain( path_loc[1], path_loc[2] ),
movement_cost = sub_cost,
required_turns = sub_turns
}
end
end
-- support for $this_unit
wml.variables["this_unit"] = nil -- clearing this_unit
utils.end_var_scope("this_unit", this_unit)
end