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