Initial Commit

This commit is contained in:
ElCeejo 2022-02-10 16:32:53 -08:00 committed by GitHub
parent f72b110b33
commit c251f07ae5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 3290 additions and 0 deletions

444
api.lua Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

2
mod.conf Normal file
View File

@ -0,0 +1,2 @@
name = creatura
description = A performant, semi-modular mob API

610
pathfinder.lua Normal file
View 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
View 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

Binary file not shown.

BIN
sounds/creatura_hit_2.ogg Normal file

Binary file not shown.

BIN
sounds/creatura_hit_3.ogg Normal file

Binary file not shown.

265
spawning.lua Normal file
View 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,
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB