mirror of
https://github.com/ElCeejo/creatura.git
synced 2025-03-15 04:11:24 +00:00
Initial Commit
This commit is contained in:
parent
f72b110b33
commit
c251f07ae5
18 changed files with 3290 additions and 0 deletions
444
api.lua
Normal file
444
api.lua
Normal file
|
@ -0,0 +1,444 @@
|
||||||
|
--------------
|
||||||
|
-- Creatura --
|
||||||
|
--------------
|
||||||
|
|
||||||
|
creatura.api = {}
|
||||||
|
|
||||||
|
-- Math --
|
||||||
|
|
||||||
|
local pi = math.pi
|
||||||
|
local pi2 = pi * 2
|
||||||
|
local abs = math.abs
|
||||||
|
local floor = math.floor
|
||||||
|
local random = math.random
|
||||||
|
|
||||||
|
local sin = math.sin
|
||||||
|
local cos = math.cos
|
||||||
|
local atan2 = math.atan2
|
||||||
|
|
||||||
|
local function diff(a, b) -- Get difference between 2 angles
|
||||||
|
return math.atan2(math.sin(b - a), math.cos(b - a))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clamp(val, min, max)
|
||||||
|
if val < min then
|
||||||
|
val = min
|
||||||
|
elseif max < val then
|
||||||
|
val = max
|
||||||
|
end
|
||||||
|
return val
|
||||||
|
end
|
||||||
|
|
||||||
|
local vec_dir = vector.direction
|
||||||
|
local vec_dist = vector.distance
|
||||||
|
local vec_multi = vector.multiply
|
||||||
|
local vec_sub = vector.subtract
|
||||||
|
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
|
||||||
|
return {x = v.x, y = v.y + n, z = v.z}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dist_2d(pos1, pos2)
|
||||||
|
local a = {x = pos1.x, y = 0, z = pos1.z}
|
||||||
|
local b = {x = pos2.x, y = 0, z = pos2.z}
|
||||||
|
return vec_dist(a, b)
|
||||||
|
end
|
||||||
|
|
||||||
|
---------------
|
||||||
|
-- Local API --
|
||||||
|
---------------
|
||||||
|
|
||||||
|
local function indicate_damage(self)
|
||||||
|
self.object:set_texture_mod("^[colorize:#FF000040")
|
||||||
|
core.after(0.2, function()
|
||||||
|
if creatura.is_alive(self) then
|
||||||
|
self.object:set_texture_mod("")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_node_height(pos)
|
||||||
|
local node = minetest.get_node(pos)
|
||||||
|
local def = minetest.registered_nodes[node.name]
|
||||||
|
if not def then return nil end
|
||||||
|
if def.walkable then
|
||||||
|
if def.drawtype == "nodebox" then
|
||||||
|
if def.node_box
|
||||||
|
and def.node_box.type == "fixed" then
|
||||||
|
if type(def.node_box.fixed[1]) == "number" then
|
||||||
|
return pos.y + node.node_box.fixed[5]
|
||||||
|
elseif type(node.node_box.fixed[1]) == "table" then
|
||||||
|
return pos.y + node.node_box.fixed[1][5]
|
||||||
|
else
|
||||||
|
return pos.y + 0.5
|
||||||
|
end
|
||||||
|
elseif node.node_box
|
||||||
|
and node.node_box.type == 'leveled' then
|
||||||
|
return minetest.get_node_level(pos) / 64 - 0.5 + pos.y
|
||||||
|
else
|
||||||
|
return pos.y + 0.5
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return pos.y + 0.5
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return pos.y - 0.5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function walkable(pos)
|
||||||
|
return minetest.registered_nodes[minetest.get_node(pos).name].walkable
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_under_solid(pos)
|
||||||
|
local pos2 = vector.new(pos.x, pos.y + 1, pos.z)
|
||||||
|
return (walkable(pos2) or ((get_node_height(pos2) or 0) < 1.5))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_node_walkable(name)
|
||||||
|
local def = minetest.registered_nodes[name]
|
||||||
|
return def and def.walkable
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_value_in_table(tbl, val)
|
||||||
|
for _, v in pairs(tbl) do
|
||||||
|
if v == val then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Utility Functions --
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
-- Movement Methods --
|
||||||
|
|
||||||
|
creatura.registered_movement_methods = {}
|
||||||
|
|
||||||
|
function creatura.register_movement_method(name, func)
|
||||||
|
creatura.registered_movement_methods[name] = func
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Utility Behaviors --
|
||||||
|
|
||||||
|
creatura.registered_utilities = {}
|
||||||
|
|
||||||
|
function creatura.register_utility(name, func)
|
||||||
|
creatura.registered_utilities[name] = func
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sensors --
|
||||||
|
|
||||||
|
function creatura.is_pos_moveable(pos, width, height)
|
||||||
|
local pos1 = {
|
||||||
|
x = pos.x - (width + 0.2),
|
||||||
|
y = pos.y,
|
||||||
|
z = pos.z - (width + 0.2),
|
||||||
|
}
|
||||||
|
local pos2 = {
|
||||||
|
x = pos.x + (width + 0.2),
|
||||||
|
y = pos.y,
|
||||||
|
z = pos.z + (width + 0.2),
|
||||||
|
}
|
||||||
|
for x = pos1.x, pos2.x do
|
||||||
|
for z = pos1.z, pos2.z do
|
||||||
|
local pos3 = {x = x, y = (pos.y + height), z = z}
|
||||||
|
local pos4 = {x = pos3.x, y = pos.y, z = pos3.z}
|
||||||
|
local ray = minetest.raycast(pos3, pos4, false, false)
|
||||||
|
for pointed_thing in ray do
|
||||||
|
if pointed_thing.type == "node" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local moveable = creatura.is_pos_moveable
|
||||||
|
|
||||||
|
function creatura.fast_ray_sight(pos1, pos2, water)
|
||||||
|
local ray = minetest.raycast(pos1, pos2, false, water or false)
|
||||||
|
for pointed_thing in ray do
|
||||||
|
if pointed_thing.type == "node" then
|
||||||
|
return false, vec_dist(pos1, pointed_thing.intersection_point), pointed_thing.ref
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true, vec_dist(pos1, pos2)
|
||||||
|
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 scan_width = width * 2
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
pos.y = floor(pos.y + 0.5)
|
||||||
|
if last_move
|
||||||
|
and last_move.pos then
|
||||||
|
local last_call = minetest.get_position_from_hash(last_move.pos)
|
||||||
|
local last_move = minetest.get_position_from_hash(last_move.move)
|
||||||
|
if vector.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({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)
|
||||||
|
if vector.equals(neighbor, pos2) then
|
||||||
|
can_move = true
|
||||||
|
end
|
||||||
|
if not self:is_pos_safe(vec_raise(neighbor, 0.6)) then
|
||||||
|
can_move = false
|
||||||
|
end
|
||||||
|
if can_move
|
||||||
|
and not moveable(vec_raise(neighbor, 0.6), width, height) then
|
||||||
|
can_move = false
|
||||||
|
end
|
||||||
|
local dist = vec_dist(neighbor, pos2)
|
||||||
|
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 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)
|
||||||
|
local last_move = minetest.get_position_from_hash(last_move.move)
|
||||||
|
if vector.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 vector.equals(neighbor, pos2) then
|
||||||
|
can_move = true
|
||||||
|
end
|
||||||
|
local dist = vec_dist(neighbor, pos2)
|
||||||
|
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)
|
||||||
|
local _, dist, node = fast_ray_sight(pos, pos2, water or false)
|
||||||
|
return dist, node
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.sensor_ceil(self, range, water)
|
||||||
|
local pos = vec_raise(self.object:get_pos(), self.height)
|
||||||
|
local pos2 = vec_raise(pos, range)
|
||||||
|
local _, dist, node = fast_ray_sight(pos, pos2, water or false)
|
||||||
|
return dist, node
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Misc
|
||||||
|
|
||||||
|
function creatura.is_valid(mob)
|
||||||
|
if not mob then return false end
|
||||||
|
if type(mob) == "table" then mob = mob.object end
|
||||||
|
if type(mob) == "userdata" then
|
||||||
|
if mob:is_player() then
|
||||||
|
if mob:get_look_horizontal() then return mob end
|
||||||
|
else
|
||||||
|
if mob:get_yaw() then return mob end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.is_alive(mob)
|
||||||
|
if not creatura.is_valid(mob) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if type(mob) == "table" then
|
||||||
|
return mob.hp > 0
|
||||||
|
end
|
||||||
|
if mob:is_player() then
|
||||||
|
return mob:get_hp() > 0
|
||||||
|
else
|
||||||
|
local ent = mob:get_luaentity()
|
||||||
|
return ent and ent.hp and ent.hp > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.get_nearby_player(self)
|
||||||
|
local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range)
|
||||||
|
for _, object in ipairs(objects) do
|
||||||
|
if object:is_player()
|
||||||
|
and creatura.is_alive(object) then
|
||||||
|
return object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.get_nearby_players(self)
|
||||||
|
local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range)
|
||||||
|
local nearby = {}
|
||||||
|
for _, object in ipairs(objects) do
|
||||||
|
if object:is_player()
|
||||||
|
and creatura.is_alive(object) then
|
||||||
|
table.insert(nearby, object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nearby
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.get_nearby_entity(self, name)
|
||||||
|
local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range)
|
||||||
|
for _, object in ipairs(objects) do
|
||||||
|
if creatura.is_alive(object)
|
||||||
|
and not object:is_player()
|
||||||
|
and object ~= self.object
|
||||||
|
and object:get_luaentity().name == name then
|
||||||
|
return object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.get_nearby_entities(self, name)
|
||||||
|
local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range)
|
||||||
|
local nearby = {}
|
||||||
|
for _, object in ipairs(objects) do
|
||||||
|
if creatura.is_alive(object)
|
||||||
|
and not object:is_player()
|
||||||
|
and object ~= self.object
|
||||||
|
and object:get_luaentity().name == name then
|
||||||
|
table.insert(nearby, object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nearby
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.get_node_def(pos)
|
||||||
|
local def = minetest.registered_nodes[minetest.get_node(pos).name]
|
||||||
|
return def
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
-- Global Mob API --
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
-- Drops --
|
||||||
|
|
||||||
|
function creatura.drop_items(self)
|
||||||
|
if not self.drops then return end
|
||||||
|
for i = 1, #self.drops do
|
||||||
|
local drop_def = self.drops[i]
|
||||||
|
local name = drop_def.name
|
||||||
|
if not name then return end
|
||||||
|
local min_amount = drop_def.min or 1
|
||||||
|
local max_amount = drop_def.max or 2
|
||||||
|
local chance = drop_def.chance or 1
|
||||||
|
local amount = random(min_amount, max_amount)
|
||||||
|
if random(chance) < 2 then
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
local item = minetest.add_item(pos, ItemStack(name .. " " .. amount))
|
||||||
|
if item then
|
||||||
|
item:add_velocity({
|
||||||
|
x = random(-2, 2),
|
||||||
|
y = 1.5,
|
||||||
|
z = random(-2, 2)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- On Punch --
|
||||||
|
|
||||||
|
function creatura.basic_punch_func(self, puncher, time_from_last_punch, tool_capabilities, direction, damage)
|
||||||
|
if not puncher then return end
|
||||||
|
local tool = ""
|
||||||
|
if puncher:is_player() then
|
||||||
|
tool = puncher:get_wielded_item():get_name()
|
||||||
|
end
|
||||||
|
if (self.immune_to
|
||||||
|
and is_value_in_table(self.immune_to, tool)) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local dir = vec_dir(puncher:get_pos(), self:get_center_pos())
|
||||||
|
self:apply_knockback(dir)
|
||||||
|
self:hurt(tool_capabilities.damage_groups.fleshy or 2)
|
||||||
|
if random(4) < 2 then
|
||||||
|
self:play_sound("hurt")
|
||||||
|
end
|
||||||
|
if time_from_last_punch > 0.5 then
|
||||||
|
self:play_sound("hit")
|
||||||
|
end
|
||||||
|
indicate_damage(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
local path = minetest.get_modpath("creatura")
|
||||||
|
|
||||||
|
dofile(path.."/mob_meta.lua")
|
158
boids.lua
Normal file
158
boids.lua
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
-----------
|
||||||
|
-- Boids --
|
||||||
|
-----------
|
||||||
|
|
||||||
|
local random = math.random
|
||||||
|
|
||||||
|
local function average(tbl)
|
||||||
|
local sum = 0
|
||||||
|
for _,v in pairs(tbl) do -- Get the sum of all numbers in t
|
||||||
|
sum = sum + v
|
||||||
|
end
|
||||||
|
return sum / #tbl
|
||||||
|
end
|
||||||
|
|
||||||
|
local function average_angle(tbl)
|
||||||
|
local sum_sin, sum_cos = 0, 0
|
||||||
|
for _, v in pairs(tbl) do
|
||||||
|
sum_sin = sum_sin + math.sin(v)
|
||||||
|
sum_cos = sum_cos + math.cos(v)
|
||||||
|
end
|
||||||
|
return math.atan2(sum_sin, sum_cos)
|
||||||
|
end
|
||||||
|
|
||||||
|
local vec_dist = vector.distance
|
||||||
|
local vec_dir = vector.direction
|
||||||
|
local vec_len = vector.length
|
||||||
|
local vec_add = vector.add
|
||||||
|
local vec_multi = vector.multiply
|
||||||
|
local vec_normal = vector.normalize
|
||||||
|
local vec_divide = vector.divide
|
||||||
|
local function vec_raise(v, n)
|
||||||
|
return {x = v.x, y = v.y + n, z = v.z}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_average_pos(vectors)
|
||||||
|
local sum = {x = 0, y = 0, z = 0}
|
||||||
|
for _, vec in pairs(vectors) do sum = vec_add(sum, vec) end
|
||||||
|
return vec_divide(sum, #vectors)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dist_2d(pos1, pos2)
|
||||||
|
local a = vector.new(
|
||||||
|
pos1.x,
|
||||||
|
0,
|
||||||
|
pos1.z
|
||||||
|
)
|
||||||
|
local b = vector.new(
|
||||||
|
pos2.x,
|
||||||
|
0,
|
||||||
|
pos2.z
|
||||||
|
)
|
||||||
|
return vec_dist(a, b)
|
||||||
|
end
|
||||||
|
|
||||||
|
local yaw2dir = minetest.yaw_to_dir
|
||||||
|
local dir2yaw = minetest.dir_to_yaw
|
||||||
|
|
||||||
|
-- Refresh Boid Leader --
|
||||||
|
|
||||||
|
local last_boid_refresh = minetest.get_us_time()
|
||||||
|
|
||||||
|
-- Get Boid Members --
|
||||||
|
|
||||||
|
-- This function scans within
|
||||||
|
-- a set radius for potential
|
||||||
|
-- boid members, and assigns
|
||||||
|
-- a leader. A new leader
|
||||||
|
-- is only assigned every 12
|
||||||
|
-- seconds or if a new mob
|
||||||
|
-- is in the boid.
|
||||||
|
|
||||||
|
function creatura.get_boid_members(pos, radius, name)
|
||||||
|
local objects = minetest.get_objects_inside_radius(pos, radius)
|
||||||
|
if #objects < 2 then return {} end
|
||||||
|
local members = {}
|
||||||
|
local max_boid = minetest.registered_entities[name].max_boids or 7
|
||||||
|
for i = 1, #objects do
|
||||||
|
if #members > max_boid then break end
|
||||||
|
local object = objects[i]
|
||||||
|
if object:get_luaentity()
|
||||||
|
and object:get_luaentity().name == name then
|
||||||
|
object:get_luaentity().boid_heading = math.rad(random(360))
|
||||||
|
table.insert(members, object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return members
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calculate Boid angles and offsets.
|
||||||
|
|
||||||
|
local function debugpart(pos, time, part)
|
||||||
|
minetest.add_particle({
|
||||||
|
pos = pos,
|
||||||
|
expirationtime = time or 0.2,
|
||||||
|
size = 8,
|
||||||
|
glow = 16,
|
||||||
|
texture = part or "creatura_particle_red.png"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.get_boid_angle(self, boid, range) -- calculates boid angle based on seperation, alignment, and cohesion
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
local boids = boid or creatura.get_boid_members(pos, range or 4, self.name)
|
||||||
|
if #boids < 3 then return end
|
||||||
|
local yaw = self.object:get_yaw()
|
||||||
|
local lift = self.object:get_velocity().y
|
||||||
|
-- Add Boid data to tables
|
||||||
|
local closest_pos
|
||||||
|
local positions = {}
|
||||||
|
local angles = {}
|
||||||
|
local lifts = {}
|
||||||
|
for i = 1, #boids do
|
||||||
|
local boid = boids[i]
|
||||||
|
if boid:get_pos() then
|
||||||
|
local boid_pos = boid:get_pos()
|
||||||
|
local boid_yaw = boid:get_yaw()
|
||||||
|
table.insert(positions, boid_pos)
|
||||||
|
if boid ~= self.object then
|
||||||
|
table.insert(lifts, boid:get_velocity().y)
|
||||||
|
table.insert(angles, boid:get_yaw())
|
||||||
|
if not closest_pos
|
||||||
|
or vec_dist(pos, boid_pos) < vec_dist(pos, closest_pos) then
|
||||||
|
closest_pos = boid_pos
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #positions < 3 then return end
|
||||||
|
local center = get_average_pos(positions)
|
||||||
|
local dir2closest = vec_dir(pos, closest_pos)
|
||||||
|
-- Calculate Parameters
|
||||||
|
local alignment = average_angle(angles)
|
||||||
|
center = vec_add(center, yaw2dir(alignment))
|
||||||
|
local dir2center = vec_dir(pos, center)
|
||||||
|
local seperation = dir2yaw(vector.multiply(dir2center, -1))
|
||||||
|
local cohesion = dir2yaw(dir2center)
|
||||||
|
local params = {alignment}
|
||||||
|
if self.boid_heading then
|
||||||
|
table.insert(params, yaw + self.boid_heading)
|
||||||
|
end
|
||||||
|
if dist_2d(pos, closest_pos) < (self.boid_seperation or self.width * 3) then -- seperation is causing north issue
|
||||||
|
table.insert(params, seperation)
|
||||||
|
elseif dist_2d(pos, center) > (#boids * 0.33) * (self.boid_seperation or self.width * 3) then
|
||||||
|
table.insert(params, cohesion)
|
||||||
|
end
|
||||||
|
-- Vertical Params
|
||||||
|
local vert_alignment = average(lifts)
|
||||||
|
local vert_seperation = (self.speed or 2) * -dir2closest.y
|
||||||
|
local vert_cohesion = (self.speed or 2) * dir2center.y
|
||||||
|
local vert_params = {vert_alignment}
|
||||||
|
if math.abs(pos.y - closest_pos.y) < (self.boid_seperation or self.width * 3) then
|
||||||
|
table.insert(vert_params, vert_seperation)
|
||||||
|
elseif math.abs(pos.y - closest_pos.y) > 1.5 * (self.boid_seperation or self.width * 3) then
|
||||||
|
table.insert(vert_params, vert_cohesion + (lift - vert_cohesion) * 0.1)
|
||||||
|
end
|
||||||
|
self.boid_heading = nil
|
||||||
|
return average_angle(params), average_angle(vert_params)
|
||||||
|
end
|
140
doc.txt
Normal file
140
doc.txt
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
|
||||||
|
Registration
|
||||||
|
------------
|
||||||
|
|
||||||
|
creatura.register_mob(name, mob definition)
|
||||||
|
|
||||||
|
Mob Definition uses almost all entity definition params
|
||||||
|
|
||||||
|
{
|
||||||
|
max_health = 10 -- Maximum Health
|
||||||
|
damage = 0 -- Damage dealt by mob
|
||||||
|
speed = 4 -- Maximum Speed
|
||||||
|
tracking_range = 16 -- Maximum range for finding entities/blocks
|
||||||
|
despawn_after = 1500 -- Despawn after being active for this amount of time
|
||||||
|
|
||||||
|
max_fall = 8 -- How far a mob can fall before taking damage (set to 0 to disable fall damage)
|
||||||
|
turn_rate = 7 -- Turn Rate in rad/s
|
||||||
|
bouyancy_multiplier = 1 -- Multiplier for bouyancy effects (set to 0 to disable bouyancy)
|
||||||
|
hydrodynamics_multiplier = 1 -- Multiplier for hydroynamic effects (set to 0 to disable hydrodynamics)
|
||||||
|
|
||||||
|
hitbox = { -- Hitbox params (Uses custom registration to force get_pos() to always return bottom of box)
|
||||||
|
width = 0.5, (total width = width * 2. A width of 0.5 results in a box with a total width of 1)
|
||||||
|
height = 1 (total height of box)
|
||||||
|
}
|
||||||
|
|
||||||
|
animations = {
|
||||||
|
anim = {range = {x = 1, y = 10}, speed = 30, frame_blend = 0.3, loop = true}
|
||||||
|
}
|
||||||
|
|
||||||
|
drops = {
|
||||||
|
{name = (itemstring), min = 1, max = 3, chance = 1},
|
||||||
|
}
|
||||||
|
follow = {
|
||||||
|
"farming:seed_wheat",
|
||||||
|
"farming:seed_cotton"
|
||||||
|
}
|
||||||
|
|
||||||
|
utility_stack = {
|
||||||
|
-- Every second, all utilities in the stack are evaluated
|
||||||
|
-- Whichever utilitiy's get_score function returns the highest number will be executed
|
||||||
|
-- If multiple utilities have the same score, the one with the highest index is executed
|
||||||
|
[1] = {
|
||||||
|
`utility` -- name of utility to evaluate
|
||||||
|
`get_score` -- function (only accepts `self` as an arg) that returns a number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activate_func = function(self, staticdata, dtime_s) -- called upon activation
|
||||||
|
step_func = function(self, dtime, moveresult) -- called every server step
|
||||||
|
death_func = function(self) -- called when mobs health drops to/below 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Lua Entity Methods
|
||||||
|
------------------
|
||||||
|
|
||||||
|
`move(pos, method, speed, animation)`
|
||||||
|
- `pos`: position to move to
|
||||||
|
- `method`: method used to move to `pos`
|
||||||
|
- `speed`: multiplier for `speed`
|
||||||
|
- `animation`: animation to play while moving
|
||||||
|
|
||||||
|
`halt()`
|
||||||
|
- stops movement
|
||||||
|
|
||||||
|
`turn_to(yaw[, turn_rate])`
|
||||||
|
- `yaw`: yaw (in radians) to turn to
|
||||||
|
- `turn_rate`: turn rate in rad/s (default: 10) -- likely to be deprecated
|
||||||
|
|
||||||
|
`set_gravity(gravity)`
|
||||||
|
- `gravity`: vertical acceleration rate
|
||||||
|
|
||||||
|
`set_forward_velocity(speed)`
|
||||||
|
- `speed`: rate in m/s to travel forward at
|
||||||
|
|
||||||
|
`set_vertical_velocity(speed)`
|
||||||
|
- `speed`: rate in m/s to travel vertically at
|
||||||
|
|
||||||
|
`apply_knockback(dir, power)`
|
||||||
|
- `dir`: direction vector
|
||||||
|
- `power`: multiplier for dir
|
||||||
|
|
||||||
|
`punch_target(target)`
|
||||||
|
- applies 'damage' to 'target'
|
||||||
|
|
||||||
|
`hurt(damage)`
|
||||||
|
- `damage`: number to subtract from health (ignores armor)
|
||||||
|
|
||||||
|
`heal(health)`
|
||||||
|
- `health`: number to add to health
|
||||||
|
|
||||||
|
`get_center_pos()`
|
||||||
|
- returns position at center of hitbox
|
||||||
|
|
||||||
|
`pos_in_box(pos[, size])`
|
||||||
|
- returns true if 'pos' is within hitbox
|
||||||
|
- `size`: width of box to check in (optional)
|
||||||
|
|
||||||
|
`animate(anim)`
|
||||||
|
- sets animation to `anim`
|
||||||
|
|
||||||
|
`set_texture(id, tbl)`
|
||||||
|
- `id`: table index
|
||||||
|
- `tbl`: table of textures
|
||||||
|
|
||||||
|
`set_scale(x)`
|
||||||
|
- `x`: multiplier for base scale (0.5 sets scale to half, 2 sets scale to double)
|
||||||
|
|
||||||
|
`fix_attached_scale(parent)`
|
||||||
|
- sets scale to appropriate value when attached to 'parent'
|
||||||
|
- `parent`: object
|
||||||
|
|
||||||
|
`memorize(id, val)`
|
||||||
|
-- stores `val` to staticdata
|
||||||
|
- `id`: key for table
|
||||||
|
- `val`: value to store
|
||||||
|
|
||||||
|
`forget(id)`
|
||||||
|
-- removes `id` from staticdata
|
||||||
|
|
||||||
|
`recall(id)`
|
||||||
|
-- returns value of `id` from staticdata
|
||||||
|
|
||||||
|
`timer(n)`
|
||||||
|
-- returns true avery `n` seconds
|
||||||
|
|
||||||
|
`get_hitbox()`
|
||||||
|
-- returns current hitbox
|
||||||
|
|
||||||
|
`get_height()`
|
||||||
|
-- returns current height
|
||||||
|
|
||||||
|
`get_visual_size()`
|
||||||
|
-- returns current visual size
|
||||||
|
|
||||||
|
`follow_wielded_item(player)`
|
||||||
|
-- returns itemstack, item name of `player`s wielded item if item is in 'follow'
|
||||||
|
|
||||||
|
`get_target(target)`
|
||||||
|
-- returns if `target` is alive, if mob has a line of sight with `target`, `target`s position
|
||||||
|
|
24
init.lua
Normal file
24
init.lua
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
creatura = {}
|
||||||
|
|
||||||
|
local path = minetest.get_modpath("creatura")
|
||||||
|
|
||||||
|
dofile(path.."/pathfinder.lua")
|
||||||
|
dofile(path.."/api.lua")
|
||||||
|
dofile(path.."/methods.lua")
|
||||||
|
|
||||||
|
-- Optional Files --
|
||||||
|
|
||||||
|
-- Optional files can be safely removed
|
||||||
|
-- by game developers who don't need the
|
||||||
|
-- extra features
|
||||||
|
|
||||||
|
local function load_file(filepath, filename)
|
||||||
|
if io.open(filepath .. "/" .. filename, "r") then
|
||||||
|
dofile(filepath .. "/" .. filename)
|
||||||
|
else
|
||||||
|
minetest.log("action", "[Creatura] The file " .. filename .. " could not be loaded.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load_file(path, "boids.lua")
|
||||||
|
load_file(path, "spawning.lua")
|
386
methods.lua
Normal file
386
methods.lua
Normal file
|
@ -0,0 +1,386 @@
|
||||||
|
-------------
|
||||||
|
-- Methods --
|
||||||
|
-------------
|
||||||
|
|
||||||
|
local pi = math.pi
|
||||||
|
local pi2 = pi * 2
|
||||||
|
local abs = math.abs
|
||||||
|
local floor = math.floor
|
||||||
|
local random = math.random
|
||||||
|
local rad = math.rad
|
||||||
|
|
||||||
|
local function diff(a, b) -- Get difference between 2 angles
|
||||||
|
return math.atan2(math.sin(b - a), math.cos(b - a))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clamp(val, min, max)
|
||||||
|
if val < min then
|
||||||
|
val = min
|
||||||
|
elseif max < val then
|
||||||
|
val = max
|
||||||
|
end
|
||||||
|
return val
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
return {x = v.x, y = v.y + n, z = v.z}
|
||||||
|
end
|
||||||
|
|
||||||
|
local vec_dir = vector.direction
|
||||||
|
local vec_dist = vector.distance
|
||||||
|
local vec_multi = vector.multiply
|
||||||
|
local vec_add = vector.add
|
||||||
|
local yaw2dir = minetest.yaw_to_dir
|
||||||
|
|
||||||
|
-------------
|
||||||
|
-- Actions --
|
||||||
|
-------------
|
||||||
|
|
||||||
|
-- Actions are more specific behaviors used
|
||||||
|
-- to compose a Utility.
|
||||||
|
|
||||||
|
-- Walk
|
||||||
|
|
||||||
|
function creatura.action_walk(self, pos2, timeout, method, speed_factor, anim)
|
||||||
|
local timer = timeout or 4
|
||||||
|
local move_init = false
|
||||||
|
local function func(self)
|
||||||
|
if not pos2
|
||||||
|
or (move_init
|
||||||
|
and not self._movement_data.goal) then return true end
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
timer = timer - self.dtime
|
||||||
|
if timer <= 0
|
||||||
|
or self:pos_in_box({x = pos2.x, y = pos.y + 0.1, z = pos2.z}) then
|
||||||
|
self:halt()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
self:move(pos2, method or "creatura:neighbors", speed_factor or 0.5, anim)
|
||||||
|
move_init = true
|
||||||
|
end
|
||||||
|
self:set_action(func)
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.action_fly(self, pos2, timeout, method, speed_factor, anim)
|
||||||
|
local timer = timeout or 4
|
||||||
|
local move_init = false
|
||||||
|
local function func(self)
|
||||||
|
if not pos2
|
||||||
|
or (move_init
|
||||||
|
and not self._movement_data.goal) then return true end
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
timer = timer - self.dtime
|
||||||
|
if timer <= 0
|
||||||
|
or self:pos_in_box(pos2) then
|
||||||
|
self:halt()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
self:move(pos2, method, speed_factor or 0.5, anim)
|
||||||
|
move_init = true
|
||||||
|
end
|
||||||
|
self:set_action(func)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Idle
|
||||||
|
|
||||||
|
function creatura.action_idle(self, time, anim)
|
||||||
|
local timer = time
|
||||||
|
local function func(self)
|
||||||
|
self:set_gravity(-9.8)
|
||||||
|
self:halt()
|
||||||
|
self:animate(anim or "stand")
|
||||||
|
timer = timer - self.dtime
|
||||||
|
if timer <= 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:set_action(func)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rotate on Z axis in random direction until 90 degree angle is reached
|
||||||
|
|
||||||
|
function creatura.action_fallover(self)
|
||||||
|
local zrot = 0
|
||||||
|
local init = false
|
||||||
|
local dir = 1
|
||||||
|
local function func(self)
|
||||||
|
if not init then
|
||||||
|
self:animate("stand")
|
||||||
|
if random(2) < 2 then
|
||||||
|
dir = -1
|
||||||
|
end
|
||||||
|
init = true
|
||||||
|
end
|
||||||
|
local rot = self.object:get_rotation()
|
||||||
|
local goal = (pi * 0.5) * dir
|
||||||
|
local dif = abs(rot.z - goal)
|
||||||
|
zrot = rot.z + (dif * dir) * 0.15
|
||||||
|
self.object:set_rotation({x = rot.x, y = rot.y, z = zrot})
|
||||||
|
if (dir > 0 and zrot >= goal)
|
||||||
|
or (dir < 0 and zrot <= goal) then return true end
|
||||||
|
end
|
||||||
|
self:set_action(func)
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
-- Movement Methods --
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
-- Pathfinding
|
||||||
|
|
||||||
|
function get_line_of_sight(a, b)
|
||||||
|
local steps = floor(vector.distance(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 minetest.registered_nodes[node.name].walkable then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function movement_theta_pathfind(self, pos2, speed)
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
local goal = pos2
|
||||||
|
self._path = self._path or {}
|
||||||
|
local temp_goal = self._movement_data.temp_goal
|
||||||
|
if not temp_goal
|
||||||
|
or self:pos_in_box({x = temp_goal.x, y = pos.y + self.height * 0.5, z = temp_goal.z}) then
|
||||||
|
self._movement_data.temp_goal = creatura.get_next_move(self, pos2)
|
||||||
|
temp_goal = self._movement_data.temp_goal
|
||||||
|
end
|
||||||
|
if #self._path < 1 then
|
||||||
|
self._path = creatura.find_theta_path(self, self.object:get_pos(), pos2, self.width, self.height, 500) or {}
|
||||||
|
else
|
||||||
|
temp_goal = self._path[2] or self._path[1]
|
||||||
|
if self:pos_in_box({x = temp_goal.x, y = pos.y + self.height * 0.5, z = temp_goal.z}) then
|
||||||
|
table.remove(self._path, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
goal.y = pos.y + 0.5
|
||||||
|
local dir = vector.direction(self.object:get_pos(), pos2)
|
||||||
|
local tyaw = minetest.dir_to_yaw(dir)
|
||||||
|
local turn_rate = self.turn_rate or 10
|
||||||
|
if temp_goal then
|
||||||
|
dir = vector.direction(self.object:get_pos(), temp_goal)
|
||||||
|
tyaw = minetest.dir_to_yaw(dir)
|
||||||
|
if #self._path < 1
|
||||||
|
and not self:is_pos_safe(temp_goal) then
|
||||||
|
self:animate("walk")
|
||||||
|
self:set_forward_velocity(0)
|
||||||
|
self:halt()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:turn_to(tyaw, turn_rate)
|
||||||
|
self:animate("walk")
|
||||||
|
self:set_gravity(-9.8)
|
||||||
|
self:set_forward_velocity(speed or 2)
|
||||||
|
if self:pos_in_box(goal) then
|
||||||
|
self:halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
creatura.register_movement_method("creatura:theta_pathfind", movement_theta_pathfind)
|
||||||
|
|
||||||
|
local function movement_pathfind(self, pos2, speed)
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
local goal = pos2
|
||||||
|
local temp_goal = self._movement_data.temp_goal
|
||||||
|
self._path = self._path or {}
|
||||||
|
if (not temp_goal
|
||||||
|
or self:pos_in_box({x = temp_goal.x, y = pos.y + self.height * 0.5, z = temp_goal.z}))
|
||||||
|
and #self._path < 1 then
|
||||||
|
self._movement_data.temp_goal = creatura.get_next_move(self, pos2)
|
||||||
|
temp_goal = self._movement_data.temp_goal
|
||||||
|
end
|
||||||
|
if #self._path < 2 then
|
||||||
|
self._path = creatura.find_path(self, self.object:get_pos(), pos2, self.width, self.height, 100) or {}
|
||||||
|
else
|
||||||
|
temp_goal = self._path[2]
|
||||||
|
if self:pos_in_box({x = temp_goal.x, y = pos.y + self.height * 0.5, z = temp_goal.z}) then
|
||||||
|
table.remove(self._path, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
goal.y = pos.y + 0.5
|
||||||
|
local dir = vector.direction(self.object:get_pos(), pos2)
|
||||||
|
local tyaw = minetest.dir_to_yaw(dir)
|
||||||
|
local turn_rate = self.turn_rate or 10
|
||||||
|
if temp_goal then
|
||||||
|
dir = vector.direction(self.object:get_pos(), temp_goal)
|
||||||
|
tyaw = minetest.dir_to_yaw(dir)
|
||||||
|
if #self._path < 2
|
||||||
|
and not self:is_pos_safe(temp_goal) then
|
||||||
|
self:animate("walk")
|
||||||
|
self:set_forward_velocity(0)
|
||||||
|
self:halt()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:turn_to(tyaw, turn_rate)
|
||||||
|
self:animate("walk")
|
||||||
|
self:set_gravity(-9.8)
|
||||||
|
self:set_forward_velocity(speed or 2)
|
||||||
|
if self:pos_in_box(pos2) then
|
||||||
|
self:halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
creatura.register_movement_method("creatura:pathfind", movement_pathfind)
|
||||||
|
|
||||||
|
-- Obstacle Avoidance
|
||||||
|
|
||||||
|
local function moveable(pos, width, height)
|
||||||
|
local pos1 = {
|
||||||
|
x = pos.x - (width + 0.2),
|
||||||
|
y = pos.y,
|
||||||
|
z = pos.z - (width + 0.2),
|
||||||
|
}
|
||||||
|
local pos2 = {
|
||||||
|
x = pos.x + (width + 0.2),
|
||||||
|
y = pos.y,
|
||||||
|
z = pos.z + (width + 0.2),
|
||||||
|
}
|
||||||
|
for z = pos1.z, pos2.z do
|
||||||
|
for x = pos1.x, pos2.x do
|
||||||
|
local pos3 = {x = x, y = (pos.y + height), z = z}
|
||||||
|
local pos4 = {x = x, y = pos.y, z = z}
|
||||||
|
local ray = minetest.raycast(pos3, pos4, false, false)
|
||||||
|
for pointed_thing in ray do
|
||||||
|
if pointed_thing.type == "node" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_obstacle_avoidance(self, pos2)
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
local yaw = minetest.dir_to_yaw(vec_dir(pos, pos2))
|
||||||
|
pos.y = pos.y + self.stepheight
|
||||||
|
local height = self.height
|
||||||
|
local width = self.width
|
||||||
|
local outset = vec_center(vec_add(pos, vec_multi(yaw2dir(yaw), width + 0.2)))
|
||||||
|
local pos2
|
||||||
|
if not moveable(outset, width, height) then
|
||||||
|
yaw = self.object:get_yaw()
|
||||||
|
for i = 1, 89, 45 do
|
||||||
|
angle = rad(i)
|
||||||
|
dir = vec_multi(yaw2dir(yaw + angle), width + 0.2)
|
||||||
|
pos2 = vec_center(vec_add(pos, dir))
|
||||||
|
if moveable(pos2, width, height) then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
angle = -rad(i)
|
||||||
|
dir = vec_multi(yaw2dir(yaw + angle), width + 0.2)
|
||||||
|
pos2 = vec_center(vec_add(pos, dir))
|
||||||
|
if moveable(pos2, width, height) then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return pos2
|
||||||
|
end
|
||||||
|
|
||||||
|
local function movement_obstacle_avoidance(self, pos2, speed)
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
local temp_goal = self._movement_data.temp_goal
|
||||||
|
if not temp_goal
|
||||||
|
or self:pos_in_box(temp_goal) then
|
||||||
|
self._movement_data.temp_goal = get_obstacle_avoidance(self, pos2)
|
||||||
|
temp_goal = self._movement_data.temp_goal
|
||||||
|
if temp_goal then
|
||||||
|
temp_goal.y = floor(pos.y + self.height * 0.5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pos2.y = floor(pos2.y + 0.5)
|
||||||
|
local dir = vector.direction(pos, pos2)
|
||||||
|
local tyaw = minetest.dir_to_yaw(dir)
|
||||||
|
local turn_rate = self.turn_rate or 10
|
||||||
|
if temp_goal then
|
||||||
|
dir = vector.direction(pos, temp_goal)
|
||||||
|
tyaw = minetest.dir_to_yaw(dir)
|
||||||
|
end
|
||||||
|
local turn_diff = abs(diff(self.object:get_yaw(), tyaw))
|
||||||
|
self:turn_to(tyaw, turn_rate)
|
||||||
|
self:animate("walk")
|
||||||
|
self:set_gravity(-9.8)
|
||||||
|
self:set_forward_velocity(speed - clamp(turn_diff, 0, speed * 0.66))
|
||||||
|
if self:pos_in_box({x = pos2.x, y = pos.y + 0.1, z = pos2.z})
|
||||||
|
or (temp_goal
|
||||||
|
and not self:is_pos_safe(temp_goal)) then
|
||||||
|
self:halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
creatura.register_movement_method("creatura:obstacle_avoidance", movement_obstacle_avoidance)
|
||||||
|
|
||||||
|
-- Neighbors
|
||||||
|
|
||||||
|
local function movement_neighbors(self, pos2, speed)
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
local temp_goal = self._movement_data.temp_goal
|
||||||
|
local width = clamp(self.width, 0.5, 1.5)
|
||||||
|
if not temp_goal
|
||||||
|
or self:pos_in_box(temp_goal) then
|
||||||
|
self._movement_data.temp_goal = creatura.get_next_move(self, pos2)
|
||||||
|
temp_goal = self._movement_data.temp_goal
|
||||||
|
end
|
||||||
|
pos2.y = pos.y + self.height * 0.5
|
||||||
|
local yaw = self.object:get_yaw()
|
||||||
|
local dir = vector.direction(self.object:get_pos(), pos2)
|
||||||
|
local tyaw = minetest.dir_to_yaw(dir)
|
||||||
|
local turn_rate = self.turn_rate or 10
|
||||||
|
if temp_goal then
|
||||||
|
temp_goal.x = math.floor(temp_goal.x + 0.5)
|
||||||
|
temp_goal.z = math.floor(temp_goal.z + 0.5)
|
||||||
|
temp_goal.y = pos.y + self.height * 0.5
|
||||||
|
dir = vector.direction(self.object:get_pos(), temp_goal)
|
||||||
|
tyaw = minetest.dir_to_yaw(dir)
|
||||||
|
if not self:is_pos_safe(temp_goal) then
|
||||||
|
self:set_forward_velocity(0)
|
||||||
|
self:halt()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local yaw_diff = abs(diff(yaw, tyaw))
|
||||||
|
self:turn_to(tyaw, turn_rate)
|
||||||
|
self:set_gravity(-9.8)
|
||||||
|
if yaw_diff < pi then
|
||||||
|
self:animate("walk")
|
||||||
|
self:set_forward_velocity(speed)
|
||||||
|
end
|
||||||
|
if self:pos_in_box(pos2) then
|
||||||
|
self:halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
creatura.register_movement_method("creatura:neighbors", movement_neighbors)
|
||||||
|
|
1244
mob_meta.lua
Normal file
1244
mob_meta.lua
Normal file
File diff suppressed because it is too large
Load diff
2
mod.conf
Normal file
2
mod.conf
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
name = creatura
|
||||||
|
description = A performant, semi-modular mob API
|
610
pathfinder.lua
Normal file
610
pathfinder.lua
Normal file
|
@ -0,0 +1,610 @@
|
||||||
|
-----------------
|
||||||
|
-- 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 function is_node_walkable(name)
|
||||||
|
local def = minetest.registered_nodes[name]
|
||||||
|
return def and def.walkable
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_node_liquid(name)
|
||||||
|
local def = minetest.registered_nodes[name]
|
||||||
|
return def and def.drawtype == "liquid"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function moveable(pos, width, height)
|
||||||
|
local pos1 = {
|
||||||
|
x = pos.x - (width + 0.2),
|
||||||
|
y = pos.y,
|
||||||
|
z = pos.z - (width + 0.2),
|
||||||
|
}
|
||||||
|
local pos2 = {
|
||||||
|
x = pos.x + (width + 0.2),
|
||||||
|
y = pos.y,
|
||||||
|
z = pos.z + (width + 0.2),
|
||||||
|
}
|
||||||
|
for z = pos1.z, pos2.z do
|
||||||
|
for x = pos1.x, pos2.x do
|
||||||
|
local pos3 = {x = x, y = pos.y + height, z = z}
|
||||||
|
local pos4 = {x = x, y = pos.y, z = z}
|
||||||
|
local ray = minetest.raycast(pos3, pos4, false, false)
|
||||||
|
for pointed_thing in ray do
|
||||||
|
if pointed_thing.type == "node" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_ground_level(pos2, max_height)
|
||||||
|
local node = minetest.get_node(pos2)
|
||||||
|
local node_under = minetest.get_node({
|
||||||
|
x = pos2.x,
|
||||||
|
y = pos2.y - 1,
|
||||||
|
z = pos2.z
|
||||||
|
})
|
||||||
|
local height = 0
|
||||||
|
local walkable = is_node_walkable(node_under.name) and not is_node_walkable(node.name)
|
||||||
|
if walkable then
|
||||||
|
return pos2
|
||||||
|
elseif not walkable then
|
||||||
|
if not is_node_walkable(node_under.name) then
|
||||||
|
while not is_node_walkable(node_under.name)
|
||||||
|
and height < max_height do
|
||||||
|
pos2.y = pos2.y - 1
|
||||||
|
node_under = minetest.get_node({
|
||||||
|
x = pos2.x,
|
||||||
|
y = pos2.y - 1,
|
||||||
|
z = pos2.z
|
||||||
|
})
|
||||||
|
height = height + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
while is_node_walkable(node.name)
|
||||||
|
and height < max_height do
|
||||||
|
pos2.y = pos2.y + 1
|
||||||
|
node = minetest.get_node(pos2)
|
||||||
|
height = height + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return pos2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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 is_node_walkable(minetest.get_node(ground).name) 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
|
||||||
|
|
||||||
|
-- Find a path from start to goal
|
||||||
|
|
||||||
|
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])
|
||||||
|
if neighbor.y == pos.y
|
||||||
|
and not fly
|
||||||
|
and not swim then
|
||||||
|
neighbor = 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 is_node_liquid(minetest.get_node(neighbor).name)) 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
|
||||||
|
local current
|
||||||
|
|
||||||
|
-- Get an initial id in open set
|
||||||
|
for i, v in pairs(openSet) do
|
||||||
|
current_id = i
|
||||||
|
current = v
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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 k, v 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
|
||||||
|
}
|
||||||
|
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 get_line_of_sight(a, b)
|
||||||
|
local steps = floor(vector.distance(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 minetest.registered_nodes[node.name].walkable then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
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 = 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 is_node_liquid(minetest.get_node(neighbor).name)) 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
|
||||||
|
local current
|
||||||
|
|
||||||
|
-- Get an initial id in open set
|
||||||
|
for i, v in pairs(openSet) do
|
||||||
|
current_id = i
|
||||||
|
current = v
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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 k, v 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
|
17
settingtypes.txt
Normal file
17
settingtypes.txt
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# How mobs step up nodes.
|
||||||
|
#
|
||||||
|
# - Simple means mobs use Minetests builtin stepping.
|
||||||
|
# - Fancy means mobs will step up nodes with a quick hop but can cause lag.
|
||||||
|
creatura_step_type (Step Type) enum simple simple,fancy
|
||||||
|
|
||||||
|
# How often (in seconds) the spawn ABM is called
|
||||||
|
creatura_spawn_interval (Spawn ABM Interval) float 10
|
||||||
|
|
||||||
|
# Time (in seconds) between spawn steps
|
||||||
|
creatura_spawn_step (Spawn Step Interval) float 15
|
||||||
|
|
||||||
|
# Allotted time (in μs) per step for A* pathfinding (lower means less lag but slower pathfinding)
|
||||||
|
creatura_a_star_alloted_time (A* Pathfinding Alloted time per step) float 500
|
||||||
|
|
||||||
|
# Allotted time (in μs) per step for Theta* pathfinding (lower means less lag but slower pathfinding)
|
||||||
|
creatura_theta_star_alloted_time (Theta* Pathfinding Alloted time per step) float 700
|
BIN
sounds/creatura_hit_1.ogg
Normal file
BIN
sounds/creatura_hit_1.ogg
Normal file
Binary file not shown.
BIN
sounds/creatura_hit_2.ogg
Normal file
BIN
sounds/creatura_hit_2.ogg
Normal file
Binary file not shown.
BIN
sounds/creatura_hit_3.ogg
Normal file
BIN
sounds/creatura_hit_3.ogg
Normal file
Binary file not shown.
265
spawning.lua
Normal file
265
spawning.lua
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
--------------
|
||||||
|
-- Spawning --
|
||||||
|
--------------
|
||||||
|
|
||||||
|
creatura.registered_mob_spawns = {}
|
||||||
|
|
||||||
|
local walkable_nodes = {}
|
||||||
|
|
||||||
|
minetest.register_on_mods_loaded(function()
|
||||||
|
for name in pairs(minetest.registered_nodes) do
|
||||||
|
if name ~= "air" and name ~= "ignore" then
|
||||||
|
if minetest.registered_nodes[name].walkable then
|
||||||
|
table.insert(walkable_nodes, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Math --
|
||||||
|
|
||||||
|
local random = math.random
|
||||||
|
|
||||||
|
local function vec_raise(v, n)
|
||||||
|
return {x = v.x, y = v.y + n, z = v.z}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Registration --
|
||||||
|
|
||||||
|
local function format_name(str)
|
||||||
|
if str then
|
||||||
|
if str:match(":") then str = str:split(":")[2] end
|
||||||
|
return (string.gsub(" " .. str, "%W%l", string.upper):sub(2):gsub("_", " "))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.register_spawn_egg(name, col1, col2, inventory_image)
|
||||||
|
if col1 and col2 then
|
||||||
|
local base = "(creatura_spawning_crystal.png^[multiply:#" .. col1 .. ")"
|
||||||
|
local spots = "(creatura_spawning_crystal_overlay.png^[multiply:#" .. col2 .. ")"
|
||||||
|
inventory_image = base .. "^" .. spots
|
||||||
|
end
|
||||||
|
local mod_name = name:split(":")[1]
|
||||||
|
local mob_name = name:split(":")[2]
|
||||||
|
minetest.register_craftitem(mod_name .. ":spawn_" .. mob_name, {
|
||||||
|
description = "Spawn " .. format_name(name),
|
||||||
|
inventory_image = inventory_image,
|
||||||
|
stack_max = 99,
|
||||||
|
on_place = function(itemstack, _, pointed_thing)
|
||||||
|
local mobdef = minetest.registered_entities[name]
|
||||||
|
local spawn_offset = math.abs(mobdef.collisionbox[2])
|
||||||
|
local pos = minetest.get_pointed_thing_position(pointed_thing, true)
|
||||||
|
pos.y = (pos.y - 0.49) + spawn_offset
|
||||||
|
local object = minetest.add_entity(pos, name)
|
||||||
|
if object then
|
||||||
|
object:set_yaw(math.random(1, 6))
|
||||||
|
object:get_luaentity().last_yaw = object:get_yaw()
|
||||||
|
end
|
||||||
|
if not creative then
|
||||||
|
itemstack:take_item()
|
||||||
|
return itemstack
|
||||||
|
end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function creatura.register_mob_spawn(name, def)
|
||||||
|
local spawn = {
|
||||||
|
chance = def.chance or 5,
|
||||||
|
min_radius = def.min_height or nil,
|
||||||
|
max_radius = def.max_radius or nil,
|
||||||
|
min_height = def.min_height or 0,
|
||||||
|
max_height = def.max_height or 128,
|
||||||
|
min_light = def.min_light or 6,
|
||||||
|
max_light = def.max_light or 15,
|
||||||
|
min_group = def.min_group or 1,
|
||||||
|
max_group = def.max_group or 4,
|
||||||
|
nodes = def.nodes or nil,
|
||||||
|
biomes = def.biomes or nil,
|
||||||
|
spawn_cluster = def.spawn_cluster or false,
|
||||||
|
spawn_in_nodes = def.spawn_in_nodes or false,
|
||||||
|
send_debug = def.send_debug or false
|
||||||
|
}
|
||||||
|
creatura.registered_mob_spawns[name] = spawn
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Utility Functions --
|
||||||
|
|
||||||
|
function is_value_in_table(tbl, val)
|
||||||
|
for _, v in pairs(tbl) do
|
||||||
|
if v == val then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_biome_name(pos)
|
||||||
|
if not pos then return end
|
||||||
|
return minetest.get_biome_name(minetest.get_biome_data(pos).biome)
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_ground_level(pos)
|
||||||
|
local node = minetest.get_node(pos)
|
||||||
|
local node_def = minetest.registered_nodes[node.name]
|
||||||
|
local height = 0
|
||||||
|
while node_def.walkable
|
||||||
|
and height < 4 do
|
||||||
|
height = height + 1
|
||||||
|
node = minetest.get_node(vec_raise(pos, height))
|
||||||
|
node_def = minetest.registered_nodes[node.name]
|
||||||
|
end
|
||||||
|
return vec_raise(pos, height)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_spawnable_mobs(pos)
|
||||||
|
local biome = get_biome_name(pos)
|
||||||
|
if not biome then biome = "_nil" end
|
||||||
|
local spawnable = {}
|
||||||
|
for k, v in pairs(creatura.registered_mob_spawns) do
|
||||||
|
if not v.biomes
|
||||||
|
or is_value_in_table(v.biomes, biome) then
|
||||||
|
table.insert(spawnable, k)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return spawnable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Spawning Function --
|
||||||
|
|
||||||
|
local spawn_queue = {}
|
||||||
|
|
||||||
|
local min_spawn_radius = 16
|
||||||
|
|
||||||
|
local min_spawn_radius = 64
|
||||||
|
|
||||||
|
function execute_spawns(player)
|
||||||
|
if not player:get_pos() then return end
|
||||||
|
local pos = player:get_pos()
|
||||||
|
local spawnable_mobs = get_spawnable_mobs(pos)
|
||||||
|
if spawnable_mobs
|
||||||
|
and #spawnable_mobs > 0 then
|
||||||
|
local mob = spawnable_mobs[random(#spawnable_mobs)]
|
||||||
|
local spawn = creatura.registered_mob_spawns[mob]
|
||||||
|
if not spawn
|
||||||
|
or random(spawn.chance) > 1 then return end
|
||||||
|
local spawn_pos_center = {
|
||||||
|
x = pos.x + random(-min_spawn_radius, spawn.min_radius or min_spawn_radius),
|
||||||
|
y = pos.y,
|
||||||
|
z = pos.z + random(-min_spawn_radius, spawn.max_radius or min_spawn_radius)
|
||||||
|
}
|
||||||
|
local index_func
|
||||||
|
if spawn.spawn_in_nodes then
|
||||||
|
index_func = minetest.find_nodes_in_area
|
||||||
|
else
|
||||||
|
index_func = minetest.find_nodes_in_area_under_air
|
||||||
|
end
|
||||||
|
local spawn_on = spawn.nodes or walkable_nodes
|
||||||
|
if type(spawn_on) == "string" then
|
||||||
|
spawn_on = {spawn_on}
|
||||||
|
end
|
||||||
|
local spawn_y_array = index_func(vec_raise(spawn_pos_center, -8), vec_raise(spawn_pos_center, 8), spawn_on)
|
||||||
|
if spawn_y_array[1] then
|
||||||
|
local spawn_pos = spawn_y_array[1]
|
||||||
|
|
||||||
|
if spawn_pos.y > spawn.max_height
|
||||||
|
or spawn_pos.y < spawn.min_height then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local light = minetest.get_node_light(spawn_pos) or 7
|
||||||
|
|
||||||
|
if light > spawn.max_light
|
||||||
|
or light < spawn.min_light then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local group_size = random(spawn.min_group, spawn.max_group)
|
||||||
|
|
||||||
|
if spawn.spawn_cluster then
|
||||||
|
minetest.add_node(spawn_pos, {name = "creatura:spawn_node"})
|
||||||
|
local meta = minetest.get_meta(spawn_pos)
|
||||||
|
meta:set_string("mob", mob)
|
||||||
|
meta:set_int("cluster", group_size)
|
||||||
|
else
|
||||||
|
for _ = 1, group_size do
|
||||||
|
spawn_pos = {
|
||||||
|
x = spawn_pos.x + random(-3, 3),
|
||||||
|
y = spawn_pos.y,
|
||||||
|
z = spawn_pos.z + random(-3, 3)
|
||||||
|
}
|
||||||
|
spawn_pos = get_ground_level(spawn_pos)
|
||||||
|
minetest.add_node(spawn_pos, {name = "creatura:spawn_node"})
|
||||||
|
local meta = minetest.get_meta(spawn_pos)
|
||||||
|
meta:set_string("mob", mob)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if spawn.send_debug then
|
||||||
|
minetest.chat_send_all(mob .. " spawned at " .. minetest.pos_to_string(spawn_pos))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local spawn_step = tonumber(minetest.settings:get("creatura_spawn_step")) or 15
|
||||||
|
|
||||||
|
local spawn_tick = 0
|
||||||
|
|
||||||
|
minetest.register_globalstep(function(dtime)
|
||||||
|
spawn_tick = spawn_tick - dtime
|
||||||
|
if spawn_tick <= 0 then
|
||||||
|
for _, player in ipairs(minetest.get_connected_players()) do
|
||||||
|
execute_spawns(player)
|
||||||
|
end
|
||||||
|
spawn_tick = spawn_step
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Node --
|
||||||
|
|
||||||
|
minetest.register_node("creatura:spawn_node", {
|
||||||
|
drawtype = "airlike",
|
||||||
|
groups = {not_in_creative_inventory = 1}
|
||||||
|
})
|
||||||
|
|
||||||
|
local spawn_interval = tonumber(minetest.settings:get("creatura_spawn_interval")) or 10
|
||||||
|
|
||||||
|
minetest.register_abm({
|
||||||
|
label = "Creatura Spawning",
|
||||||
|
nodenames = {"creatura:spawn_node"},
|
||||||
|
interval = spawn_interval,
|
||||||
|
chance = 1,
|
||||||
|
action = function(pos)
|
||||||
|
local meta = minetest.get_meta(pos)
|
||||||
|
local name = meta:get_string("mob")
|
||||||
|
local amount = meta:get_int("cluster")
|
||||||
|
if amount > 0 then
|
||||||
|
for i = 1, amount do
|
||||||
|
minetest.add_entity(pos, name)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
minetest.add_entity(pos, name)
|
||||||
|
end
|
||||||
|
minetest.remove_node(pos)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
minetest.register_lbm({
|
||||||
|
name = "creatura:spawning",
|
||||||
|
nodenames = {"creatura:spawn_node"},
|
||||||
|
run_at_every_load = true,
|
||||||
|
action = function(pos)
|
||||||
|
local meta = minetest.get_meta(pos)
|
||||||
|
local name = meta:get_string("mob")
|
||||||
|
local amount = meta:get_int("cluster")
|
||||||
|
if amount > 0 then
|
||||||
|
for i = 1, amount do
|
||||||
|
minetest.add_entity(pos, name)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
minetest.add_entity(pos, name)
|
||||||
|
end
|
||||||
|
minetest.remove_node(pos)
|
||||||
|
end,
|
||||||
|
})
|
BIN
textures/creatura_particle_green.png
Normal file
BIN
textures/creatura_particle_green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
BIN
textures/creatura_particle_red.png
Normal file
BIN
textures/creatura_particle_red.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.5 KiB |
BIN
textures/creatura_smoke_particle.png
Normal file
BIN
textures/creatura_smoke_particle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
BIN
textures/creatura_spawning_crystal.png
Normal file
BIN
textures/creatura_spawning_crystal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
textures/creatura_spawning_crystal_overlay.png
Normal file
BIN
textures/creatura_spawning_crystal_overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
Loading…
Add table
Reference in a new issue