Rewrote pathfinder to use navigational sectors and "waypoints" instead of grids.

master
Kalabasa 2016-02-02 00:08:44 +08:00
parent dbb81e6c7a
commit 11e4d9828f
6 changed files with 639 additions and 536 deletions

View File

@ -1,122 +1,115 @@
function defense:toggle_debug(on)
self.debug = on
if self.debug then
regeneration.rate = 100
minetest.set_timeofday(0.3)
return true, "Debug mode activated"
else
regeneration.rate = self.regeneration_rate
return true, "Debug mode deactivated"
end
end
minetest.register_chatcommand("debug", {
description = "Toggle Defense mod debug mode",
privs = {server=true},
func = function(name)
defense.debug = not defense.debug
if defense.debug then
regeneration.rate = 100
minetest.set_timeofday(0.3)
return true, "Debug mode activated"
else
regeneration.rate = defense.regeneration_rate
return true, "Debug mode deactivated"
end
return defense:toggle_debug(not defense.debug)
end,
})
-- Pathfinder debugger
local path_interval = 3
local path_class = nil
local path_visit = nil
local path_visited = nil
local path_waiting = {}
local path_active = {}
local path_timer = 0
local path_count = 0
local path_directions = {
{x=0, y=0, z=0},
{x=0, y=-1, z=0},
{x=0, y=1, z=0},
{x=0, y=0, z=-1},
{x=1, y=0, z=0},
{x=-1, y=0, z=0},
{x=0, y=0, z=1},
}
local pf_player = nil
local pf_class_name = nil
local pf_update_interval = 1.0
minetest.register_chatcommand("debug_path", {
params = "[<class>]",
description = "Debug the pathfinder flow field",
minetest.register_chatcommand("debug_pf", {
description = "Debug the pathfinder",
params = "<class>",
privs = {server=true},
func = function(name, class)
if not defense.debug then
return false, "Debug mode required!"
end
if not class or class == "" then
path_class = nil
return true, "Pathfinder debug off"
else
if class then
if defense.pathfinder.classes[class] then
local pos = minetest.get_player_by_name(name):getpos()
pos = {
x = math.floor(pos.x + 0.5),
y = math.floor(pos.y + 0.5),
z = math.floor(pos.z + 0.5)
}
path_visited = {}
path_visit = Queue.new() Queue.push(path_visit, pos)
path_timer = path_interval
path_class = class
return true
pf_class_name = class
pf_player = minetest.get_player_by_name(name)
return true, "Pathfinder debugger for " .. pf_class_name .. " activated"
else
return false, "Invalid class!"
return false, "No pathfinder class of that name"
end
else
pf_class_name = nil
pf_player = nil
return true, "Pathfinder debugger deactivated"
end
end,
})
minetest.register_node("defense:debug_path", {
minetest.register_node("defense:debug_pf", {
drawtype = "allfaces",
visual_scale = 1.0,
tiles = {"defense_debug_path.png"},
use_texture_alpha = true,
paramtype = "light",
sunlight_propagates = true,
light_source = 14,
groups = {dig_immediate = 3},
drop = "",
walkable = false,
groups = {dig_immediate=3},
})
local function path_update()
for _=1,Queue.size(path_visit) do
local pos = Queue.pop(path_visit)
table.insert(path_waiting, pos)
path_visited[pos.x .. ":" .. pos.y .. ":" .. pos.z] = true
for _,dir in ipairs(path_directions) do
local nxt = vector.add(pos, dir)
if not path_visited[nxt.x .. ":" .. nxt.y .. ":" .. nxt.z] then
Queue.push(path_visit, nxt)
end
end
end
for _,pos in ipairs(path_active) do
minetest.register_abm({
nodenames = {"defense:debug_pf"},
interval = 2.0,
chance = 1,
action = function(pos)
minetest.remove_node(pos)
end
path_active = {}
end,
})
for i=#path_waiting,1,-1 do
local pos = path_waiting[i]
local field = defense.pathfinder:get_field(path_class, pos)
if field and field.distance == path_count then
minetest.place_node(pos, {name="defense:debug_path"})
table.remove(path_waiting, i)
table.insert(path_active, pos)
local function pf_update()
if pf_class_name then
local pathfinder = defense.pathfinder
local pos = pf_player:getpos()
local sector = pathfinder.find_containing_sector(pathfinder.classes[pf_class_name], math.floor(pos.x + 0.5), math.floor(pos.y + 0.5), math.floor(pos.z + 0.5))
if sector then
local distance_str = sector.distance
if sector.distance == nil then
distance_str = "nil"
end
local bounds_str = "(" .. sector.min_x .. "," .. sector.min_y .. "," .. sector.min_z .. ";" .. sector.max_x .. "," .. sector.max_y .. "," .. sector.max_z .. ")"
local links_str = ""
for i,l in pairs(sector.links) do
links_str = links_str .. " " .. i
end
links_str = "[" .. links_str .. " ]"
defense:log("You are in sector " .. sector.id .. " {d=" .. distance_str .. " b=" .. bounds_str .. " l=" .. links_str .. "}")
for z = sector.min_z,sector.max_z do
for y = sector.min_y,sector.max_y do
for x = sector.min_x,sector.max_x do
if (x == sector.min_x or x == sector.max_x)
and (y == sector.min_y or y == sector.max_y)
and (z == sector.min_z or z == sector.max_z) then
local pos = {x=x,y=y,z=z}
local node = minetest.get_node_or_nil(pos)
if node and node.name == "air" then
minetest.set_node(pos, {name="defense:debug_pf"})
end
end
end
end
end
else
defense:log("You are not in a sector")
end
end
path_count = path_count + 1
if path_count > 20 then
path_class = nil
end
end
local pf_last_update_time = 0
minetest.register_globalstep(function(dtime)
if path_class then
path_timer = path_timer - dtime
if path_timer <= 0 then
path_timer = path_interval
path_update()
end
local gt = minetest.get_gametime()
if pf_last_update_time + pf_update_interval < gt then
pf_update()
pf_last_update_time = gt
end
end)

View File

@ -1,6 +1,6 @@
defense.director = {}
local director = defense.director
director.call_interval = 1.0
director.update_interval = 1.0
director.intensity_decay = 0.93
director.max_entities = 50
director.spawn_list = {
@ -14,7 +14,7 @@ director.spawn_list = {
probability = 0.4,
day_start = 0,
spawn_time = 14.0,
spawn_location = "ground",
spawn_location_type = "ground",
},
{
description = "Unggoy horde",
@ -26,7 +26,7 @@ director.spawn_list = {
probability = 0.8,
day_start = 1,
spawn_time = 31.0,
spawn_location = "ground",
spawn_location_type = "ground",
},
{
description = "Aranay group",
@ -38,7 +38,7 @@ director.spawn_list = {
probability = 0.3,
day_start = 0,
spawn_time = 18.0,
spawn_location = "ground",
spawn_location_type = "ground",
},
{
description = "Paniki group",
@ -50,7 +50,7 @@ director.spawn_list = {
probability = 0.6,
day_start = 0,
spawn_time = 9.0,
spawn_location = "air",
spawn_location_type = "air",
},
{
description = "Sarangay",
@ -62,7 +62,7 @@ director.spawn_list = {
probability = 0.4,
day_start = 2,
spawn_time = 90.0,
spawn_location = "ground",
spawn_location_type = "ground",
},
{
description = "Botete",
@ -74,7 +74,7 @@ director.spawn_list = {
probability = 0.4,
day_start = 1,
spawn_time = 90.0,
spawn_location = "air",
spawn_location_type = "air",
},
}
@ -90,43 +90,15 @@ for _,m in ipairs(director.spawn_list) do
spawn_timers[m.description] = m.spawn_time/2
end
function director:on_interval()
self:update_intensity()
if defense.debug then
-- minetest.chat_send_all("Intensity: " .. self.intensity)
end
if self.cooldown_timer <= 0 then
if defense:is_dark() and #minetest.luaentities < self.max_entities and not defense.debug then
self:spawn_monsters()
end
if self.intensity > 0.5 then
self.cooldown_timer = math.random(5, 5 + 80 * (self.intensity - 0.5))
end
else
self.cooldown_timer = self.cooldown_timer - self.call_interval
if defense.debug then
minetest.chat_send_all("Cooldown: " .. self.cooldown_timer)
end
end
for k,v in pairs(spawn_timers) do
if v > 0 then
spawn_timers[k] = v - self.call_interval
end
end
end
function director:spawn_monsters()
local function spawn_monsters()
-- Filter eligible monsters
local filtered = {}
for _,m in ipairs(self.spawn_list) do
for _,m in ipairs(director.spawn_list) do
if spawn_timers[m.description] <= 0
and self:get_day_count() >= m.day_start
and defense.get_day_count() >= m.day_start
and math.random() < m.probability
and self.intensity >= m.intensity_min
and self.intensity <= m.intensity_max then
and director.intensity >= m.intensity_min
and director.intensity <= m.intensity_max then
table.insert(filtered, m)
end
end
@ -136,23 +108,18 @@ function director:spawn_monsters()
local monster = filtered[math.random(#filtered)]
-- Determine group size
local intr = math.max(0, math.min(1, self.intensity + math.random() * 2 - 1))
local intr = math.max(0, math.min(1, director.intensity + math.random() * 2 - 1))
local group_size = math.floor(0.5 + monster.group_max + (monster.group_min - monster.group_max) * intr)
-- Find the spawn position
local pos = self:find_spawn_position(monster.spawn_location)
local pos = find_spawn_position(monster.spawn_location_type)
if not pos then
if defense.debug then
minetest.chat_send_all("No spawn point found for " .. monster.description .. "!")
end
defense:log("No spawn point found for " .. monster.description .. "!")
return false
end
-- Spawn
if defense.debug then
minetest.chat_send_all("Spawn " .. monster.description .. " (" .. group_size .. " " ..
monster.name .. ") at " .. minetest.pos_to_string(pos))
end
defense:log("Spawn " .. monster.description .. " (" .. group_size .. " " .. monster.name .. ") at " .. minetest.pos_to_string(pos))
repeat
minetest.after(group_size * (math.random() * 0.2), function()
local obj = minetest.add_entity(pos, monster.name)
@ -163,7 +130,7 @@ function director:spawn_monsters()
return true
end
function director:find_spawn_position(spawn_location)
local function find_spawn_position(spawn_location_type)
local players = minetest.get_connected_players()
if #players == 0 then
return nil
@ -185,7 +152,7 @@ function director:find_spawn_position(spawn_location)
local a = math.random() * 2 * math.pi
pos.x = pos.x + math.cos(a) * r
pos.z = pos.z + math.sin(a) * r
if spawn_location == "ground" then
if spawn_location_type == "ground" then
-- Move pos to on ground
pos.y = pos.y + 10
local d = -1
@ -205,7 +172,7 @@ function director:find_spawn_position(spawn_location)
break
end
end
elseif spawn_location == "air" then
elseif spawn_location_type == "air" then
-- Move pos up
pos.y = pos.y + 12 + math.random() * 12
local node = minetest.get_node_or_nil(pos)
@ -236,7 +203,7 @@ function director:find_spawn_position(spawn_location)
return nil
end
function director:update_intensity()
local function update_intensity()
local players = minetest.get_connected_players()
if #players == 0 then
return
@ -258,19 +225,37 @@ function director:update_intensity()
last_average_health = average_health
last_mob_count = mob_count
self.intensity = math.max(0, math.min(1, self.intensity * self.intensity_decay + delta))
director.intensity = math.max(0, math.min(1, director.intensity * director.intensity_decay + delta))
end
function director:get_day_count()
local time_speed = minetest.setting_get("time_speed")
return math.floor(minetest.get_gametime() * time_speed / 86400)
local function update()
update_intensity()
if director.cooldown_timer <= 0 then
if defense:is_dark() and #minetest.luaentities < director.max_entities and not defense.debug then
spawn_monsters()
end
if director.intensity > 0.5 then
director.cooldown_timer = math.random(5, 5 + 80 * (director.intensity - 0.5))
end
else
director.cooldown_timer = director.cooldown_timer - director.update_interval
defense:log("Cooldown: " .. director.cooldown_timer)
end
for k,v in pairs(spawn_timers) do
if v > 0 then
spawn_timers[k] = v - director.update_interval
end
end
end
function director:save()
local function save()
local file = assert(io.open(minetest.get_worldpath() .. "/defense_director_state.txt", "w"))
local data = {
intensity = self.intensity,
cooldown_timer = self.cooldown_timer,
intensity = director.intensity,
cooldown_timer = director.cooldown_timer,
spawn_timers = spawn_timers,
last_average_health = last_average_health,
last_mob_count = last_mob_count,
@ -279,13 +264,13 @@ function director:save()
assert(file:close())
end
function director:load()
local function load()
local file = io.open(minetest.get_worldpath() .. "/defense_director_state.txt", "r")
if file then
local data = minetest.deserialize(file:read("*all"))
if data then
self.intensity = data.intensity
self.cooldown_timer = data.cooldown_timer
director.intensity = data.intensity
director.cooldown_timer = data.cooldown_timer
last_average_health = data.last_average_health
last_mob_count = data.last_mob_count
for k,v in pairs(data.spawn_timers) do
@ -297,15 +282,15 @@ function director:load()
end
minetest.register_on_shutdown(function()
director:save()
save()
end)
director:load()
load()
local last_call_time = 0
local last_update_time = 0
minetest.register_globalstep(function(dtime)
local gt = minetest.get_gametime()
if last_call_time + director.call_interval < gt then
director:on_interval()
last_call_time = gt
if last_update_time + director.update_interval < gt then
update()
last_update_time = gt
end
end)

View File

@ -9,11 +9,23 @@ local function dofile2(file)
dofile(modpath .. "/" .. file)
end
local time_speed = minetest.setting_get("time_speed")
function defense:get_day_count()
return math.floor(minetest.get_gametime() * time_speed / 86400)
end
function defense:is_dark()
local tod = minetest.get_timeofday()
return tod < 0.2 or tod > 0.8 or defense.debug
end
function defense:log(message)
if self.debug then
minetest.chat_send_all("[debug] " .. message)
end
minetest.debug(message)
end
dofile2("util.lua")
dofile2("Queue.lua")
@ -29,4 +41,6 @@ dofile2("mobs/sarangay.lua")
dofile2("mobs/paniki.lua")
dofile2("mobs/botete.lua")
dofile2("debug.lua")
dofile2("debug.lua")
defense:toggle_debug(true)

View File

@ -13,7 +13,7 @@ mobs.default_prototype = {
id = 0,
smart_path = true,
mass = 1,
movement = "ground", -- "ground"/"air"
movement = "ground", -- "ground"/"air"/"crawl"
move_speed = 1,
jump_height = 1,
armor = 0,
@ -34,6 +34,8 @@ mobs.default_prototype = {
cache_find_nearest_player = nil,
}
local reg_nodes = minetest.registered_nodes
local function vec_zero() return {x=0, y=0, z=0} end
function mobs.default_prototype:on_activate(staticdata)
@ -83,7 +85,7 @@ function mobs.default_prototype:on_step(dtime)
-- Remove when far enough and may not reach the player at all
local nearest = self:find_nearest_player()
if self.life_timer <= 0 then
if nearest.distance > 6 then
if nearest.distance > 12 then
self.object:remove()
end
else
@ -154,16 +156,9 @@ function mobs.default_prototype:hunt()
self:do_attack(nearest.player)
end
if nearest.distance > self.attack_range or nearest.distance < self.attack_range/2-1 then
local pos = self.object:getpos()
local direction = nil
if self.smart_path and nearest.distance < defense.pathfinder.path_max_range then
direction = defense.pathfinder:get_direction(self.name, pos)
end
-- TODO Use pathfinder
if direction then
minetest.chat_send_all("dir:" .. minetest.pos_to_string(direction))
self.destination = vector.add(pos, vector.multiply(direction, 1.2))
else
if not self.destination then
local r = math.max(0, self.attack_range - 2)
local dir = vector.direction(nearest.position, self.object:getpos())
self.destination = vector.add(nearest.position, vector.multiply(dir, r))
@ -239,7 +234,7 @@ function mobs.default_prototype:is_standing()
}
for _,c in ipairs(corners) do
local node = minetest.get_node_or_nil(c)
if not node or minetest.registered_nodes[node.name].walkable then
if not node or reg_nodes[node.name].walkable then
self.cache_is_standing = true
return true
end
@ -316,7 +311,7 @@ function mobs.default_prototype:calculate_wall_normal()
if xi ~= 2 and yi ~= 2 and zi ~= 2 then
local sp = vector.add(p, {x=xs[xi], y=ys[yi], z=zs[zi]})
local node = minetest.get_node_or_nil(sp)
if node and minetest.registered_nodes[node.name].walkable then
if node and reg_nodes[node.name].walkable then
normal = vector.add(normal, {x=normals[xi], y=normals[yi], z=normals[zi]})
count = count + 1
end
@ -398,15 +393,7 @@ function mobs.move_method:ground(dtime, destination)
-- Check for jump
local jump = nil
if self.smart_path then
local p = self.object:getpos()
if destination.y > p.y + 0.55 then
for y=p.y,p.y+self.jump_height do
jump = defense.pathfinder:get_direction(self.name, {x=p.x, y=y, z=p.z})
if not jump or (jump.x ~= 0 or jump.z ~= 0) then
break
end
end
end
-- TODO Jump to destination
else
if dist > 1 then
local p = self.object:getpos()
@ -422,7 +409,7 @@ function mobs.move_method:ground(dtime, destination)
}
for _,f in ipairs(fronts) do
local node = minetest.get_node_or_nil(vector.add(p, f))
if not node or minetest.registered_nodes[node.name].walkable then
if not node or reg_nodes[node.name].walkable then
jump = vector.direction(self.object:getpos(), destination)
break
end
@ -488,14 +475,10 @@ function mobs.register_mob(name, def)
if defense.pathfinder and prototype.smart_path then
defense.pathfinder:register_class(name, {
size = {
x = math.ceil(prototype.collisionbox[4] - prototype.collisionbox[1]),
y = math.ceil(prototype.collisionbox[5] - prototype.collisionbox[2]),
z = math.ceil(prototype.collisionbox[6] - prototype.collisionbox[3])
},
collisionbox = prototype.collisionbox,
jump_height = math.floor(prototype.jump_height),
cost_method = def.pathfinder_cost or defense.pathfinder.cost_method[prototype.movement]
path_check = def.pathfinder_check or defense.pathfinder.default_path_check[prototype.movement],
cost_method = def.pathfinder_cost or defense.pathfinder.default_cost_method[prototype.movement],
})
end

View File

@ -8,7 +8,7 @@ local current_music = nil
local last_intensity = 0
local last_update_time = 0
function music:update()
local function update()
local time = os.time()
if current_level > 0 then
if time < last_update_time + music.loop_length then
@ -77,5 +77,5 @@ function music:update()
end
minetest.register_globalstep(function(dtime)
music:update()
update()
end)

View File

@ -1,407 +1,535 @@
defense.pathfinder = {}
local pathfinder = defense.pathfinder
pathfinder.path_max_range = 32
pathfinder.path_max_range_far = 64
pathfinder.classes = {}
local chunk_size = 16
pathfinder.update_interval = 2.0
pathfinder.max_sector_size = 16
pathfinder.max_sector_count = 800
pathfinder.max_distance = 100
pathfinder.class_names = {}
-- State
local fields = {}
local visit_queues = {}
local visit_queue_far = Queue.new()
local player_last_update = {}
local morning_reset = false
local classes = {}
local next_sector_id = 1
-- local cid_data = {}
-- minetest.after(0, function()
-- for name, def in pairs(minetest.registered_nodes) do
-- cid_data[minetest.get_content_id(name)] = {
-- name = name,
-- walkable = def.walkable,
-- }
-- end
-- end)
local reg_nodes = minetest.registered_nodes
local neighbors = {
{x =-1, y = 0, z = 0},
{x = 1, y = 0, z = 0},
{x = 0, y =-1, z = 0},
{x = 0, y = 1, z = 0},
{x = 0, y = 0, z =-1},
{x = 0, y = 0, z = 1},
}
function pathfinder:register_class(class, properties)
self.classes[class] = properties
fields[class] = fields[class] or {}
visit_queues[class] = Queue.new()
pathfinder.classes = classes -- For debug
function pathfinder:register_class(class_name, properties)
table.insert(pathfinder.class_names, class_name)
classes[class_name] = {
name = class_name,
sectors = {},
sector_seeds = Queue.new(),
jump_height = properties.jump_height,
path_check = properties.path_check,
cost_method = properties.cost_method,
x_offset = properties.collisionbox[1],
y_offset = properties.collisionbox[2],
z_offset = properties.collisionbox[3],
x_size = math.ceil(properties.collisionbox[4] - properties.collisionbox[1]),
y_size = math.ceil(properties.collisionbox[5] - properties.collisionbox[2]),
z_size = math.ceil(properties.collisionbox[6] - properties.collisionbox[3]),
}
end
-- Returns a number
function pathfinder:get_distance(class, position)
local field = self:get_field(class, position)
if not field then
return nil
local function sector_contains(sector, x, y, z)
return x <= sector.max_x and x >= sector.min_x
and y <= sector.max_y and y >= sector.min_y
and z <= sector.max_z and z >= sector.min_z
end
local function find_containing_sector(class, x, y, z)
for i,s in pairs(class.sectors) do
if (sector_contains(s, x, y, z)) then
return s
end
end
return field.distance
return nil
end
-- Returns a vector
function pathfinder:get_direction(class, position)
local directions = {
[0]={x=0, y=0, z=0},
{x=0, y=-1, z=0},
{x=0, y=1, z=0},
{x=0, y=0, z=-1},
{x=1, y=0, z=0},
{x=-1, y=0, z=0},
{x=0, y=0, z=1},
}
pathfinder.find_containing_sector = find_containing_sector -- For debug
local total = vector.new(0, 0, 0)
local count = 0
local function get_player_pos(player)
local pos = player:getpos()
return math.floor(pos.x + 0.5), math.floor(pos.y + 0.5), math.floor(pos.z + 0.5)
end
local ipos = {x=math.floor(position.x), y=math.floor(position.y), z=math.floor(position.z)}
local cells = {
ipos,
{x=ipos.x + 1, y=ipos.y, z=ipos.z},
{x=ipos.x - 1, y=ipos.y, z=ipos.z},
{x=ipos.x, y=ipos.y + 1, z=ipos.z},
{x=ipos.x, y=ipos.y - 1, z=ipos.z},
{x=ipos.x, y=ipos.y, z=ipos.z + 1},
{x=ipos.x, y=ipos.y, z=ipos.z - 1},
}
for _,p in ipairs(cells) do
local field = self:get_field(class, p)
if field then
local last_time = player_last_update[field.player] or field.time
if field.time + field.distance * 4 > last_time then
local direction = directions[field.direction]
local weight = 1/(1 + field.distance)
total = vector.add(total, vector.multiply(direction, weight))
-- Deletes and queues a sector for regeneration
local function invalidate_sector(sector, class)
local id = sector.id
class.sectors[id] = nil
for i,l in pairs(sector.links) do
l.links[id] = nil
end
Queue.push(class.sector_seeds, {sector.min_x,sector.min_y,sector.min_z, nil,0})
-- TODO what if replacement seed is blocked?
end
-- Calculates the distances for each sector
local function calculate_distances(class)
local cost_method = class.cost_method
local sectors = class.sectors
for i,s in pairs(sectors) do
s.distance = nil
end
local visited_ids = {}
local visit = Queue.new()
local players = minetest.get_connected_players()
if #players then
for _,p in ipairs(players) do
local x, y, z = get_player_pos(p)
local sector = find_containing_sector(class, x, y, z)
if sector then
sector.distance = 0
Queue.push(visit, sector)
end
end
end
if total.x ~= 0 or total.y ~= 0 or total.z ~= 0 then
return vector.normalize(total)
else
return nil
end
end
while Queue.size(visit) > 0 do
local sector = Queue.pop(visit)
visited_ids[sector.id] = true
-- Returns a table {time, distance}
function pathfinder:get_field(class, position, no_position_adjust)
local collisionbox = self.classes[class].collisionbox
if not no_position_adjust then
position.x = position.x + collisionbox[1] + 0.01
position.y = position.y + collisionbox[2] + 0.01
position.z = position.z + collisionbox[3] + 0.01
end
local x = math.floor(position.x)
local y = math.floor(position.y)
local z = math.floor(position.z)
local chunk_key = math.floor(x/chunk_size) ..
":" .. math.floor(y/chunk_size) ..
":" .. math.floor(z/chunk_size)
local chunk = fields[class][chunk_key]
if not chunk then
return nil
end
local cx = x % chunk_size
local cy = y % chunk_size
local cz = z % chunk_size
local index = (cy * chunk_size + cz) * chunk_size + cx
return chunk[index]
end
function pathfinder:set_field(class, position, player, distance, direction, time, no_position_adjust)
local collisionbox = self.classes[class].collisionbox
if not no_position_adjust then
position.x = position.x + collisionbox[1] + 0.01
position.y = position.y + collisionbox[2] + 0.01
position.z = position.z + collisionbox[3] + 0.01
end
local x = math.floor(position.x)
local y = math.floor(position.y)
local z = math.floor(position.z)
local chunk_key = math.floor(x/chunk_size) ..
":" .. math.floor(y/chunk_size) ..
":" .. math.floor(z/chunk_size)
local chunk = fields[class][chunk_key]
if not chunk then
chunk = {}
fields[class][chunk_key] = chunk
end
local cx = x % chunk_size
local cy = y % chunk_size
local cz = z % chunk_size
local index = (cy * chunk_size + cz) * chunk_size + cx
chunk[index] = {time=time, direction=direction, distance=distance, player=player}
end
function pathfinder:delete_field(class, position)
local collisionbox = self.classes[class].collisionbox
local x = math.floor(position.x)
local y = math.floor(position.y)
local z = math.floor(position.z)
local chunk_key = math.floor(x/chunk_size) ..
":" .. math.floor(y/chunk_size) ..
":" .. math.floor(z/chunk_size)
local chunk = fields[class][chunk_key]
if not chunk then
return
end
local cx = x % chunk_size
local cy = y % chunk_size
local cz = z % chunk_size
local index = (cy * chunk_size + cz) * chunk_size + cx
chunk[index] = nil
end
function pathfinder:update(dtime)
if not defense:is_dark() then
-- reset flow fields in the morning
if not morning_reset then
morning_reset = true
player_last_update = {}
for c,_ in pairs(self.classes) do
fields[c] = {}
visit_queues[c] = Queue.new()
local distance = sector.distance
for i,l in pairs(sector.links) do
if not visited_ids[i] then
local cost = cost_method(sector, l)
local new_ldist = distance + cost
local ldist = l.distance
if ldist == nil or ldist > new_ldist then
l.distance = new_ldist
Queue.push(visit, l)
end
end
end
return
end
morning_reset = false
end
local neighborhood = {
{x=0, y=1, z=0},
{x=0, y=-1, z=0},
{x=0, y=0, z=1},
{x=-1, y=0, z=0},
{x=1, y=0, z=0},
{x=0, y=0, z=-1},
-- Returns array of {x,y,z,parent,parent_dir}
local function find_sector_exits(sector, class)
local sides = {
{0,1,1,
sector.max_x + 1, sector.min_y, sector.min_z,
sector.max_x + 1, sector.max_y, sector.max_z},
{0,1,1,
sector.min_x - 1, sector.min_y, sector.min_z,
sector.min_x - 1, sector.max_y, sector.max_z},
{1,0,1,
sector.min_x, sector.max_y + 1, sector.min_z,
sector.max_x, sector.max_y + 1, sector.max_z},
{1,0,1,
sector.min_x, sector.min_y - 1, sector.min_z,
sector.max_x, sector.min_y - 1, sector.max_z},
{1,1,0,
sector.min_x, sector.min_y, sector.max_z + 1,
sector.max_x, sector.max_y, sector.max_z + 1},
{1,1,0,
sector.min_x, sector.min_y, sector.min_z - 1,
sector.max_x, sector.max_y, sector.min_z - 1},
}
-- Update the field
local max_iter = 100 - math.floor(defense.director.intensity * 90)
local total_queues_size = 0
for c,class in pairs(self.classes) do
local vq = visit_queues[c]
local size = Queue.size(vq)
for i=1,math.min(size,max_iter) do
local current = Queue.pop(vq)
for di,n in ipairs(neighborhood) do
local npos = vector.add(current.position, n)
npos.x = math.floor(npos.x + 0.5)
npos.y = math.floor(npos.y + 0.5)
npos.z = math.floor(npos.z + 0.5)
local cost = class.cost_method(class, npos, current.position)
if cost and cost < self.path_max_range_far then
local next_distance = current.distance + cost
local neighbor_field = self:get_field(c, npos, true)
if not neighbor_field or
neighbor_field.time < current.time and
neighbor_field.direction ~= di or
neighbor_field.time == current.time and
(neighbor_field.distance > next_distance or
neighbor_field.distance == next_distance and
math.random() < 0.5) then
self:set_field(c, npos, current.player, next_distance, di, current.time, true)
if next_distance < self.path_max_range or current.far and next_distance < self.path_max_range_far then
if size < 800 then
Queue.push(vq, {
position = npos,
player = current.player,
distance = next_distance,
direction = di,
time = current.time,
})
end
elseif next_distance < self.path_max_range_far then
Queue.push(visit_queue_far, {
far = true,
class = c,
position = npos,
player = current.player,
distance = next_distance,
direction = di,
time = current.time,
})
else
self:delete_field(c, npos)
local path_check = class.path_check
local tmp_vec = vector.new()
local function path_check_i(x, y, z)
tmp_vec.x = x
tmp_vec.y = y
tmp_vec.z = z
return path_check(class, tmp_vec, nil)
end
local exits = {}
-- Find passable nodes that are cornered by >=2 different sector or passability nodes
for i,s in ipairs(sides) do
local xs = s[1]
local ys = s[2]
local zs = s[3]
local min_x = s[4]
local min_y = s[5]
local min_z = s[6]
local max_x = s[7]
local max_y = s[8]
local max_z = s[9]
local map = {}
for z = min_z,max_z,zs do
for y = min_y,max_y,ys do
for x = min_x,max_x,xs do
tmp_vec.x = x
tmp_vec.y = y
tmp_vec.z = z
local hash = minetest.hash_node_position(tmp_vec)
if path_check_i(x, y, z) then
local val = 0
local sector = find_containing_sector(class, x, y, z)
if sector then
val = sector.id
end
local edges = 0
if xs ~= 0 then
tmp_vec.x = x - xs
tmp_vec.y = y
tmp_vec.z = z
if val ~= map[minetest.hash_node_position(tmp_vec)] then
edges = edges + 1
end
end
if ys ~= 0 then
tmp_vec.x = x
tmp_vec.y = y - ys
tmp_vec.z = z
if val ~= map[minetest.hash_node_position(tmp_vec)] then
edges = edges + 1
end
end
if zs ~= 0 then
tmp_vec.x = x
tmp_vec.y = y
tmp_vec.z = z - zs
if val ~= map[minetest.hash_node_position(tmp_vec)] then
edges = edges + 1
end
end
if edges >= 2 then
table.insert(exits, {x,y,z, sector,i})
end
map[hash] = val
else
map[hash] = -1
end
if xs == 0 then break end
end
if ys == 0 then break end
end
if zs == 0 then break end
end
end
return exits
end
-- Returns a sector object {id, distance, links, min_x,min_y,min_z, max_x,max_y,max_z}
local function generate_sector(class, x, y, z, origin_dir)
local max_sector_span = pathfinder.max_sector_size - 1
local path_check = class.path_check
local min_x = -math.huge
local min_y = -math.huge
local min_z = -math.huge
local max_x = math.huge
local max_y = math.huge
local max_z = math.huge
local half_mss = math.floor(max_sector_span / 2)
local half_mss2 = math.ceil(max_sector_span / 2)
local size_min_x = x - half_mss
local size_min_y = y - half_mss
local size_min_z = z - half_mss
local size_max_x = x + half_mss2
local size_max_y = y + half_mss2
local size_max_z = z + half_mss2
local visited = {}
local visit = Queue.new()
Queue.push(visit, {x=x,y=y,z=z})
visited[minetest.hash_node_position(visit[1])] = true
while Queue.size(visit) > 0 do
local pos = Queue.pop(visit)
for i,n in ipairs(neighbors) do
local nxt = vector.add(pos, n)
local nhash = minetest.hash_node_position(nxt)
local nx = nxt.x
local ny = nxt.y
local nz = nxt.z
if not visited[nhash]
and nx <= max_x and nx >= min_x
and ny <= max_y and ny >= min_y
and nz <= max_z and nz >= min_z then
visited[nhash] = true
local passable = path_check(class, nxt, pos)
if passable == nil then return nil end
if passable and origin_dir ~= i
and not find_containing_sector(class, nx, ny, nz)
and nx <= size_max_x and nx >= size_min_x
and ny <= size_max_y and ny >= size_min_y
and nz <= size_max_z and nz >= size_min_z then
Queue.push(visit, nxt)
else
if i == 1 then
min_x = pos.x
size_max_x = min_x + max_sector_span
elseif i == 2 then
max_x = pos.x
size_min_x = max_x - max_sector_span
elseif i == 3 then
min_y = pos.y
size_max_y = min_y + max_sector_span
elseif i == 4 then
max_y = pos.y
size_min_y = max_y - max_sector_span
elseif i == 5 then
min_z = pos.z
size_max_z = min_z + max_sector_span
elseif i == 6 then
max_z = pos.z
size_min_z = max_z - max_sector_span
end
end
end
end
total_queues_size = total_queues_size + math.max(0, size - max_iter)
end
-- Update far fields
if total_queues_size == 0 then
local size = Queue.size(visit_queue_far)
for i=1,math.min(size,max_iter/2) do
local current = Queue.pop(visit_queue_far)
Queue.push(visit_queues[current.class], current)
local id = next_sector_id
next_sector_id = next_sector_id + 1
return {
id = id,
distance = nil,
links = {},
min_x = min_x,
min_y = min_y,
min_z = min_z,
max_x = max_x,
max_y = max_y,
max_z = max_z,
}
end
-- Removes sectors and seeds too far away from players
local function prune_sectors(class)
defense:log("Pruning sectors...")
local max_distance = pathfinder.max_distance
local sectors = class.sectors
local sector_seeds = class.sector_seeds
local players = minetest.get_connected_players()
-- Remove sectors
local to_remove = {}
for i,s in pairs(sectors) do
if s.distance == nil or s.distance > max_distance then
to_remove[i] = true
end
end
-- Update player positions
local time = minetest.get_gametime()
for _,p in ipairs(minetest.get_connected_players()) do
local pos = p:getpos()
for c,_ in pairs(self.classes) do
for y=math.floor(pos.y),math.ceil(pos.y) do
local tp = {x=pos.x, y=y, z=pos.z}
local field = self:get_field(c, tp)
if not field or field.distance > 0 then
local name = p:get_player_name()
self:set_field(c, tp, name, 0, 0, time)
Queue.push(visit_queues[c], {position=tp, player=name, distance=0, direction=0, time=time})
player_last_update[name] = time
end
for i,_ in pairs(to_remove) do
local s = sectors[i]
sectors[i] = nil
for __,l in pairs(s.links) do
if not to_remove[l.id] then
invalidate_sector(l, class)
end
end
end
-- Remove seeds
for i = sector_seeds.last,sector_seeds.first,-1 do
local seed = sector_seeds[i]
local seed_pos = {x=seed[1], y=seed[2], z=seed[3]}
local far = true
for _,p in ipairs(players) do
local x, y, z = get_player_pos(p)
if vector.distance({x=x,y=y,z=z}, seed_pos) <= max_distance then
far = false
end
end
if far then
Queue.remove(sector_seeds, i)
end
end
end
pathfinder.cost_method = {}
function pathfinder.cost_method.air(class, pos, parent)
-- Check if in solid
for y=pos.y,pos.y+class.size.y-1 do
for z=pos.z,pos.z+class.size.z-1 do
for x=pos.x,pos.x+class.size.x-1 do
local node = minetest.get_node_or_nil({x=x, y=y, z=z})
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
return math.huge
end
end
end
end
return 1
end
function pathfinder.cost_method.ground(class, pos, parent)
-- Check if in solid
for z=pos.z,pos.z+class.size.z-1 do
for x=pos.x,pos.x+class.size.x-1 do
for y=pos.y,pos.y+class.size.y-1 do
local node = minetest.get_node_or_nil({x=x, y=y, z=z})
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
return math.huge
local function update_class(class)
local max_sector_count = pathfinder.max_sector_count
local max_distance = pathfinder.max_distance
local sectors = class.sectors
local sector_seeds = class.sector_seeds
local path_check = class.path_check
local should_refresh_distances = false
-- Generate new seeds from player positions
local players = minetest.get_connected_players()
if #players then
for _,p in ipairs(players) do
local x, y, z = get_player_pos(p)
local sector = find_containing_sector(class, x, y, z)
if not sector then
Queue.push_back(sector_seeds, {x,y,z, nil,0})
should_refresh_distances = true
else
local distance = sector.distance
if distance == nil or distance > 0 then
should_refresh_distances = true
end
end
end
end
-- Check if on top of solid
local ground_distance = 9999
for z=pos.z,pos.z+class.size.z-1 do
for x=pos.x,pos.x+class.size.x-1 do
for y=pos.y-1,pos.y-class.jump_height-1,-1 do
local node = minetest.get_node_or_nil({x=x, y=y, z=z})
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
ground_distance = math.min(ground_distance, pos.y - y)
if ground_distance == 1 then
return 1
-- Grow sector seeds into sectors
local sector_count = 0
for _,__ in pairs(sectors) do
sector_count = sector_count + 1
end
local unready_seeds = {}
local target_sector_count = math.min(sector_count + math.max(math.ceil(100 / (math.log(sector_count + 10))), 1), max_sector_count)
while sector_count < target_sector_count and Queue.size(sector_seeds) > 0 do
local seed = Queue.pop(sector_seeds)
local x = seed[1]
local y = seed[2]
local z = seed[3]
if not find_containing_sector(class, x, y, z) and path_check(class, {x=x,y=y,z=z}, nil) then
local new_sector = generate_sector(class, x, y, z, seed[5])
local parent = seed[4]
if new_sector and (not parent or parent.distance == nil or parent.distance < max_distance) then
local id = new_sector.id
sectors[id] = new_sector
sector_count = sector_count + 1
-- Link parent
if parent then
new_sector.links[parent.id] = parent
parent.links[id] = new_sector
end
-- Generate new seeds and link adjacent sectors
local exits = find_sector_exits(new_sector, class)
for i,e in ipairs(exits) do
local exited_sector = find_containing_sector(class, e[1], e[2], e[3])
if exited_sector then
if not exited_sector.links[new_sector.id] then
exited_sector.links[new_sector.id] = new_sector
new_sector.links[exited_sector.id] = exited_sector
end
else
Queue.push(sector_seeds, e)
end
break
end
end
end
end
if ground_distance > 1 then
if ground_distance <= class.jump_height + 1 then
local ledges = {
{x=pos.x + class.size.x, y=pos.y - 1, z=pos.z},
{x=pos.x - 1, y=pos.y - 1, z=pos.z},
{x=pos.x, y=pos.y - 1, z=pos.z + class.size.z},
{x=pos.x, y=pos.y - 1, z=pos.z - 1},
}
for _,l in ipairs(ledges) do
local node = minetest.get_node_or_nil(l)
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
return 1 + ground_distance
end
should_refresh_distances = true
else
table.insert(unready_seeds, seed)
end
end
-- Check if this is a fall
if parent.y < pos.y then
return 2
end
return math.huge
end
-- Update sector distance values
if should_refresh_distances then
calculate_distances(class)
end
-- Requeue seeds outside of loaded area
for _,s in ipairs(unready_seeds) do
Queue.push(sector_seeds, s)
end
defense:log(class.name .. ": There are " .. sector_count .. " sectors, " .. Queue.size(sector_seeds) .. " seeds.")
-- Prune excess sectors
if sector_count + Queue.size(sector_seeds) >= max_sector_count then
prune_sectors(class)
end
end
local function update()
for n,c in pairs(classes) do
update_class(c)
end
end
-- Cost methods
pathfinder.default_cost_method = {}
function pathfinder.default_cost_method.air(src_sector, dst_sector)
local dx = ((dst_sector.min_x + dst_sector.max_x) - (src_sector.min_x + src_sector.max_x)) / 2
local dy = ((dst_sector.min_y + dst_sector.max_y) - (src_sector.min_y + src_sector.max_y)) / 2
local dz = ((dst_sector.min_z + dst_sector.max_z) - (src_sector.min_z + src_sector.max_z)) / 2
return math.sqrt(dx*dx + dy*dy + dz*dz)
end
function pathfinder.default_cost_method.ground(src_sector, dst_sector)
return 1
end
function pathfinder.cost_method.crawl(class, pos, parent)
-- Check if in solid
for y=pos.y,pos.y+class.size.y-1 do
for z=pos.z,pos.z+class.size.z-1 do
for x=pos.x,pos.x+class.size.x-1 do
local node = minetest.get_node_or_nil({x=x, y=y, z=z})
function pathfinder.default_cost_method.crawl(src_sector, dst_sector)
return 1
end
-- Path checks
pathfinder.default_path_check = {}
function pathfinder.default_path_check.air(class, pos, parent)
local tmp_vec = vector.new()
for z = pos.z, pos.z + class.z_size - 1 do
for y = pos.y, pos.y + class.y_size - 1 do
for x = pos.x, pos.x + class.x_size - 1 do
tmp_vec.x = x
tmp_vec.y = y
tmp_vec.z = z
local node = minetest.get_node_or_nil(tmp_vec)
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
return math.huge
if reg_nodes[node.name].walkable then
return false
end
end
end
end
-- Check if touching solid
-- xz-plane
for x=pos.x-1,pos.x+class.size.x do
for z=pos.z-1,pos.z+class.size.z do
local node_n = minetest.get_node_or_nil({x=x, y=pos.y-1, z=z})
if not node_n then return nil end
if minetest.registered_nodes[node_n.name].walkable then
return 1
end
local node_p = minetest.get_node_or_nil({x=x, y=pos.y+class.size.y, z=z})
if not node_p then return nil end
if minetest.registered_nodes[node_p.name].walkable then
return 1
end
end
end
-- xy-plane
for x=pos.x,pos.x+class.size.x-1 do
for y=pos.y,pos.y+class.size.y-1 do
local node_n = minetest.get_node_or_nil({x=x, y=y, z=pos.z-1})
if not node_n then return nil end
if minetest.registered_nodes[node_n.name].walkable then
return 1
end
local node_p = minetest.get_node_or_nil({x=x, y=y, z=pos.z+class.size.z})
if not node_p then return nil end
if minetest.registered_nodes[node_p.name].walkable then
return 1
end
end
end
-- yz-plane
for y=pos.y,pos.y+class.size.y-1 do
for z=pos.z,pos.z+class.size.z-1 do
local node_n = minetest.get_node_or_nil({x=pos.x-1, y=y, z=z})
if not node_n then return nil end
if minetest.registered_nodes[node_n.name].walkable then
return 1
end
local node_p = minetest.get_node_or_nil({x=pos.x+class.size.z, y=y, z=z})
if not node_p then return nil end
if minetest.registered_nodes[node_p.name].walkable then
return 1
end
end
end
return math.huge
return true
end
function pathfinder.default_path_check.ground(class, pos, parent)
return false
end
function pathfinder.default_path_check.crawl(class, pos, parent)
return false
end
local last_update_time = 0
minetest.register_globalstep(function(dtime)
pathfinder:update(dtime)
local gt = minetest.get_gametime()
if last_update_time + pathfinder.update_interval < gt then
update()
last_update_time = gt
end
end)