Updated Physics and Vitals, New Pathfinding

This commit is contained in:
ElCeejo 2023-06-09 23:15:07 -07:00
parent 69cd063c43
commit 59602c9d36
8 changed files with 1746 additions and 1136 deletions

317
api.lua
View file

@ -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()

25
doc.txt
View file

@ -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
* 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

View file

@ -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 --

View file

@ -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)

View file

@ -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

File diff suppressed because it is too large Load diff

620
pathfinding.lua Normal file
View file

@ -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

View file

@ -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