Rewrote pathfinder to use navigational sectors and "waypoints" instead of grids.
parent
dbb81e6c7a
commit
11e4d9828f
|
@ -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)
|
|
@ -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)
|
|
@ -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")
|
||||
|
||||
|
@ -30,3 +42,5 @@ dofile2("mobs/paniki.lua")
|
|||
dofile2("mobs/botete.lua")
|
||||
|
||||
dofile2("debug.lua")
|
||||
|
||||
defense:toggle_debug(true)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue