From 59602c9d36e1a3be863d070a9bc5cba5644354a7 Mon Sep 17 00:00:00 2001 From: ElCeejo Date: Fri, 9 Jun 2023 23:15:07 -0700 Subject: [PATCH] Updated Physics and Vitals, New Pathfinding --- api.lua | 317 ++-- doc.txt | 25 +- init.lua | 3 +- methods.lua | 88 +- mob_meta.lua | 181 +-- pathfinder.lua => pathfinder_deprecated.lua | 1610 +++++++++---------- pathfinding.lua | 620 +++++++ spawning.lua | 38 +- 8 files changed, 1746 insertions(+), 1136 deletions(-) rename pathfinder.lua => pathfinder_deprecated.lua (96%) create mode 100644 pathfinding.lua diff --git a/api.lua b/api.lua index a9bd763..1d9dafb 100644 --- a/api.lua +++ b/api.lua @@ -6,6 +6,7 @@ creatura.api = {} -- Math -- +local abs = math.abs local floor = math.floor local random = math.random @@ -19,12 +20,6 @@ local function clamp(val, min_n, max_n) end local vec_dist = vector.distance -local vec_equals = vector.equals -local vec_add = vector.add - -local function vec_center(v) - return {x = floor(v.x + 0.5), y = floor(v.y + 0.5), z = floor(v.z + 0.5)} -end local function vec_raise(v, n) if not v then return end @@ -190,7 +185,61 @@ function creatura.is_pos_moveable(pos, width, height) return true end -local moveable = creatura.is_pos_moveable +local function is_blocked_thin(pos, height) + local node + local pos2 = { + x = floor(pos.x + 0.5), + y = floor(pos.y + 0.5) - 1, + z = floor(pos.z + 0.5) + } + + for _ = 1, height do + pos2.y = pos2.y + 1 + node = minetest.get_node_or_nil(pos2) + + if not node + or get_node_def(node.name).walkable then + return true + end + end + return false +end + +function creatura.is_blocked(pos, width, height) + if width <= 0.5 then + return is_blocked_thin(pos, height) + end + + local p1 = { + x = pos.x - (width + 0.2), + y = pos.y, + z = pos.z - (width + 0.2), + } + local p2 = { + x = pos.x + (width + 0.2), + y = pos.y + (height + 0.2), + z = pos.z + (width + 0.2), + } + + local node + local pos2 = {} + for z = p1.z, p2.z do + pos2.z = z + for y = p1.y, p2.y do + pos2.y = y + for x = p1.x, p2.x do + pos2.x = x + node = minetest.get_node_or_nil(pos2) + + if not node + or get_node_def(node.name).walkable then + return true + end + end + end + end + return false +end function creatura.fast_ray_sight(pos1, pos2, water) local ray = minetest.raycast(pos1, pos2, false, water or false) @@ -207,127 +256,6 @@ end local fast_ray_sight = creatura.fast_ray_sight -function creatura.get_next_move(self, pos2) - local last_move = self._movement_data.last_move - local width = self.width - local height = self.height - local pos = self.object:get_pos() - pos = { - x = floor(pos.x), - y = pos.y + 0.01, - z = floor(pos.z) - } - pos.y = pos.y + 0.01 - if last_move - and last_move.pos then - local last_call = minetest.get_position_from_hash(last_move.pos) - last_move = minetest.get_position_from_hash(last_move.move) - if vec_equals(vec_center(last_call), vec_center(pos)) then - return last_move - end - end - local neighbors = { - vec_add(pos, {x = 1, y = 0, z = 0}), - vec_add(pos, {x = 1, y = 0, z = 1}), - vec_add(pos, {x = 0, y = 0, z = 1}), - vec_add(pos, {x = -1, y = 0, z = 1}), - vec_add(pos, {x = -1, y = 0, z = 0}), - vec_add(pos, {x = -1, y = 0, z = -1}), - vec_add(pos, {x = 0, y = 0, z = -1}), - vec_add(pos, {x = 1, y = 0, z = -1}) - } - local _next - table.sort(neighbors, function(a, b) - return vec_dist(a, pos2) < vec_dist(b, pos2) - end) - for i = 1, #neighbors do - local neighbor = neighbors[i] - local can_move = fast_ray_sight(pos, neighbor) - if vec_equals(neighbor, pos2) then - can_move = true - end - if can_move - and not moveable(neighbor, width, height) then - can_move = false - if moveable(vec_raise(neighbor, 0.5), width, height) then - can_move = true - end - end - if can_move - and not self:is_pos_safe(neighbor) then - can_move = false - end - if can_move then - _next = vec_raise(neighbor, 0.1) - break - end - end - if _next then - self._movement_data.last_move = { - pos = minetest.hash_node_position(pos), - move = minetest.hash_node_position(_next) - } - _next = { - x = floor(_next.x), - y = _next.y, - z = floor(_next.z) - } - end - return _next -end - -function creatura.get_next_move_3d(self, pos2) - local last_move = self._movement_data.last_move - local width = self.width - local height = self.height - local scan_width = width * 2 - local pos = self.object:get_pos() - pos.y = pos.y + 0.5 - if last_move - and last_move.pos then - local last_call = minetest.get_position_from_hash(last_move.pos) - last_move = minetest.get_position_from_hash(last_move.move) - if vec_equals(vec_center(last_call), vec_center(pos)) then - return last_move - end - end - local neighbors = { - vec_add(pos, {x = scan_width, y = 0, z = 0}), - vec_add(pos, {x = scan_width, y = 0, z = scan_width}), - vec_add(pos, {x = 0, y = 0, z = scan_width}), - vec_add(pos, {x = -scan_width, y = 0, z = scan_width}), - vec_add(pos, {x = -scan_width, y = 0, z = 0}), - vec_add(pos, {x = -scan_width, y = 0, z = -scan_width}), - vec_add(pos, {x = 0, y = 0, z = -scan_width}), - vec_add(pos, {x = scan_width, y = 0, z = -scan_width}) - } - local next - table.sort(neighbors, function(a, b) - return vec_dist(a, pos2) < vec_dist(b, pos2) - end) - for i = 1, #neighbors do - local neighbor = neighbors[i] - local can_move = fast_ray_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) - if not moveable(vec_raise(neighbor, 0.6), width, height) then - can_move = false - end - if vec_equals(neighbor, pos2) then - can_move = true - end - if can_move then - next = neighbor - break - end - end - if next then - self._movement_data.last_move = { - pos = minetest.hash_node_position(pos), - move = minetest.hash_node_position(next) - } - end - return vec_raise(next, clamp((pos2.y - pos.y) + -0.6, -1, 1)) -end - function creatura.sensor_floor(self, range, water) local pos = self.object:get_pos() local pos2 = vec_raise(pos, -range) @@ -413,6 +341,139 @@ creatura.get_nearby_entities = creatura.get_nearby_objects -- Global Mob API -- -------------------- +function creatura.default_water_physics(self) + local pos = self.stand_pos + local stand_node = self.stand_node + if not pos or not stand_node then return end + local gravity = self._movement_data.gravity or -9.8 + local submergence = self.liquid_submergence or 0.25 + local drag = self.liquid_drag or 0.7 + + if minetest.get_item_group(stand_node.name, "liquid") > 0 then -- In Liquid + local vel = self.object:get_velocity() + if not vel then return end + + self.in_liquid = stand_node.name + + if submergence < 1 then + local mob_level = pos.y + (self.height * submergence) + + -- Find Water Surface + local nodes = minetest.find_nodes_in_area_under_air( + {x = pos.x, y = pos.y, z = pos.z}, + {x = pos.x, y = pos.y + 3, z = pos.z}, + "group:liquid" + ) or {} + + local surface_level = (#nodes > 0 and nodes[#nodes].y or pos.y + self.height + 3) + surface_level = floor(surface_level + 0.9) + + local height_diff = mob_level - surface_level + + -- Apply Bouyancy + if height_diff <= 0 then + local displacement = clamp(abs(height_diff) / submergence, 0.5, 1) * self.width + + self.object:set_acceleration({x = 0, y = displacement, z = 0}) + else + self.object:set_acceleration({x = 0, y = gravity, z = 0}) + end + end + + -- Apply Drag + self.object:set_velocity({ + x = vel.x * (1 - self.dtime * drag), + y = vel.y * (1 - self.dtime * drag), + z = vel.z * (1 - self.dtime * drag) + }) + else + self.in_liquid = nil + + self.object:set_acceleration({x = 0, y = gravity, z = 0}) + end +end + +function creatura.default_vitals(self) + local pos = self.stand_pos + local node = self.stand_node + if not pos or node then return end + + local max_fall = self.max_fall or 3 + local in_liquid = self.in_liquid + local on_ground = self.touching_ground + local damage = 0 + + -- Fall Damage + if max_fall > 0 + and not in_liquid then + local fall_start = self._fall_start or (not on_ground and pos.y) + if fall_start + and on_ground then + damage = floor(fall_start - pos.y) + if damage < max_fall then + damage = 0 + else + local resist = self.fall_resistance or 0 + damage = damage - damage * resist + end + fall_start = nil + end + self._fall_start = fall_start + end + + -- Environment Damage + if self:timer(1) then + local stand_def = creatura.get_node_def(node.name) + local max_breath = self.max_breath or 0 + + -- Suffocation + if max_breath > 0 then + local head_pos = {x = pos.x, y = pos.y + self.height, z = pos.z} + local head_def = creatura.get_node_def(head_pos) + if head_def.groups + and (minetest.get_item_group(head_def.name, "water") > 0 + or (head_def.walkable + and head_def.groups.disable_suffocation ~= 1 + and head_def.drawtype == "normal")) then + local breath = self._breath + if breath <= 0 then + damage = damage + 1 + else + self._breath = breath - 1 + self:memorize("_breath", breath) + end + end + end + + -- Burning + local fire_resist = self.fire_resistance or 0 + if fire_resist < 1 + and minetest.get_item_group(stand_def.name, "igniter") > 0 + and stand_def.damage_per_second then + damage = (damage or 0) + stand_def.damage_per_second * fire_resist + end + end + + -- Apply Damage + if damage > 0 then + self:hurt(damage) + self:indicate_damage() + if random(4) < 2 then + self:play_sound("hurt") + end + end + + -- Entity Cramming + if self:timer(5) then + local objects = minetest.get_objects_inside_radius(pos, 0.2) + if #objects > 10 then + self:indicate_damage() + self.hp = self:memorize("hp", -1) + self:death_func() + end + end +end + function creatura.drop_items(self) if not self.drops then return end local pos = self.object:get_pos() diff --git a/doc.txt b/doc.txt index 85d7aae..494d9c6 100644 --- a/doc.txt +++ b/doc.txt @@ -213,4 +213,27 @@ Global Mob API * `creatura.basic_punch_func(self, puncher, time_from_last_punch, tool_capabilities, direction, damage)` * Deals damage * Applies knockback - * Visualy and audibly indicates damage \ No newline at end of file + * Visualy and audibly indicates damage + +Pathfinding +----------- + +Creatura's pathfinder uses the A* algorithm for speed, as well as Theta* for decent performance and more natural looking paths. + +Both pathfinders will carry out pathfinding over multiple server steps to reduce lag spikes which does result in the path not +being returned immediately, so your code will have to account for this. + +The maximum amount of time the pathfinder can spend per-step (in microseconds) can be adjusted in settings. + + +* `creatura.pathfinder.find_path(self, pos1, pos2, get_neighbors)` + * Finds a path from `pos1` to `pos2` + * `get_neighbors` is a function used to find valid neighbors + * `creatura.pathfinder.get_neighbors_fly` and `creatura.pathfinder.get_neighbors_swim` are bundled by default + + +* `creatura.pathfinder.find_path_theta(self, pos1, pos2, get_neighbors)` + * Finds a path from `pos1` to `pos2` + * Returns a path with arbitrary angles for natural looking paths at the expense of performance + * `get_neighbors` is a function used to find valid neighbors + * `creatura.pathfinder.get_neighbors_fly` and `creatura.pathfinder.get_neighbors_swim` are bundled by default \ No newline at end of file diff --git a/init.lua b/init.lua index a5b920d..8970300 100644 --- a/init.lua +++ b/init.lua @@ -3,7 +3,8 @@ creatura = {} local path = minetest.get_modpath("creatura") dofile(path.."/api.lua") -dofile(path.."/pathfinder.lua") +dofile(path.."/pathfinding.lua") +dofile(path.."/pathfinder_deprecated.lua") dofile(path.."/methods.lua") -- Optional Files -- diff --git a/methods.lua b/methods.lua index d86d423..70cd022 100644 --- a/methods.lua +++ b/methods.lua @@ -468,49 +468,41 @@ end return path end]] -creatura.register_movement_method("creatura:theta_pathfind", function(self) +creatura.register_movement_method("creatura:pathfind_theta", function(self) local path = {} - local box = clamp(self.width, 0.5, 1.5) + local steer_to + local steer_int = 0 + local arrival_threshold = clamp(self.width, 0.5, 1) + + self:set_gravity(-9.8) local function func(_self, goal, speed_factor) local pos = _self.object:get_pos() - if not pos then return end - pos.y = pos.y + 0.5 - -- Return true when goal is reached - if vec_dist(pos, goal) < box * 1.33 then + if not pos or not goal then return end + + if vec_dist(pos, goal) < arrival_threshold then _self:halt() return true end - self:set_gravity(-9.8) - -- Get movement direction - local steer_to = get_avoidance_dir(_self, goal) - local goal_dir = vec_dir(pos, goal) - if steer_to then - goal_dir = steer_to - if #path < 1 then - path = creatura.find_theta_path(_self, pos, goal, _self.width, _self.height, 300) or {} - end - end - if #path > 0 then - goal_dir = vec_dir(pos, path[2] or path[1]) - if vec_dist(pos, path[1]) < box then - table.remove(path, 1) - end - end - local yaw = _self.object:get_yaw() - local goal_yaw = dir2yaw(goal_dir) - local speed = abs(_self.speed or 2) * speed_factor or 0.5 + + -- Calculate Movement local turn_rate = abs(_self.turn_rate or 5) - -- Movement - local yaw_diff = abs(diff(yaw, goal_yaw)) - if yaw_diff < pi * 0.25 - or steer_to then - _self:set_forward_velocity(speed) - else - _self:set_forward_velocity(speed * 0.33) - end - if yaw_diff > 0.1 then - _self:turn_to(goal_yaw, turn_rate) + local speed = abs(_self.speed or 2) * speed_factor or 0.5 + local path_dir = #path > 0 and vec_dir(pos, path[2] or path[1]) + + steer_int = (steer_int > 0 and steer_int - _self.dtime) or 1 / math.max(speed, 1) + steer_to = path_dir or (steer_int <= 0 and creatura.calc_steering(_self, goal)) or steer_to + + path = (#path > 0 and path) or (creatura.pathfinder.find_path_theta(_self, pos, goal) or {}) + + if path_dir + and ((path[2] and vec_dist(pos, path[2]) < arrival_threshold) + or vec_dist(pos, path[1]) < arrival_threshold) then + table.remove(path, 1) end + + -- Apply Movement + _self:turn_to(dir2yaw(steer_to or vec_dir(pos, goal)), turn_rate) + _self:set_forward_velocity(speed) end return func end) @@ -519,25 +511,34 @@ creatura.register_movement_method("creatura:pathfind", function(self) local path = {} local steer_to local steer_int = 0 + local arrival_threshold = clamp(self.width, 0.5, 1) + self:set_gravity(-9.8) local function func(_self, goal, speed_factor) local pos = _self.object:get_pos() if not pos or not goal then return end - if vec_dist(pos, goal) < clamp(self.width, 0.5, 1) then + + if vec_dist(pos, goal) < arrival_threshold then _self:halt() return true end + -- Calculate Movement local turn_rate = abs(_self.turn_rate or 5) local speed = abs(_self.speed or 2) * speed_factor or 0.5 - steer_int = (not steer_to and steer_int > 0 and steer_int - _self.dtime) or 1 / math.max(speed, 1) - steer_to = (steer_int <= 0 and creatura.calc_steering(_self, goal)) or steer_to - if steer_to then - path = creatura.find_lvm_path(_self, pos, goal, _self.width, _self.height, 400) or {} - if #path > 0 then - steer_to = vec_dir(pos, path[2] or path[1]) - end + local path_dir = #path > 0 and vec_dir(pos, path[2] or path[1]) + + steer_int = (steer_int > 0 and steer_int - _self.dtime) or 1 / math.max(speed, 1) + steer_to = path_dir or (steer_int <= 0 and creatura.calc_steering(_self, goal)) or steer_to + + path = (#path > 0 and path) or (creatura.pathfinder.find_path(_self, pos, goal) or {}) + + if path_dir + and ((path[2] and vec_dist(pos, path[2]) < arrival_threshold + 0.5) + or vec_dist(pos, path[1]) < arrival_threshold) then + table.remove(path, 1) end + -- Apply Movement _self:turn_to(dir2yaw(steer_to or vec_dir(pos, goal)), turn_rate) _self:set_forward_velocity(speed) @@ -545,6 +546,7 @@ creatura.register_movement_method("creatura:pathfind", function(self) return func end) + -- Steering creatura.register_movement_method("creatura:steer_small", function(self) diff --git a/mob_meta.lua b/mob_meta.lua index 89a07e2..d9e84a8 100644 --- a/mob_meta.lua +++ b/mob_meta.lua @@ -87,8 +87,8 @@ local mob = { }, follow = {}, fancy_collide = false, - bouyancy_multiplier = 1, - hydrodynamics_multiplier = 1 + liquid_submergence = 0.25, + liquid_drag = 1 } @@ -505,12 +505,11 @@ function mob:set_mesh(id) self.object:set_properties({ mesh = meshes[mesh_no] }) - self:memorize("mesh_no", self.mesh_no) + self.mesh_no = mesh_no if self.mesh_textures then self.textures = self.mesh_textures[mesh_no] self.texture_no = random(#self.textures) self:set_texture(self.texture_no, self.textures) - self:memorize("texture_no", self.texture_no) end return meshes[mesh_no] end @@ -818,22 +817,20 @@ function mob:activate(staticdata, dtime) end -- Initialize Stats and Visuals + if self.meshes + and #self.meshes > 0 then + if not self.mesh_no + or not self.meshes[self.mesh_no] then + self.mesh_no = random(#self.meshes) + end + self:set_mesh(self.mesh_no) + end + if not self.textures then local textures = self:get_props().textures if textures then self.textures = textures end end - if self.meshes then - local mesh_no = self.mesh_no or random(#self.meshes) - if self.mesh_textures then - self.textures = self.mesh_textures[mesh_no] - end - self.mesh_no = mesh_no - self.object:set_properties({ - mesh = self.meshes[mesh_no] - }) - end - if not self.perm_data then if self.memory then self.perm_data = self.memory @@ -857,11 +854,14 @@ function mob:activate(staticdata, dtime) return end - self._breath = self:recall("_breath") or (self.max_breath or 30) + self._breath = self:recall("_breath") or (self.max_breath or 30) --self._border = index_box_border(self) if self.textures and self.texture_no then + if not self.textures[self.texture_no] then + self.texture_no = random(#self.textures) + end self:set_texture(self.texture_no, self.textures) end @@ -960,11 +960,11 @@ function mob:on_step(dtime, moveresult) end end -function mob:on_deactivate() +function mob:on_deactivate(removal) self._task = {} self._action = {} if self.deactivate_func then - self:deactivate_func(self) + self:deactivate_func(removal) end end @@ -1005,65 +1005,14 @@ local function collision_detection(self) end end -local function water_physics(self, pos, node) - -- Props - local gravity = self._movement_data.gravity - local height = self.height - -- Vectors - pos.y = pos.y + 0.01 - if minetest.get_item_group(node.name, "liquid") < 1 then - self.object:set_acceleration({ - x = 0, - y = gravity, - z = 0 - }) - if self.in_liquid then - self.in_liquid = false - end - return - end - self.in_liquid = node.name - self.object:set_acceleration({ - x = 0, - y = gravity * 0.5, - z = 0 - }) - local center = { - x = pos.x, - y = pos.y + height * 0.5, - z = pos.z - } - if minetest.get_item_group(minetest.get_node(center).name, "liquid") < 1 then - return - end - -- Calculate Physics - local vel = self.object:get_velocity() - local bouyancy_x = self.bouyancy_multiplier or 1 - local bouyancy = (abs(gravity * 0.5) / height) * bouyancy_x - if bouyancy > 0 then - if bouyancy > 4.9 then bouyancy = 4.9 end - self.object:set_acceleration({ - x = 0, - y = 0, - z = 0 - }) - vel.y = vel.y + (bouyancy - vel.y) * (self.dtime * 0.5) - end - local hydrodynamics_x = self.hydrodynamics_multiplier or 0.7 - vel.x = vel.x * hydrodynamics_x - vel.y = vel.y * ((bouyancy == 0 and hydrodynamics_x) or 1) - vel.z = vel.z * hydrodynamics_x - -- Apply Physics - self.object:set_velocity(vel) -end +local mob_friction = 7 function mob:_physics() - local pos = self.stand_pos - local node = self.stand_node - if not pos or not node then return end - water_physics(self, pos, node) - -- Object collision + -- Physics + creatura.default_water_physics(self) collision_detection(self) + + -- Cache Environment Info local in_liquid = self.in_liquid local on_ground = self.touching_ground if not in_liquid @@ -1077,9 +1026,7 @@ function mob:_physics() --and not move_data.func and move_data.gravity ~= 0 then local vel = self.object:get_velocity() - local friction = self.dtime * 10 - if friction > 0.5 then friction = 0.5 end - if not on_ground then friction = 0.25 end + local friction = math.min(self.dtime * mob_friction, 0.5) local nvel = {x = vel.x * (1 - friction), y = vel.y, z = vel.z * (1 - friction)} self.object:set_velocity(nvel) end @@ -1124,6 +1071,8 @@ function mob:_execute_utilities() step_delay = nil, score = 0 } + end + if not self._util_cooldown then self._util_cooldown = {} end local loop_data = { @@ -1182,6 +1131,7 @@ function mob:_execute_utilities() self._util_cooldown[i] = cooldown end end + if loop_data.utility and loop_data.args then if not self._utility_data @@ -1197,7 +1147,8 @@ function mob:_execute_utilities() end end end - if self._utility_data.utility then + + if self._utility_data.utility then -- If a utility is currently selected local util_data = self._utility_data if not util_data.func then self:initiate_utility(util_data.utility, unpack(util_data.args)) @@ -1255,78 +1206,6 @@ end -- Vitals -function mob:_vitals() - local pos = self.stand_pos - local node = self.stand_node - if not pos or not node then return end - local max_fall = self.max_fall or 3 - local in_liquid = self.in_liquid - local on_ground = self.touching_ground - local damage = 0 - if max_fall > 0 - and not in_liquid then - local fall_start = self._fall_start or (not on_ground and pos.y) - if fall_start then - if on_ground then - damage = fall_start - pos.y - if damage < max_fall then - damage = 0 - else - local resist = self.fall_resistance or 0 - damage = damage - damage * resist - end - fall_start = nil - end - end - self._fall_start = fall_start - end - if self:timer(1) then - local stand_def = creatura.get_node_def(node.name) - local max_breath = self.max_breath - if not max_breath - or max_breath > 0 then - local breath = self._breath or max_breath - local head_pos = vec_raise(pos, self.height - 0.01) - local head_def = creatura.get_node_def(head_pos) - if minetest.get_item_group(head_def.name, "liquid") > 0 - or (head_def.walkable - and head_def.drawtype == "normal") then - if breath <= 0 then - damage = (damage or 0) + 1 - else - breath = breath - 1 - end - else - breath = (breath < max_breath and breath + 1) or max_breath - end - self._breath = self:memorize("_breath", breath) - end - if (not self.fire_resistance - or self.fire_resistance < 1) - and minetest.get_item_group(stand_def.name, "igniter") > 0 - and stand_def.damage_per_second then - local resist = self.fire_resistance or 0.5 - damage = (damage or 0) + stand_def.damage_per_second * resist - end - end - if damage > 0 then - self:hurt(damage) - self:indicate_damage() - if random(4) < 2 then - self:play_sound("hurt") - end - end - -- Entity Cramming - if self:timer(5) then - local objects = minetest.get_objects_inside_radius(pos, 0.2) - if #objects > 10 then - self:indicate_damage() - self.hp = self:memorize("hp", -1) - self:death_func() - end - end -end - function creatura.register_mob(name, def) local box_width = def.hitbox and def.hitbox.width or 0.5 local box_height = def.hitbox and def.hitbox.height or 1 @@ -1354,6 +1233,8 @@ function creatura.register_mob(name, def) } end + def._vitals = def._vitals or creatura.default_vitals + def.on_activate = function(self, staticdata, dtime) return self:activate(staticdata, dtime) end diff --git a/pathfinder.lua b/pathfinder_deprecated.lua similarity index 96% rename from pathfinder.lua rename to pathfinder_deprecated.lua index 83d5ea8..6c7aa64 100644 --- a/pathfinder.lua +++ b/pathfinder_deprecated.lua @@ -1,805 +1,805 @@ ------------------ --- Pathfinding -- ------------------ - -local a_star_alloted_time = tonumber(minetest.settings:get("creatura_a_star_alloted_time")) or 500 -local theta_star_alloted_time = tonumber(minetest.settings:get("creatura_theta_star_alloted_time")) or 700 - -local floor = math.floor -local abs = math.abs - -local vec_dist, vec_round = vector.distance, vector.round - -local moveable = creatura.is_pos_moveable - -local function get_distance(start_pos, end_pos) - local distX = abs(start_pos.x - end_pos.x) - local distZ = abs(start_pos.z - end_pos.z) - - if distX > distZ then - return 14 * distZ + 10 * (distX - distZ) - else - return 14 * distX + 10 * (distZ - distX) - end -end - -local function get_distance_to_neighbor(start_pos, end_pos) - local distX = abs(start_pos.x - end_pos.x) - local distY = abs(start_pos.y - end_pos.y) - local distZ = abs(start_pos.z - end_pos.z) - - if distX > distZ then - return (14 * distZ + 10 * (distX - distZ)) * (distY + 1) - else - return (14 * distX + 10 * (distZ - distX)) * (distY + 1) - end -end - -local function is_on_ground(pos) - local ground = { - x = pos.x, - y = pos.y - 1, - z = pos.z - } - if creatura.get_node_def(ground).walkable then - return true - end - return false -end - -local function vec_raise(v, n) - return {x = v.x, y = v.y + n, z = v.z} -end - -local function get_line_of_sight(a, b) - local steps = floor(vec_dist(a, b)) - local line = {} - - for i = 0, steps do - local pos - - if steps > 0 then - pos = { - x = a.x + (b.x - a.x) * (i / steps), - y = a.y + (b.y - a.y) * (i / steps), - z = a.z + (b.z - a.z) * (i / steps) - } - else - pos = a - end - table.insert(line, pos) - end - - if #line < 1 then - return false - else - for i = 1, #line do - local node = minetest.get_node(line[i]) - if creatura.get_node_def(node.name).walkable then - return false - end - end - end - return true -end - --- Find a path from start to goal - ---[[local function debugpart(pos, time, tex) - minetest.add_particle({ - pos = pos, - texture = tex or "creatura_particle_red.png", - expirationtime = time or 0.1, - glow = 6, - size = 12 - }) -end]] - -local c_air = minetest.get_content_id("air") - -local function is_pos_moveable_vm(pos, width, height, area, data) - pos = vector.round(pos) - local pos1 = { - x = pos.x - math.ceil(width), - y = pos.y, - z = pos.z - math.ceil(width) - } - local pos2 = { - x = pos.x + math.ceil(width), - y = pos.y + math.ceil(height), - z = pos.z + math.ceil(width) - } - for z = pos1.z, pos2.z do - for y = pos1.y, pos2.y do - for x = pos1.x, pos2.x do - if not area:contains(x, y, z) then return false end - local vi = area:index(x, y, z) - local c = data[vi] - if c ~= c_air then - local c_name = minetest.get_name_from_content_id(c) - if creatura.get_node_def(c_name).walkable then - return false - end - end - end - end - end - return true -end - -local vm_buffer = {} - -function creatura.find_lvm_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) - climb = climb or false - fly = fly or false - swim = swim or false - - if vec_dist(start, goal) > (self.tracking_range or 128) then return {} end - - self._path_data.start = start - - local path_neighbors = { - {x = 1, y = 0, z = 0}, - {x = 1, y = 0, z = 1}, - {x = 0, y = 0, z = 1}, - {x = -1, y = 0, z = 1}, - {x = -1, y = 0, z = 0}, - {x = -1, y = 0, z = -1}, - {x = 0, y = 0, z = -1}, - {x = 1, y = 0, z = -1} - } - - if climb then - table.insert(path_neighbors, {x = 0, y = 1, z = 0}) - end - - if fly - or swim then - path_neighbors = { - -- Central - {x = 1, y = 0, z = 0}, - {x = 0, y = 0, z = 1}, - {x = -1, y = 0, z = 0}, - {x = 0, y = 0, z = -1}, - -- Directly Up or Down - {x = 0, y = 1, z = 0}, - {x = 0, y = -1, z = 0} - } - end - - - local function get_neighbors(pos, width, height, tbl, open, closed, vm_area, vm_data) - local result = {} - for i = 1, #tbl do - local neighbor = vector.add(pos, tbl[i]) - if not vm_area or not vm_data or not vm_area:containsp(neighbor) then return end - local can_move = (not swim and get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)) or true - if open[minetest.hash_node_position(neighbor)] - or closed[minetest.hash_node_position(neighbor)] then - can_move = false - end - if can_move then - can_move = is_pos_moveable_vm(neighbor, width, height, vm_area, vm_data) - if not fly and not swim then - if not can_move then -- Step Up - local step = vec_raise(neighbor, 1) - can_move = is_pos_moveable_vm(vec_round(step), width, height, vm_area, vm_data) - neighbor = vec_round(step) - else - local step = creatura.get_ground_level(vector.new(neighbor), 1) - if step.y < neighbor.y - and is_pos_moveable_vm(vec_round(step), width, height, vm_area, vm_data) then - neighbor = step - end - end - end - end - if vector.equals(neighbor, goal) then - can_move = true - end - if can_move - and (not swim - or creatura.get_node_def(neighbor).drawtype == "liquid") then - table.insert(result, neighbor) - end - end - return result - end - - local function find_path(_start, _goal) - local us_time = minetest.get_us_time() - - _start = { - x = floor(_start.x + 0.5), - y = floor(_start.y + 0.5), - z = floor(_start.z + 0.5) - } - - _goal = { - x = floor(_goal.x + 0.5), - y = floor(_goal.y + 0.5), - z = floor(_goal.z + 0.5) - } - - if _goal.x == _start.x - and _goal.z == _start.z then -- No path can be found - return nil - end - - local vm_area = self._path_data.vm_area - local vm_data = self._path_data.vm_data - - if not vm_area - or not vm_data then - local vm_center = vector.add(_start, vector.divide(vector.subtract(_goal, _start), 2)) - local vm_size = vec_dist(_goal, _start) - if vm_size < 24 then vm_size = 24 end - local e1 = vector.subtract(vm_center, vm_size) - local e2 = vector.add(vm_center, vm_size) - local vm = minetest.get_voxel_manip(e1, e2) - e1, e2 = vm:read_from_map(e1, e2) - vm_area = VoxelArea:new{MinEdge=e1, MaxEdge=e2} - vm_data = vm:get_data(vm_buffer) - end - - local openSet = self._path_data.open or {} - - local closedSet = self._path_data.closed or {} - - local start_index = minetest.hash_node_position(_start) - - openSet[start_index] = { - pos = _start, - parent = nil, - gScore = 0, - fScore = get_distance(_start, _goal) - } - - local count = self._path_data.count or 1 - - while count > 0 do - -- Initialize ID and data - local current_id, current = next(openSet) - - -- Find lowest f cost - for i, v in pairs(openSet) do - if v.fScore < current.fScore then - current_id = i - current = v - end - end - - -- Add lowest fScore to closedSet and remove from openSet - openSet[current_id] = nil - closedSet[current_id] = current - - self._path_data.open = openSet - self._path_data.closedSet = closedSet - - local current_start = vec_round(self._path_data.start) - - if closedSet[minetest.hash_node_position(current_start)] then - start_index = minetest.hash_node_position(current_start) - end - - -- Reconstruct path if end is reached - if ((is_on_ground(_goal) - or fly) - and current_id == minetest.hash_node_position(_goal)) - or (not fly - and not is_on_ground(_goal) - and math.abs(_goal.x - current.pos.x) < 1.1 - and math.abs(_goal.z - current.pos.z) < 1.1) then - local path = {} - local fail_safe = 0 - for _ in pairs(closedSet) do - fail_safe = fail_safe + 1 - end - repeat - if not closedSet[current_id] then return end - table.insert(path, closedSet[current_id].pos) - current_id = closedSet[current_id].parent - until current_id == start_index or #path >= fail_safe - if not closedSet[current_id] then self._path_data = {} return nil end - table.insert(path, closedSet[current_id].pos) - local reverse_path = {} - repeat table.insert(reverse_path, table.remove(path)) until #path == 0 - self._path_data = {} - return reverse_path - end - - count = count - 1 - - local adjacent = get_neighbors( - current.pos, - obj_width, - obj_height, - path_neighbors, - openSet, - closedSet, - vm_area, - vm_data - ) - - -- Go through neighboring nodes - if not adjacent or #adjacent < 1 then self._path_data = {} return {} end - for i = 1, #adjacent do - local neighbor = { - pos = adjacent[i], - parent = current_id, - gScore = 0, - fScore = 0 - } - local temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos) - local new_gScore = 0 - if openSet[minetest.hash_node_position(neighbor.pos)] then - new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore - end - if (temp_gScore < new_gScore - or not openSet[minetest.hash_node_position(neighbor.pos)]) - and not closedSet[minetest.hash_node_position(neighbor.pos)] then - if not openSet[minetest.hash_node_position(neighbor.pos)] then - count = count + 1 - end - local hCost = get_distance_to_neighbor(neighbor.pos, _goal) - neighbor.gScore = temp_gScore - neighbor.fScore = temp_gScore + hCost - openSet[minetest.hash_node_position(neighbor.pos)] = neighbor - end - end - if minetest.get_us_time() - us_time > a_star_alloted_time then - self._path_data = { - start = _start, - open = openSet, - closed = closedSet, - count = count, - vm_area = vm_area, - vm_data = vm_data - } - return {} - end - if count > (max_open or 100) then - self._path_data = {} - return - end - end - self._path_data = {} - return nil - end - return find_path(start, goal) -end - -function creatura.find_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) - climb = climb or false - fly = fly or false - swim = swim or false - - start = self._path_data.start or start - - self._path_data.start = start - - local path_neighbors = { - {x = 1, y = 0, z = 0}, - {x = 1, y = 0, z = 1}, - {x = 0, y = 0, z = 1}, - {x = -1, y = 0, z = 1}, - {x = -1, y = 0, z = 0}, - {x = -1, y = 0, z = -1}, - {x = 0, y = 0, z = -1}, - {x = 1, y = 0, z = -1} - } - - if climb then - table.insert(path_neighbors, {x = 0, y = 1, z = 0}) - end - - if fly - or swim then - path_neighbors = { - -- Central - {x = 1, y = 0, z = 0}, - {x = 0, y = 0, z = 1}, - {x = -1, y = 0, z = 0}, - {x = 0, y = 0, z = -1}, - -- Directly Up or Down - {x = 0, y = 1, z = 0}, - {x = 0, y = -1, z = 0} - } - end - - local function get_neighbors(pos, width, height, tbl, open, closed) - local result = {} - for i = 1, #tbl do - local neighbor = vector.add(pos, tbl[i]) - local can_move = (not swim and get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)) or true - if open[minetest.hash_node_position(neighbor)] - or closed[minetest.hash_node_position(neighbor)] then - can_move = false - end - if can_move then - can_move = moveable(neighbor, width, height) - if not fly and not swim then - if not can_move then -- Step Up - local step = vec_raise(neighbor, 1) - can_move = moveable(vec_round(step), width, height) - neighbor = vec_round(step) - else - local step = creatura.get_ground_level(vector.new(neighbor), 1) - if step.y < neighbor.y - and moveable(vec_round(step), width, height) then - neighbor = step - end - end - end - end - if vector.equals(neighbor, goal) then - can_move = true - end - if can_move - and (not swim - or creatura.get_node_def(neighbor).drawtype == "liquid") then - table.insert(result, neighbor) - end - end - return result - end - - local function find_path(_start, _goal) - local us_time = minetest.get_us_time() - - _start = { - x = floor(_start.x + 0.5), - y = floor(_start.y + 0.5), - z = floor(_start.z + 0.5) - } - - _goal = { - x = floor(_goal.x + 0.5), - y = floor(_goal.y + 0.5), - z = floor(_goal.z + 0.5) - } - - if _goal.x == _start.x - and _goal.z == _start.z then -- No path can be found - return nil - end - - local openSet = self._path_data.open or {} - - local closedSet = self._path_data.closed or {} - - local start_index = minetest.hash_node_position(_start) - - openSet[start_index] = { - pos = _start, - parent = nil, - gScore = 0, - fScore = get_distance(_start, _goal) - } - - local count = self._path_data.count or 1 - - while count > 0 do - if minetest.get_us_time() - us_time > a_star_alloted_time then - self._path_data = { - start = _start, - open = openSet, - closed = closedSet, - count = count - } - return - end - -- Initialize ID and data - local current_id, current = next(openSet) - - -- Find lowest f cost - for i, v in pairs(openSet) do - if v.fScore < current.fScore then - current_id = i - current = v - end - end - - -- Add lowest fScore to closedSet and remove from openSet - openSet[current_id] = nil - closedSet[current_id] = current - - self._path_data.open = openSet - self._path_data.closedSet = closedSet - - -- Reconstruct path if end is reached - if ((is_on_ground(_goal) - or fly) - and current_id == minetest.hash_node_position(_goal)) - or (not fly - and not is_on_ground(_goal) - and _goal.x == current.pos.x - and _goal.z == current.pos.z) then - local path = {} - local fail_safe = 0 - for _ in pairs(closedSet) do - fail_safe = fail_safe + 1 - end - repeat - if not closedSet[current_id] then return end - table.insert(path, closedSet[current_id].pos) - current_id = closedSet[current_id].parent - until current_id == start_index or #path >= fail_safe - if not closedSet[current_id] then self._path_data = {} return nil end - table.insert(path, closedSet[current_id].pos) - local reverse_path = {} - repeat table.insert(reverse_path, table.remove(path)) until #path == 0 - self._path_data = {} - return reverse_path - end - - count = count - 1 - - local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet) - - -- Go through neighboring nodes - for i = 1, #adjacent do - local neighbor = { - pos = adjacent[i], - parent = current_id, - gScore = 0, - fScore = 0 - } - local temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos) - local new_gScore = 0 - if openSet[minetest.hash_node_position(neighbor.pos)] then - new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore - end - if (temp_gScore < new_gScore - or not openSet[minetest.hash_node_position(neighbor.pos)]) - and not closedSet[minetest.hash_node_position(neighbor.pos)] then - if not openSet[minetest.hash_node_position(neighbor.pos)] then - count = count + 1 - end - local hCost = get_distance_to_neighbor(neighbor.pos, _goal) - neighbor.gScore = temp_gScore - neighbor.fScore = temp_gScore + hCost - openSet[minetest.hash_node_position(neighbor.pos)] = neighbor - end - end - if count > (max_open or 100) then - self._path_data = {} - return - end - end - self._path_data = {} - return nil - end - return find_path(start, goal) -end - ------------- --- Theta* -- ------------- - -function creatura.find_theta_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) - climb = climb or false - fly = fly or false - swim = swim or false - - start = self._path_data.start or start - - self._path_data.start = start - - local path_neighbors = { - {x = 1, y = 0, z = 0}, - {x = 0, y = 0, z = 1}, - {x = -1, y = 0, z = 0}, - {x = 0, y = 0, z = -1}, - } - - if climb then - table.insert(path_neighbors, {x = 0, y = 1, z = 0}) - end - - if fly - or swim then - path_neighbors = { - -- Central - {x = 1, y = 0, z = 0}, - {x = 0, y = 0, z = 1}, - {x = -1, y = 0, z = 0}, - {x = 0, y = 0, z = -1}, - -- Directly Up or Down - {x = 0, y = 1, z = 0}, - {x = 0, y = -1, z = 0} - } - end - - local function get_neighbors(pos, width, height, tbl, open, closed) - local result = {} - for i = 1, #tbl do - local neighbor = vector.add(pos, tbl[i]) - if neighbor.y == pos.y - and not fly - and not swim then - neighbor = creatura.get_ground_level(neighbor, 1) - end - local can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) - if swim then - can_move = true - end - if not moveable(vec_raise(neighbor, -0.49), width, height) then - can_move = false - if neighbor.y == pos.y - and moveable(vec_raise(neighbor, 0.51), width, height) then - neighbor = vec_raise(neighbor, 1) - can_move = true - end - end - if vector.equals(neighbor, goal) then - can_move = true - end - if open[minetest.hash_node_position(neighbor)] - or closed[minetest.hash_node_position(neighbor)] then - can_move = false - end - if can_move - and ((is_on_ground(neighbor) - or (fly or swim)) - or (neighbor.x == pos.x - and neighbor.z == pos.z - and climb)) - and (not swim - or creatura.get_node_def(neighbor).drawtype == "liquid") then - table.insert(result, neighbor) - end - end - return result - end - - local function find_path(_start, _goal) - local us_time = minetest.get_us_time() - - _start = { - x = floor(_start.x + 0.5), - y = floor(_start.y + 0.5), - z = floor(_start.z + 0.5) - } - - _goal = { - x = floor(_goal.x + 0.5), - y = floor(_goal.y + 0.5), - z = floor(_goal.z + 0.5) - } - - if _goal.x == _start.x - and _goal.z == _start.z then -- No path can be found - return nil - end - - local openSet = self._path_data.open or {} - - local closedSet = self._path_data.closed or {} - - local start_index = minetest.hash_node_position(_start) - - openSet[start_index] = { - pos = _start, - parent = nil, - gScore = 0, - fScore = get_distance(_start, _goal) - } - - local count = self._path_data.count or 1 - - while count > 0 do - if minetest.get_us_time() - us_time > theta_star_alloted_time then - self._path_data = { - start = _start, - open = openSet, - closed = closedSet, - count = count - } - return - end - - -- Initialize ID and data - local current_id, current = next(openSet) - - -- Find lowest f cost - for i, v in pairs(openSet) do - if v.fScore < current.fScore then - current_id = i - current = v - end - end - - -- Add lowest fScore to closedSet and remove from openSet - openSet[current_id] = nil - closedSet[current_id] = current - - -- Reconstruct path if end is reached - if (is_on_ground(_goal) - and current_id == minetest.hash_node_position(_goal)) - or (not is_on_ground(_goal) - and _goal.x == current.pos.x - and _goal.z == current.pos.z) then - local path = {} - local fail_safe = 0 - for _ in pairs(closedSet) do - fail_safe = fail_safe + 1 - end - repeat - if not closedSet[current_id] then return end - table.insert(path, closedSet[current_id].pos) - current_id = closedSet[current_id].parent - until current_id == start_index or #path >= fail_safe - if not closedSet[current_id] then self._path_data = {} return nil end - table.insert(path, closedSet[current_id].pos) - local reverse_path = {} - repeat table.insert(reverse_path, table.remove(path)) until #path == 0 - self._path_data = {} - return reverse_path - end - - count = count - 1 - - local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet) - - -- Go through neighboring nodes - for i = 1, #adjacent do - local neighbor = { - pos = adjacent[i], - parent = current_id, - gScore = 0, - fScore = 0 - } - if not openSet[minetest.hash_node_position(neighbor.pos)] - and not closedSet[minetest.hash_node_position(neighbor.pos)] then - local current_parent = closedSet[current.parent] or closedSet[start_index] - if not current_parent then - current_parent = openSet[current.parent] or openSet[start_index] - end - if current_parent - and get_line_of_sight(current_parent.pos, neighbor.pos) then - local temp_gScore = current_parent.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos) - local new_gScore = 999 - if openSet[minetest.hash_node_position(neighbor.pos)] then - new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore - end - if temp_gScore < new_gScore then - local hCost = get_distance_to_neighbor(neighbor.pos, _goal) - neighbor.gScore = temp_gScore - neighbor.fScore = temp_gScore + hCost - neighbor.parent = minetest.hash_node_position(current_parent.pos) - if openSet[minetest.hash_node_position(neighbor.pos)] then - openSet[minetest.hash_node_position(neighbor.pos)] = nil - end - openSet[minetest.hash_node_position(neighbor.pos)] = neighbor - count = count + 1 - end - else - local temp_gScore = current.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos) - local new_gScore = 999 - if openSet[minetest.hash_node_position(neighbor.pos)] then - new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore - end - if temp_gScore < new_gScore then - local hCost = get_distance_to_neighbor(neighbor.pos, _goal) - neighbor.gScore = temp_gScore - neighbor.fScore = temp_gScore + hCost - if openSet[minetest.hash_node_position(neighbor.pos)] then - openSet[minetest.hash_node_position(neighbor.pos)] = nil - end - openSet[minetest.hash_node_position(neighbor.pos)] = neighbor - count = count + 1 - end - end - end - end - if count > (max_open or 100) then - self._path_data = {} - return - end - end - self._path_data = {} - return nil - end - return find_path(start, goal) -end +----------------- +-- Pathfinding -- +----------------- + +local a_star_alloted_time = tonumber(minetest.settings:get("creatura_a_star_alloted_time")) or 500 +local theta_star_alloted_time = tonumber(minetest.settings:get("creatura_theta_star_alloted_time")) or 700 + +local floor = math.floor +local abs = math.abs + +local vec_dist, vec_round = vector.distance, vector.round + +local moveable = creatura.is_pos_moveable + +local function get_distance(start_pos, end_pos) + local distX = abs(start_pos.x - end_pos.x) + local distZ = abs(start_pos.z - end_pos.z) + + if distX > distZ then + return 14 * distZ + 10 * (distX - distZ) + else + return 14 * distX + 10 * (distZ - distX) + end +end + +local function get_distance_to_neighbor(start_pos, end_pos) + local distX = abs(start_pos.x - end_pos.x) + local distY = abs(start_pos.y - end_pos.y) + local distZ = abs(start_pos.z - end_pos.z) + + if distX > distZ then + return (14 * distZ + 10 * (distX - distZ)) * (distY + 1) + else + return (14 * distX + 10 * (distZ - distX)) * (distY + 1) + end +end + +local function is_on_ground(pos) + local ground = { + x = pos.x, + y = pos.y - 1, + z = pos.z + } + if creatura.get_node_def(ground).walkable then + return true + end + return false +end + +local function vec_raise(v, n) + return {x = v.x, y = v.y + n, z = v.z} +end + +local function get_line_of_sight(a, b) + local steps = floor(vec_dist(a, b)) + local line = {} + + for i = 0, steps do + local pos + + if steps > 0 then + pos = { + x = a.x + (b.x - a.x) * (i / steps), + y = a.y + (b.y - a.y) * (i / steps), + z = a.z + (b.z - a.z) * (i / steps) + } + else + pos = a + end + table.insert(line, pos) + end + + if #line < 1 then + return false + else + for i = 1, #line do + local node = minetest.get_node(line[i]) + if creatura.get_node_def(node.name).walkable then + return false + end + end + end + return true +end + +-- Find a path from start to goal + +--[[local function debugpart(pos, time, tex) + minetest.add_particle({ + pos = pos, + texture = tex or "creatura_particle_red.png", + expirationtime = time or 0.1, + glow = 6, + size = 12 + }) +end]] + +local c_air = minetest.get_content_id("air") + +local function is_pos_moveable_vm(pos, width, height, area, data) + pos = vector.round(pos) + local pos1 = { + x = pos.x - math.ceil(width), + y = pos.y, + z = pos.z - math.ceil(width) + } + local pos2 = { + x = pos.x + math.ceil(width), + y = pos.y + math.ceil(height), + z = pos.z + math.ceil(width) + } + for z = pos1.z, pos2.z do + for y = pos1.y, pos2.y do + for x = pos1.x, pos2.x do + if not area:contains(x, y, z) then return false end + local vi = area:index(x, y, z) + local c = data[vi] + if c ~= c_air then + local c_name = minetest.get_name_from_content_id(c) + if creatura.get_node_def(c_name).walkable then + return false + end + end + end + end + end + return true +end + +local vm_buffer = {} + +function creatura.find_lvm_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) + climb = climb or false + fly = fly or false + swim = swim or false + + if vec_dist(start, goal) > (self.tracking_range or 128) then return {} end + + self._path_data.start = start + + local path_neighbors = { + {x = 1, y = 0, z = 0}, + {x = 1, y = 0, z = 1}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = -1, y = 0, z = -1}, + {x = 0, y = 0, z = -1}, + {x = 1, y = 0, z = -1} + } + + if climb then + table.insert(path_neighbors, {x = 0, y = 1, z = 0}) + end + + if fly + or swim then + path_neighbors = { + -- Central + {x = 1, y = 0, z = 0}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = 0, y = 0, z = -1}, + -- Directly Up or Down + {x = 0, y = 1, z = 0}, + {x = 0, y = -1, z = 0} + } + end + + + local function get_neighbors(pos, width, height, tbl, open, closed, vm_area, vm_data) + local result = {} + for i = 1, #tbl do + local neighbor = vector.add(pos, tbl[i]) + if not vm_area or not vm_data or not vm_area:containsp(neighbor) then return end + local can_move = (not swim and get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)) or true + if open[minetest.hash_node_position(neighbor)] + or closed[minetest.hash_node_position(neighbor)] then + can_move = false + end + if can_move then + can_move = is_pos_moveable_vm(neighbor, width, height, vm_area, vm_data) + if not fly and not swim then + if not can_move then -- Step Up + local step = vec_raise(neighbor, 1) + can_move = is_pos_moveable_vm(vec_round(step), width, height, vm_area, vm_data) + neighbor = vec_round(step) + else + local step = creatura.get_ground_level(vector.new(neighbor), 1) + if step.y < neighbor.y + and is_pos_moveable_vm(vec_round(step), width, height, vm_area, vm_data) then + neighbor = step + end + end + end + end + if vector.equals(neighbor, goal) then + can_move = true + end + if can_move + and (not swim + or creatura.get_node_def(neighbor).drawtype == "liquid") then + table.insert(result, neighbor) + end + end + return result + end + + local function find_path(_start, _goal) + local us_time = minetest.get_us_time() + + _start = { + x = floor(_start.x + 0.5), + y = floor(_start.y + 0.5), + z = floor(_start.z + 0.5) + } + + _goal = { + x = floor(_goal.x + 0.5), + y = floor(_goal.y + 0.5), + z = floor(_goal.z + 0.5) + } + + if _goal.x == _start.x + and _goal.z == _start.z then -- No path can be found + return nil + end + + local vm_area = self._path_data.vm_area + local vm_data = self._path_data.vm_data + + if not vm_area + or not vm_data then + local vm_center = vector.add(_start, vector.divide(vector.subtract(_goal, _start), 2)) + local vm_size = vec_dist(_goal, _start) + if vm_size < 24 then vm_size = 24 end + local e1 = vector.subtract(vm_center, vm_size) + local e2 = vector.add(vm_center, vm_size) + local vm = minetest.get_voxel_manip(e1, e2) + e1, e2 = vm:read_from_map(e1, e2) + vm_area = VoxelArea:new{MinEdge=e1, MaxEdge=e2} + vm_data = vm:get_data(vm_buffer) + end + + local openSet = self._path_data.open or {} + + local closedSet = self._path_data.closed or {} + + local start_index = minetest.hash_node_position(_start) + + openSet[start_index] = { + pos = _start, + parent = nil, + gScore = 0, + fScore = get_distance(_start, _goal) + } + + local count = self._path_data.count or 1 + + while count > 0 do + -- Initialize ID and data + local current_id, current = next(openSet) + + -- Find lowest f cost + for i, v in pairs(openSet) do + if v.fScore < current.fScore then + current_id = i + current = v + end + end + + -- Add lowest fScore to closedSet and remove from openSet + openSet[current_id] = nil + closedSet[current_id] = current + + self._path_data.open = openSet + self._path_data.closedSet = closedSet + + local current_start = vec_round(self._path_data.start) + + if closedSet[minetest.hash_node_position(current_start)] then + start_index = minetest.hash_node_position(current_start) + end + + -- Reconstruct path if end is reached + if ((is_on_ground(_goal) + or fly) + and current_id == minetest.hash_node_position(_goal)) + or (not fly + and not is_on_ground(_goal) + and math.abs(_goal.x - current.pos.x) < 1.1 + and math.abs(_goal.z - current.pos.z) < 1.1) then + local path = {} + local fail_safe = 0 + for _ in pairs(closedSet) do + fail_safe = fail_safe + 1 + end + repeat + if not closedSet[current_id] then return end + table.insert(path, closedSet[current_id].pos) + current_id = closedSet[current_id].parent + until current_id == start_index or #path >= fail_safe + if not closedSet[current_id] then self._path_data = {} return nil end + table.insert(path, closedSet[current_id].pos) + local reverse_path = {} + repeat table.insert(reverse_path, table.remove(path)) until #path == 0 + self._path_data = {} + return reverse_path + end + + count = count - 1 + + local adjacent = get_neighbors( + current.pos, + obj_width, + obj_height, + path_neighbors, + openSet, + closedSet, + vm_area, + vm_data + ) + + -- Go through neighboring nodes + if not adjacent or #adjacent < 1 then self._path_data = {} return {} end + for i = 1, #adjacent do + local neighbor = { + pos = adjacent[i], + parent = current_id, + gScore = 0, + fScore = 0 + } + local temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos) + local new_gScore = 0 + if openSet[minetest.hash_node_position(neighbor.pos)] then + new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore + end + if (temp_gScore < new_gScore + or not openSet[minetest.hash_node_position(neighbor.pos)]) + and not closedSet[minetest.hash_node_position(neighbor.pos)] then + if not openSet[minetest.hash_node_position(neighbor.pos)] then + count = count + 1 + end + local hCost = get_distance_to_neighbor(neighbor.pos, _goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + openSet[minetest.hash_node_position(neighbor.pos)] = neighbor + end + end + if minetest.get_us_time() - us_time > a_star_alloted_time then + self._path_data = { + start = _start, + open = openSet, + closed = closedSet, + count = count, + vm_area = vm_area, + vm_data = vm_data + } + return {} + end + if count > (max_open or 100) then + self._path_data = {} + return + end + end + self._path_data = {} + return nil + end + return find_path(start, goal) +end + +function creatura.find_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) + climb = climb or false + fly = fly or false + swim = swim or false + + start = self._path_data.start or start + + self._path_data.start = start + + local path_neighbors = { + {x = 1, y = 0, z = 0}, + {x = 1, y = 0, z = 1}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = -1, y = 0, z = -1}, + {x = 0, y = 0, z = -1}, + {x = 1, y = 0, z = -1} + } + + if climb then + table.insert(path_neighbors, {x = 0, y = 1, z = 0}) + end + + if fly + or swim then + path_neighbors = { + -- Central + {x = 1, y = 0, z = 0}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = 0, y = 0, z = -1}, + -- Directly Up or Down + {x = 0, y = 1, z = 0}, + {x = 0, y = -1, z = 0} + } + end + + local function get_neighbors(pos, width, height, tbl, open, closed) + local result = {} + for i = 1, #tbl do + local neighbor = vector.add(pos, tbl[i]) + local can_move = (not swim and get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)) or true + if open[minetest.hash_node_position(neighbor)] + or closed[minetest.hash_node_position(neighbor)] then + can_move = false + end + if can_move then + can_move = moveable(neighbor, width, height) + if not fly and not swim then + if not can_move then -- Step Up + local step = vec_raise(neighbor, 1) + can_move = moveable(vec_round(step), width, height) + neighbor = vec_round(step) + else + local step = creatura.get_ground_level(vector.new(neighbor), 1) + if step.y < neighbor.y + and moveable(vec_round(step), width, height) then + neighbor = step + end + end + end + end + if vector.equals(neighbor, goal) then + can_move = true + end + if can_move + and (not swim + or creatura.get_node_def(neighbor).drawtype == "liquid") then + table.insert(result, neighbor) + end + end + return result + end + + local function find_path(_start, _goal) + local us_time = minetest.get_us_time() + + _start = { + x = floor(_start.x + 0.5), + y = floor(_start.y + 0.5), + z = floor(_start.z + 0.5) + } + + _goal = { + x = floor(_goal.x + 0.5), + y = floor(_goal.y + 0.5), + z = floor(_goal.z + 0.5) + } + + if _goal.x == _start.x + and _goal.z == _start.z then -- No path can be found + return nil + end + + local openSet = self._path_data.open or {} + + local closedSet = self._path_data.closed or {} + + local start_index = minetest.hash_node_position(_start) + + openSet[start_index] = { + pos = _start, + parent = nil, + gScore = 0, + fScore = get_distance(_start, _goal) + } + + local count = self._path_data.count or 1 + + while count > 0 do + if minetest.get_us_time() - us_time > a_star_alloted_time then + self._path_data = { + start = _start, + open = openSet, + closed = closedSet, + count = count + } + return + end + -- Initialize ID and data + local current_id, current = next(openSet) + + -- Find lowest f cost + for i, v in pairs(openSet) do + if v.fScore < current.fScore then + current_id = i + current = v + end + end + + -- Add lowest fScore to closedSet and remove from openSet + openSet[current_id] = nil + closedSet[current_id] = current + + self._path_data.open = openSet + self._path_data.closedSet = closedSet + + -- Reconstruct path if end is reached + if ((is_on_ground(_goal) + or fly) + and current_id == minetest.hash_node_position(_goal)) + or (not fly + and not is_on_ground(_goal) + and _goal.x == current.pos.x + and _goal.z == current.pos.z) then + local path = {} + local fail_safe = 0 + for _ in pairs(closedSet) do + fail_safe = fail_safe + 1 + end + repeat + if not closedSet[current_id] then return end + table.insert(path, closedSet[current_id].pos) + current_id = closedSet[current_id].parent + until current_id == start_index or #path >= fail_safe + if not closedSet[current_id] then self._path_data = {} return nil end + table.insert(path, closedSet[current_id].pos) + local reverse_path = {} + repeat table.insert(reverse_path, table.remove(path)) until #path == 0 + self._path_data = {} + return reverse_path + end + + count = count - 1 + + local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet) + + -- Go through neighboring nodes + for i = 1, #adjacent do + local neighbor = { + pos = adjacent[i], + parent = current_id, + gScore = 0, + fScore = 0 + } + local temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos) + local new_gScore = 0 + if openSet[minetest.hash_node_position(neighbor.pos)] then + new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore + end + if (temp_gScore < new_gScore + or not openSet[minetest.hash_node_position(neighbor.pos)]) + and not closedSet[minetest.hash_node_position(neighbor.pos)] then + if not openSet[minetest.hash_node_position(neighbor.pos)] then + count = count + 1 + end + local hCost = get_distance_to_neighbor(neighbor.pos, _goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + openSet[minetest.hash_node_position(neighbor.pos)] = neighbor + end + end + if count > (max_open or 100) then + self._path_data = {} + return + end + end + self._path_data = {} + return nil + end + return find_path(start, goal) +end + +------------ +-- Theta* -- +------------ + +function creatura.find_theta_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) + climb = climb or false + fly = fly or false + swim = swim or false + + start = self._path_data.start or start + + self._path_data.start = start + + local path_neighbors = { + {x = 1, y = 0, z = 0}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = 0, y = 0, z = -1}, + } + + if climb then + table.insert(path_neighbors, {x = 0, y = 1, z = 0}) + end + + if fly + or swim then + path_neighbors = { + -- Central + {x = 1, y = 0, z = 0}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = 0, y = 0, z = -1}, + -- Directly Up or Down + {x = 0, y = 1, z = 0}, + {x = 0, y = -1, z = 0} + } + end + + local function get_neighbors(pos, width, height, tbl, open, closed) + local result = {} + for i = 1, #tbl do + local neighbor = vector.add(pos, tbl[i]) + if neighbor.y == pos.y + and not fly + and not swim then + neighbor = creatura.get_ground_level(neighbor, 1) + end + local can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) + if swim then + can_move = true + end + if not moveable(vec_raise(neighbor, -0.49), width, height) then + can_move = false + if neighbor.y == pos.y + and moveable(vec_raise(neighbor, 0.51), width, height) then + neighbor = vec_raise(neighbor, 1) + can_move = true + end + end + if vector.equals(neighbor, goal) then + can_move = true + end + if open[minetest.hash_node_position(neighbor)] + or closed[minetest.hash_node_position(neighbor)] then + can_move = false + end + if can_move + and ((is_on_ground(neighbor) + or (fly or swim)) + or (neighbor.x == pos.x + and neighbor.z == pos.z + and climb)) + and (not swim + or creatura.get_node_def(neighbor).drawtype == "liquid") then + table.insert(result, neighbor) + end + end + return result + end + + local function find_path(_start, _goal) + local us_time = minetest.get_us_time() + + _start = { + x = floor(_start.x + 0.5), + y = floor(_start.y + 0.5), + z = floor(_start.z + 0.5) + } + + _goal = { + x = floor(_goal.x + 0.5), + y = floor(_goal.y + 0.5), + z = floor(_goal.z + 0.5) + } + + if _goal.x == _start.x + and _goal.z == _start.z then -- No path can be found + return nil + end + + local openSet = self._path_data.open or {} + + local closedSet = self._path_data.closed or {} + + local start_index = minetest.hash_node_position(_start) + + openSet[start_index] = { + pos = _start, + parent = nil, + gScore = 0, + fScore = get_distance(_start, _goal) + } + + local count = self._path_data.count or 1 + + while count > 0 do + if minetest.get_us_time() - us_time > theta_star_alloted_time then + self._path_data = { + start = _start, + open = openSet, + closed = closedSet, + count = count + } + return + end + + -- Initialize ID and data + local current_id, current = next(openSet) + + -- Find lowest f cost + for i, v in pairs(openSet) do + if v.fScore < current.fScore then + current_id = i + current = v + end + end + + -- Add lowest fScore to closedSet and remove from openSet + openSet[current_id] = nil + closedSet[current_id] = current + + -- Reconstruct path if end is reached + if (is_on_ground(_goal) + and current_id == minetest.hash_node_position(_goal)) + or (not is_on_ground(_goal) + and _goal.x == current.pos.x + and _goal.z == current.pos.z) then + local path = {} + local fail_safe = 0 + for _ in pairs(closedSet) do + fail_safe = fail_safe + 1 + end + repeat + if not closedSet[current_id] then return end + table.insert(path, closedSet[current_id].pos) + current_id = closedSet[current_id].parent + until current_id == start_index or #path >= fail_safe + if not closedSet[current_id] then self._path_data = {} return nil end + table.insert(path, closedSet[current_id].pos) + local reverse_path = {} + repeat table.insert(reverse_path, table.remove(path)) until #path == 0 + self._path_data = {} + return reverse_path + end + + count = count - 1 + + local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet) + + -- Go through neighboring nodes + for i = 1, #adjacent do + local neighbor = { + pos = adjacent[i], + parent = current_id, + gScore = 0, + fScore = 0 + } + if not openSet[minetest.hash_node_position(neighbor.pos)] + and not closedSet[minetest.hash_node_position(neighbor.pos)] then + local current_parent = closedSet[current.parent] or closedSet[start_index] + if not current_parent then + current_parent = openSet[current.parent] or openSet[start_index] + end + if current_parent + and get_line_of_sight(current_parent.pos, neighbor.pos) then + local temp_gScore = current_parent.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos) + local new_gScore = 999 + if openSet[minetest.hash_node_position(neighbor.pos)] then + new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore + end + if temp_gScore < new_gScore then + local hCost = get_distance_to_neighbor(neighbor.pos, _goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + neighbor.parent = minetest.hash_node_position(current_parent.pos) + if openSet[minetest.hash_node_position(neighbor.pos)] then + openSet[minetest.hash_node_position(neighbor.pos)] = nil + end + openSet[minetest.hash_node_position(neighbor.pos)] = neighbor + count = count + 1 + end + else + local temp_gScore = current.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos) + local new_gScore = 999 + if openSet[minetest.hash_node_position(neighbor.pos)] then + new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore + end + if temp_gScore < new_gScore then + local hCost = get_distance_to_neighbor(neighbor.pos, _goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + if openSet[minetest.hash_node_position(neighbor.pos)] then + openSet[minetest.hash_node_position(neighbor.pos)] = nil + end + openSet[minetest.hash_node_position(neighbor.pos)] = neighbor + count = count + 1 + end + end + end + end + if count > (max_open or 100) then + self._path_data = {} + return + end + end + self._path_data = {} + return nil + end + return find_path(start, goal) +end diff --git a/pathfinding.lua b/pathfinding.lua new file mode 100644 index 0000000..4dd6d41 --- /dev/null +++ b/pathfinding.lua @@ -0,0 +1,620 @@ +----------------- +-- Pathfinding -- +----------------- + +local a_star_alloted_time = tonumber(minetest.settings:get("creatura_a_star_alloted_time")) or 500 +local theta_star_alloted_time = tonumber(minetest.settings:get("creatura_theta_star_alloted_time")) or 700 + +creatura.pathfinder = {} + +local max_open = 300 + +-- Math + +local floor = math.floor +local abs = math.abs + +local vec_add, vec_dist, vec_new, vec_round = vector.add, vector.distance, vector.new, vector.round + +local function vec_raise(v, n) + return {x = v.x, y = v.y + n, z = v.z} +end + +-- Heuristic + +local function get_distance(start_pos, end_pos) + local distX = abs(start_pos.x - end_pos.x) + local distZ = abs(start_pos.z - end_pos.z) + + if distX > distZ then + return 14 * distZ + 10 * (distX - distZ) + else + return 14 * distX + 10 * (distZ - distX) + end +end + +local function get_distance_to_neighbor(start_pos, end_pos) + local distX = abs(start_pos.x - end_pos.x) + local distY = abs(start_pos.y - end_pos.y) + local distZ = abs(start_pos.z - end_pos.z) + + if distX > distZ then + return (14 * distZ + 10 * (distX - distZ)) * (distY + 1) + else + return (14 * distX + 10 * (distZ - distX)) * (distY + 1) + end +end + +-- Blocked Movement Checks + +local is_blocked = creatura.is_blocked + +local function get_line_of_sight(a, b) + local steps = floor(vec_dist(a, b)) + local line = {} + + for i = 0, steps do + local pos + + if steps > 0 then + pos = { + x = a.x + (b.x - a.x) * (i / steps), + y = a.y + (b.y - a.y) * (i / steps), + z = a.z + (b.z - a.z) * (i / steps) + } + else + pos = a + end + table.insert(line, pos) + end + + if #line < 1 then + return false + else + for i = 1, #line do + local node = minetest.get_node(line[i]) + if creatura.get_node_def(node.name).walkable then + return false + end + end + end + return true +end + +local function is_on_ground(pos) + local ground = { + x = pos.x, + y = pos.y - 1, + z = pos.z + } + if creatura.get_node_def(ground).walkable then + return true + end + return false +end + +-- Neighbor Check Grids + +local neighbor_grid = { + {x = 1, y = 0, z = 0}, + {x = 1, y = 0, z = 1}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = -1, y = 0, z = -1}, + {x = 0, y = 0, z = -1}, + {x = 1, y = 0, z = -1} +} + +local neighbor_grid_climb = { + {x = 1, y = 0, z = 0}, + {x = 1, y = 0, z = 1}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = -1, y = 0, z = -1}, + {x = 0, y = 0, z = -1}, + {x = 1, y = 0, z = -1}, + + {x = 0, y = 1, z = 0}, + {x = 0, y = -1, z = 0} +} + +local neighbor_grid_3d = { + -- Central + {x = 1, y = 0, z = 0}, + {x = 0, y = 0, z = 1}, + {x = -1, y = 0, z = 0}, + {x = 0, y = 0, z = -1}, + -- Directly Up or Down + {x = 0, y = 1, z = 0}, + {x = 0, y = -1, z = 0} +} + +-- Get Neighbors + +local function get_neighbors(pos, width, height, open, closed, parent) + local result = {} + local neighbor + local can_move + local hashed_pos + local step + + for i = 1, #neighbor_grid do + neighbor = vec_add(pos, neighbor_grid[i]) + can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) + hashed_pos = minetest.hash_node_position(neighbor) + + if parent + and vec_dist(parent, neighbor) < vec_dist(pos, neighbor) then + can_move = false + end + + if open[hashed_pos] + or closed[hashed_pos] then + can_move = false + elseif can_move then + can_move = not is_blocked(neighbor, width, height) + + if not can_move then -- Step Up + step = vec_raise(neighbor, 1) + can_move = not is_blocked(vec_round(step), width, height) + neighbor = vec_round(step) + else + step = creatura.get_ground_level(vec_new(neighbor), 1) + if step.y < neighbor.y + and not is_blocked(vec_round(step), width, height) then + neighbor = step + end + end + end + + if can_move then + table.insert(result, neighbor) + end + end + return result +end + +function creatura.pathfinder.get_neighbors_climb(pos, width, height, open, closed) + local result = {} + local neighbor + local can_move + local hashed_pos + local step + + for i = 1, #neighbor_grid_climb do + neighbor = vec_add(pos, neighbor_grid_climb[i]) + can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor) + hashed_pos = minetest.hash_node_position(neighbor) + + if open[hashed_pos] + or closed[hashed_pos] then + can_move = false + elseif can_move then + can_move = not is_blocked(neighbor, width, height) + + if not can_move then -- Step Up + step = vec_raise(neighbor, 1) + can_move = not is_blocked(vec_round(step), width, height) + neighbor = vec_round(step) + elseif i < 9 then + step = creatura.get_ground_level(vec_new(neighbor), 1) + if step.y < neighbor.y + and not is_blocked(vec_round(step), width, height) then + neighbor = step + end + end + end + + if can_move then + table.insert(result, neighbor) + end + end + return result +end + +function creatura.pathfinder.get_neighbors_fly(pos, width, height, open, closed, parent) + local result = {} + local neighbor + local can_move + local hashed_pos + + for i = 1, #neighbor_grid_3d do + neighbor = vec_add(pos, neighbor_grid_3d[i]) + can_move = get_line_of_sight({x = pos.x, y = pos.y, z = pos.z}, neighbor) + hashed_pos = minetest.hash_node_position(neighbor) + + if parent + and vec_dist(parent, neighbor) < vec_dist(pos, neighbor) then + can_move = false + end + + if open[hashed_pos] + or closed[hashed_pos] then + can_move = false + elseif can_move then + can_move = not is_blocked(neighbor, width, height) + end + + if can_move then + table.insert(result, neighbor) + end + end + return result, true +end + +function creatura.pathfinder.get_neighbors_swim(pos, width, height, open, closed, parent) + local result = {} + local neighbor + local can_move + local hashed_pos + + for i = 1, #neighbor_grid_3d do + neighbor = vec_add(pos, neighbor_grid_3d[i]) + can_move = get_line_of_sight({x = pos.x, y = pos.y, z = pos.z}, neighbor) + hashed_pos = minetest.hash_node_position(neighbor) + + if (parent + and vec_dist(parent, neighbor) < vec_dist(pos, neighbor)) + or creatura.get_node_def(neighbor).drawtype ~= "liquid" then + can_move = false + end + + if open[hashed_pos] + or closed[hashed_pos] then + can_move = false + elseif can_move then + can_move = not is_blocked(neighbor, width, height) + end + + if can_move then + table.insert(result, neighbor) + end + end + return result, true +end + +-- A* + +function creatura.pathfinder.find_path(self, pos1, pos2, neighbor_func) + local us_time = minetest.get_us_time() + local check_vertical = false + neighbor_func = neighbor_func or get_neighbors + + local start = self._path_data.start or { + x = floor(pos1.x + 0.5), + y = floor(pos1.y + 0.5), + z = floor(pos1.z + 0.5) + } + local goal = { + x = floor(pos2.x + 0.5), + y = floor(pos2.y + 0.5), + z = floor(pos2.z + 0.5) + } + + self._path_data.start = start + + if goal.x == start.x + and goal.z == start.z then -- No path can be found + self._path_data = {} + return + end + + local openSet = self._path_data.open or {} + local closedSet = self._path_data.closed or {} + + local start_index = minetest.hash_node_position(start) + + openSet[start_index] = { + pos = start, + parent = nil, + gScore = 0, + fScore = get_distance(start, goal) + } + + local count = self._path_data.count or 1 + local current_id, current + local adjacent + local neighbor + + local temp_gScore + local new_gScore + local hCost + + local hashed_pos + local parent_open + local parent_closed + + while count > 0 do + + -- Initialize ID and data + current_id, current = next(openSet) + + -- Find lowest f cost + for i, v in pairs(openSet) do + if v.fScore < current.fScore then + current_id = i + current = v + end + end + + if not current_id then self._path_data = {} return end -- failsafe + + -- Add lowest fScore to closedSet and remove from openSet + openSet[current_id] = nil + closedSet[current_id] = current + + if ((check_vertical or is_on_ground(goal)) + and current_id == minetest.hash_node_position(goal)) + or ((not check_vertical and not is_on_ground(goal)) + and goal.x == current.pos.x + and goal.z == current.pos.z) then + local path = {} + local fail_safe = 0 + + for _ in pairs(closedSet) do + fail_safe = fail_safe + 1 + end + + repeat + if not closedSet[current_id] then self._path_data = {} return end + table.insert(path, closedSet[current_id].pos) + current_id = closedSet[current_id].parent + until current_id == start_index or #path >= fail_safe + + if not closedSet[current_id] then self._path_data = {} return nil end + + table.insert(path, closedSet[current_id].pos) + + local reverse_path = {} + repeat table.insert(reverse_path, table.remove(path)) until #path == 0 + + self._path_data = {} + return reverse_path + end + + parent_open = openSet[current.parent] + parent_closed = closedSet[current.parent] + adjacent, check_vertical = neighbor_func( + current.pos, + self.width, + self.height, + openSet, + closedSet, + (parent_closed and parent_closed.pos) or (parent_open and parent_open.pos) + ) + -- Fly, Swim, and Climb all return true for check_vertical to properly check if goal has been reached + + -- Go through neighboring nodes + for i = 1, #adjacent do + neighbor = { + pos = adjacent[i], + parent = current_id, + gScore = 0, + fScore = 0 + } + + temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos) + new_gScore = 0 + + hashed_pos = minetest.hash_node_position(neighbor.pos) + + if openSet[hashed_pos] then + new_gScore = openSet[hashed_pos].gScore + end + + if (temp_gScore < new_gScore + or not openSet[hashed_pos]) + and not closedSet[hashed_pos] then + if not openSet[hashed_pos] then + count = count + 1 + end + + hCost = get_distance_to_neighbor(neighbor.pos, goal) + + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + openSet[hashed_pos] = neighbor + end + end + + if minetest.get_us_time() - us_time > a_star_alloted_time then + self._path_data = { + start = start, + open = openSet, + closed = closedSet, + count = count + } + return + end + + if count > (max_open or 100) then + self._path_data = {} + return + end + end +end + +-- Theta* + +function creatura.pathfinder.find_path_theta(self, pos1, pos2, neighbor_func) + local us_time = minetest.get_us_time() + local check_vertical = false + neighbor_func = neighbor_func or get_neighbors + + local start = self._path_data.start or { + x = floor(pos1.x + 0.5), + y = floor(pos1.y + 0.5), + z = floor(pos1.z + 0.5) + } + local goal = { + x = floor(pos2.x + 0.5), + y = floor(pos2.y + 0.5), + z = floor(pos2.z + 0.5) + } + + self._path_data.start = start + + if goal.x == start.x + and goal.z == start.z then -- No path can be found + return + end + + local openSet = self._path_data.open or {} + local closedSet = self._path_data.closed or {} + + local start_index = minetest.hash_node_position(start) + + openSet[start_index] = { + pos = start, + parent = nil, + gScore = 0, + fScore = get_distance(start, goal) + } + + local count = self._path_data.count or 1 + local current_id, current + local current_parent + local adjacent + local neighbor + + local temp_gScore + local new_gScore + local hCost + + local hashed_pos + local parent_open + local parent_closed + + while count > 0 do + + -- Initialize ID and data + current_id, current = next(openSet) + + -- Find lowest f cost + for i, v in pairs(openSet) do + if v.fScore < current.fScore then + current_id = i + current = v + end + end + + if not current_id then return end -- failsafe + + -- Add lowest fScore to closedSet and remove from openSet + openSet[current_id] = nil + closedSet[current_id] = current + + if ((check_vertical or is_on_ground(goal)) + and current_id == minetest.hash_node_position(goal)) + or ((not check_vertical and not is_on_ground(goal)) + and goal.x == current.pos.x + and goal.z == current.pos.z) then + local path = {} + local fail_safe = 0 + + for _ in pairs(closedSet) do + fail_safe = fail_safe + 1 + end + + repeat + if not closedSet[current_id] then return end + table.insert(path, closedSet[current_id].pos) + current_id = closedSet[current_id].parent + until current_id == start_index or #path >= fail_safe + + if not closedSet[current_id] then self._path_data = {} return nil end + + table.insert(path, closedSet[current_id].pos) + + local reverse_path = {} + repeat table.insert(reverse_path, table.remove(path)) until #path == 0 + + self._path_data = {} + return reverse_path + end + + parent_open = openSet[current.parent] + parent_closed = closedSet[current.parent] + adjacent, check_vertical = neighbor_func( + current.pos, + self.width, + self.height, + openSet, + closedSet, + (parent_closed and parent_closed.pos) or (parent_open and parent_open.pos) + ) + -- Fly, Swim, and Climb all return true for check_vertical to properly check if goal has been reached + + -- Go through neighboring nodes + for i = 1, #adjacent do + neighbor = { + pos = adjacent[i], + parent = current_id, + gScore = 0, + fScore = 0 + } + + hashed_pos = minetest.hash_node_position(neighbor.pos) + + if not openSet[hashed_pos] + and not closedSet[hashed_pos] then + current_parent = closedSet[current.parent] or closedSet[start_index] + if not current_parent then + current_parent = openSet[current.parent] or openSet[start_index] + end + + if current_parent + and get_line_of_sight(current_parent.pos, neighbor.pos) then + temp_gScore = current_parent.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos) + new_gScore = 999 + + if openSet[hashed_pos] then + new_gScore = openSet[hashed_pos].gScore + end + + if temp_gScore < new_gScore then + hCost = get_distance_to_neighbor(neighbor.pos, goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + neighbor.parent = minetest.hash_node_position(current_parent.pos) + openSet[hashed_pos] = neighbor + count = count + 1 + end + else + temp_gScore = current.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos) + new_gScore = 999 + + if openSet[hashed_pos] then + new_gScore = openSet[hashed_pos].gScore + end + + if temp_gScore < new_gScore then + hCost = get_distance_to_neighbor(neighbor.pos, goal) + neighbor.gScore = temp_gScore + neighbor.fScore = temp_gScore + hCost + + openSet[hashed_pos] = neighbor + count = count + 1 + end + end + end + end + + if minetest.get_us_time() - us_time > theta_star_alloted_time then + self._path_data = { + start = start, + open = openSet, + closed = closedSet, + count = count + } + return + end + + if count > (max_open or 100) then + self._path_data = {} + return + end + end +end \ No newline at end of file diff --git a/spawning.lua b/spawning.lua index 4da656c..05c80f8 100644 --- a/spawning.lua +++ b/spawning.lua @@ -87,6 +87,19 @@ function creatura.register_spawn_item(name, def) def.description = def.description or "Spawn " .. format_name(name) def.inventory_image = def.inventory_image or inventory_image def.on_place = function(itemstack, player, pointed_thing) + -- If the player right-clicks something like a chest or item frame then + -- run the node's on_rightclick callback + local under = pointed_thing.under + local node = minetest.get_node(under) + local node_def = minetest.registered_nodes[node.name] + if node_def and node_def.on_rightclick and + not (player and player:is_player() and + player:get_player_control().sneak) then + return node_def.on_rightclick(under, node, player, itemstack, + pointed_thing) or itemstack + end + + -- Otherwise spawn the mob local pos = minetest.get_pointed_thing_position(pointed_thing, true) if minetest.is_protected(pos, player and player:get_player_name() or "") then return end local mobdef = minetest.registered_entities[name] @@ -518,8 +531,11 @@ local function can_spawn(pos, width, height) return true end +local mobs_spawn = minetest.settings:get_bool("mobs_spawn") ~= false + function creatura.register_abm_spawn(mob, def) local chance = def.chance or 3000 + local chance_on_load = def.chance_on_load or def.chance / 32 local interval = def.interval or 30 local min_height = def.min_height or 0 local max_height = def.max_height or 128 @@ -534,10 +550,15 @@ function creatura.register_abm_spawn(mob, def) local nodes = def.nodes or {"group:soil", "group:stone"} local neighbors = def.neighbors or {"air"} local spawn_on_load = def.spawn_on_load or false + local spawn_active = def.spawn_active or true local spawn_in_nodes = def.spawn_in_nodes or false local spawn_cap = def.spawn_cap or 5 - local function spawn_func(pos, _, _, aocw) + local function spawn_func(pos, aocw, is_lbm) + + if not mobs_spawn then + return + end if not spawn_in_nodes then pos.y = pos.y + 1 @@ -549,8 +570,8 @@ function creatura.register_abm_spawn(mob, def) return end - if spawn_on_load then -- Manual checks for LBMs - if random(chance) > 1 then return end + if is_lbm then -- Manual checks for LBMs + if random(chance_on_load or chance) > 1 then return end if not minetest.find_node_near(pos, 1, neighbors) then return end if pos.y > max_height or pos.y < min_height then return end end @@ -674,11 +695,12 @@ function creatura.register_abm_spawn(mob, def) label = mob .. " spawning", nodenames = nodes, run_at_every_load = false, - action = function(pos, ...) - spawn_func(pos, ...) + action = function(pos, _, _, aocw) + spawn_func(pos, aocw, true) end }) - else + end + if spawn_active then minetest.register_abm({ label = mob .. " spawning", nodenames = nodes, @@ -688,8 +710,8 @@ function creatura.register_abm_spawn(mob, def) min_y = min_height, max_y = max_height, catch_up = false, - action = function(pos, ...) - spawn_func(pos, ...) + action = function(pos, _, _, aocw) + spawn_func(pos, aocw, false) end }) end