write something there

This commit is contained in:
N-Nachtigal 2025-05-04 16:01:41 +02:00
commit b4b6c08f4f
8546 changed files with 309825 additions and 0 deletions

15
mods/creatura/.luacheckrc Normal file
View file

@ -0,0 +1,15 @@
max_line_length = 120
globals = {
"minetest",
"VoxelArea",
"creatura",
}
read_globals = {
"vector",
"ItemStack",
table = {fields = {"copy"}}
}
ignore = {"212/self", "212/this"}

21
mods/creatura/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 ElCeejo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

556
mods/creatura/api.lua Normal file
View file

@ -0,0 +1,556 @@
--------------
-- Creatura --
--------------
creatura.api = {}
-- Math --
local abs = math.abs
local floor = math.floor
local random = math.random
local function clamp(val, min_n, max_n)
if val < min_n then
val = min_n
elseif max_n < val then
val = max_n
end
return val
end
local vec_dist = vector.distance
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 API --
---------------
local function contains_val(tbl, val)
for _, v in pairs(tbl) do
if v == val then return true end
end
return false
end
----------------------------
-- Registration Functions --
----------------------------
creatura.registered_movement_methods = {}
function creatura.register_movement_method(name, func)
creatura.registered_movement_methods[name] = func
end
creatura.registered_utilities = {}
function creatura.register_utility(name, func)
creatura.registered_utilities[name] = func
end
---------------
-- Utilities --
---------------
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 or mob.health or 0) > 0
end
if mob:is_player() then
return mob:get_hp() > 0
else
local ent = mob:get_luaentity()
return ent and (ent.hp or ent.health or 0) > 0
end
end
------------------------
-- Environment access --
------------------------
local default_node_def = {walkable = true} -- both ignore and unknown nodes are walkable
function creatura.get_node_height_from_def(name)
local def = minetest.registered_nodes[name] or default_node_def
if not def then return 0.5 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 0.5 + def.node_box.fixed[5]
elseif type(def.node_box.fixed[1]) == "table" then
return 0.5 + def.node_box.fixed[1][5]
else
return 1
end
else
return 1
end
else
return 1
end
else
return 1
end
end
local get_node = minetest.get_node
function creatura.get_node_def(node) -- Node can be name or pos
if type(node) == "table" then
node = get_node(node).name
end
local def = minetest.registered_nodes[node] or default_node_def
if def.walkable
and creatura.get_node_height_from_def(node) < 0.26 then
def.walkable = false -- workaround for nodes like snow
end
return def
end
local get_node_def = creatura.get_node_def
function creatura.get_ground_level(pos, range)
range = range or 2
local above = vector.round(pos)
local under = {x = above.x, y = above.y - 1, z = above.z}
if not get_node_def(above).walkable and get_node_def(under).walkable then return above end
if get_node_def(above).walkable then
for _ = 1, range do
under = above
above = {x = above.x, y = above.y + 1, z = above.z}
if not get_node_def(above).walkable and get_node_def(under).walkable then return above end
end
end
if not get_node_def(under).walkable then
for _ = 1, range do
above = under
under = {x = under.x, y = under.y - 1, z = under.z}
if not get_node_def(above).walkable and get_node_def(under).walkable then return above end
end
end
return above
end
function creatura.is_pos_moveable(pos, width, height)
local edge1 = {
x = pos.x - (width + 0.2),
y = pos.y,
z = pos.z - (width + 0.2),
}
local edge2 = {
x = pos.x + (width + 0.2),
y = pos.y,
z = pos.z + (width + 0.2),
}
local base_p = {x = pos.x, y = pos.y, z = pos.z}
local top_p = {x = pos.x, y = pos.y + height, z = pos.z}
for z = edge1.z, edge2.z do
for x = edge1.x, edge2.x do
base_p.x, base_p.z = pos.x + x, pos.z + z
top_p.x, top_p.z = pos.x + x, pos.z + z
local ray = minetest.raycast(base_p, top_p, false, false)
for pointed_thing in ray do
if pointed_thing.type == "node" then
local name = get_node(pointed_thing.under).name
if creatura.get_node_def(name).walkable then
return false
end
end
end
end
end
return true
end
local function is_blocked_thin(pos, height)
local node
local pos2 = {
x = floor(pos.x + 0.5),
y = floor(pos.y + 0.5) - 1,
z = floor(pos.z + 0.5)
}
for _ = 1, height do
pos2.y = pos2.y + 1
node = minetest.get_node_or_nil(pos2)
if not node
or get_node_def(node.name).walkable then
return true
end
end
return false
end
function creatura.is_blocked(pos, width, height)
if width <= 0.5 then
return is_blocked_thin(pos, height)
end
local p1 = {
x = pos.x - (width + 0.2),
y = pos.y,
z = pos.z - (width + 0.2),
}
local p2 = {
x = pos.x + (width + 0.2),
y = pos.y + (height + 0.2),
z = pos.z + (width + 0.2),
}
local node
local pos2 = {}
for z = p1.z, p2.z do
pos2.z = z
for y = p1.y, p2.y do
pos2.y = y
for x = p1.x, p2.x do
pos2.x = x
node = minetest.get_node_or_nil(pos2)
if not node
or get_node_def(node.name).walkable then
return true
end
end
end
end
return false
end
function creatura.fast_ray_sight(pos1, pos2, water)
local ray = minetest.raycast(pos1, pos2, false, water or false)
local pointed_thing = ray:next()
while pointed_thing do
if pointed_thing.type == "node"
and creatura.get_node_def(pointed_thing.under).walkable then
return false, vec_dist(pos1, pointed_thing.intersection_point), pointed_thing.ref, pointed_thing.intersection_point
end
pointed_thing = ray:next()
end
return true, vec_dist(pos1, pos2), false, pos2
end
local fast_ray_sight = creatura.fast_ray_sight
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
function creatura.get_nearby_player(self, range)
local pos = self.object:get_pos()
if not pos then return end
local stored = self._nearby_obj or {}
local objects = (#stored > 0 and stored) or self:store_nearby_objects(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, range)
local pos = self.object:get_pos()
if not pos then return end
local stored = self._nearby_obj or {}
local objects = (#stored > 0 and stored) or self:store_nearby_objects(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_object(self, name, range)
local pos = self.object:get_pos()
if not pos then return end
local stored = self._nearby_obj or {}
local objects = (#stored > 0 and stored) or self:store_nearby_objects(range)
for _, object in ipairs(objects) do
local ent = creatura.is_alive(object) and object:get_luaentity()
if ent
and object ~= self.object
and not ent._ignore
and ((type(name) == "table" and contains_val(name, ent.name))
or ent.name == name) then
return object
end
end
end
function creatura.get_nearby_objects(self, name, range)
local pos = self.object:get_pos()
if not pos then return end
local stored = self._nearby_obj or {}
local objects = (#stored > 0 and stored) or self:store_nearby_objects(range)
local nearby = {}
for _, object in ipairs(objects) do
local ent = creatura.is_alive(object) and object:get_luaentity()
if ent
and object ~= self.object
and not ent._ignore
and ((type(name) == "table" and contains_val(name, ent.name))
or ent.name == name) then
table.insert(nearby, object)
end
end
return nearby
end
creatura.get_nearby_entity = creatura.get_nearby_object
creatura.get_nearby_entities = creatura.get_nearby_objects
--------------------
-- Global Mob API --
--------------------
function creatura.default_water_physics(self)
local pos = self.stand_pos
local stand_node = self.stand_node
if not pos or not stand_node then return end
local gravity = self._movement_data.gravity or -9.8
local submergence = self.liquid_submergence or 0.25
local drag = self.liquid_drag or 0.7
if minetest.get_item_group(stand_node.name, "liquid") > 0 then -- In Liquid
local vel = self.object:get_velocity()
if not vel then return end
self.in_liquid = stand_node.name
if submergence < 1 then
local mob_level = pos.y + (self.height * submergence)
-- Find Water Surface
local nodes = minetest.find_nodes_in_area_under_air(
{x = pos.x, y = pos.y, z = pos.z},
{x = pos.x, y = pos.y + 3, z = pos.z},
"group:liquid"
) or {}
local surface_level = (#nodes > 0 and nodes[#nodes].y or pos.y + self.height + 3)
surface_level = floor(surface_level + 0.9)
local height_diff = mob_level - surface_level
-- Apply Bouyancy
if height_diff <= 0 then
local displacement = clamp(abs(height_diff) / submergence, 0.5, 1) * self.width
self.object:set_acceleration({x = 0, y = displacement, z = 0})
else
self.object:set_acceleration({x = 0, y = gravity, z = 0})
end
end
-- Apply Drag
self.object:set_velocity({
x = vel.x * (1 - self.dtime * drag),
y = vel.y * (1 - self.dtime * drag),
z = vel.z * (1 - self.dtime * drag)
})
else
self.in_liquid = nil
self.object:set_acceleration({x = 0, y = gravity, z = 0})
end
end
function creatura.default_vitals(self)
local pos = self.stand_pos
local node = self.stand_node
if not pos or node then return end
local max_fall = self.max_fall or 3
local in_liquid = self.in_liquid
local on_ground = self.touching_ground
local damage = 0
-- Fall Damage
if max_fall > 0
and not in_liquid then
local fall_start = self._fall_start or (not on_ground and pos.y)
if fall_start
and on_ground then
damage = floor(fall_start - pos.y)
if damage < max_fall then
damage = 0
else
local resist = self.fall_resistance or 0
damage = damage - damage * resist
end
fall_start = nil
end
self._fall_start = fall_start
end
-- Environment Damage
if self:timer(1) then
local stand_def = creatura.get_node_def(node.name)
local max_breath = self.max_breath or 0
-- Suffocation
if max_breath > 0 then
local head_pos = {x = pos.x, y = pos.y + self.height, z = pos.z}
local head_def = creatura.get_node_def(head_pos)
if head_def.groups
and (minetest.get_item_group(head_def.name, "water") > 0
or (head_def.walkable
and head_def.groups.disable_suffocation ~= 1
and head_def.drawtype == "normal")) then
local breath = self._breath
if breath <= 0 then
damage = damage + 1
else
self._breath = breath - 1
self:memorize("_breath", breath)
end
end
end
-- Burning
local fire_resist = self.fire_resistance or 0
if fire_resist < 1
and minetest.get_item_group(stand_def.name, "igniter") > 0
and stand_def.damage_per_second then
damage = (damage or 0) + stand_def.damage_per_second * fire_resist
end
end
-- Apply Damage
if damage > 0 then
self:hurt(damage)
self:indicate_damage()
if random(4) < 2 then
self:play_sound("hurt")
end
end
-- Entity Cramming
if self:timer(5) then
local objects = minetest.get_objects_inside_radius(pos, 0.2)
if #objects > 10 then
self:indicate_damage()
self.hp = self:memorize("hp", -1)
self:death_func()
end
end
end
function creatura.drop_items(self)
if not self.drops then return end
local pos = self.object:get_pos()
if not pos then return end
local drop_def, item_name, min_items, max_items, chance, amount, drop_pos
for i = 1, #self.drops do
drop_def = self.drops[i]
item_name = drop_def.name
if not item_name then return end
chance = drop_def.chance or 1
if random(chance) < 2 then
min_items = drop_def.min or 1
max_items = drop_def.max or 2
amount = random(min_items, max_items)
drop_pos = {
x = pos.x + random(-5, 5) * 0.1,
y = pos.y,
z = pos.z + random(-5, 5) * 0.1
}
local item = minetest.add_item(drop_pos, ItemStack(item_name .. " " .. amount))
if item then
item:add_velocity({
x = random(-2, 2),
y = 1.5,
z = random(-2, 2)
})
end
end
end
end
function creatura.basic_punch_func(self, puncher, tflp, tool_caps, dir)
if not puncher then return end
local tool
local tool_name = ""
local add_wear = false
if puncher:is_player() then
tool = puncher:get_wielded_item()
tool_name = tool:get_name()
add_wear = not minetest.is_creative_enabled(puncher:get_player_name())
end
if (self.immune_to
and contains_val(self.immune_to, tool_name)) then
return
end
local damage = 0
local armor_grps = self.object:get_armor_groups() or self.armor_groups or {}
for group, val in pairs(tool_caps.damage_groups or {}) do
local dmg_x = tflp / (tool_caps.full_punch_interval or 1.4)
damage = damage + val * clamp(dmg_x, 0, 1) * ((armor_grps[group] or 0) / 100.0)
end
if damage > 0 then
local dist = vec_dist(self.object:get_pos(), puncher:get_pos())
dir.y = 0.2
if self.touching_ground then
local power = clamp((damage / dist) * 8, 0, 8)
self:apply_knockback(dir, power)
end
self:hurt(damage)
end
if add_wear then
local wear = floor((tool_caps.full_punch_interval / 75) * 9000)
tool:add_wear(wear)
puncher:set_wielded_item(tool)
end
if random(2) < 2 then
self:play_sound("hurt")
end
if (tflp or 0) > 0.5 then
self:play_sound("hit")
end
self:indicate_damage()
end
local path = minetest.get_modpath("creatura")
dofile(path.."/mob_meta.lua")

232
mods/creatura/boids.lua Normal file
View file

@ -0,0 +1,232 @@
-----------
-- Boids --
-----------
local abs = math.abs
local atan2 = math.atan2
local sin = math.sin
local cos = math.cos
local function average_angle(tbl)
local sum_sin, sum_cos = 0, 0
for _, v in pairs(tbl) do
sum_sin = sum_sin + sin(v)
sum_cos = sum_cos + cos(v)
end
return atan2(sum_sin, sum_cos)
end
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 interp_rad(a, b, w)
local cs = (1 - w) * cos(a) + w * cos(b)
local sn = (1 - w) * sin(a) + w * sin(b)
return atan2(sn, cs)
end
local vec_add = vector.add
local vec_dir = vector.direction
local vec_dist = vector.distance
local vec_divide = vector.divide
local vec_normal = vector.normalize
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
-- Get Boid Members --
function creatura.get_boid_cached(self)
local pos = self.object:get_pos()
if not pos then return end
local radius = self.tracking_range * 0.5 or 4
local members = self._movement_data.boids or {}
local max_boids = self.max_boids or 7
if #members > 0 then
for i = #members, 1, -1 do
local object = members[i]
if not object or not object:get_yaw() then members[i] = nil end
end
if #members >= max_boids then return members end
end
local objects = minetest.get_objects_inside_radius(pos, radius)
if #objects < 2 then return {} end
for _, object in ipairs(objects) do
local ent = object and object ~= self.object and object:get_luaentity()
if ent
and ent.name == self.name then
local move_data = ent._movement_data
if move_data
and (not move_data.boids
or #move_data.boids < max_boids) then
table.insert(members, object)
end
end
if #members >= max_boids then break end
end
self._movement_data.boids = members
return members
end
-- Calculate Boid Movement Direction
function creatura.get_boid_dir(self)
local pos = self.object:get_pos()
if not pos then return end
local boids = creatura.get_boid_cached(self)
if #boids < 2 then return end
local pos_no, pos_sum = 0, {x = 0, y = 0, z = 0}
local sum_sin, sum_cos = 0, 0
local lift_no, lift_sum = 0, 0
local vel
local boid_pos
local closest_pos
for _, object in ipairs(boids) do
if object then
boid_pos, vel = object:get_pos(), object:get_velocity()
if boid_pos then
vel = vec_normal(vel)
local obj_yaw = object:get_yaw()
pos_no, pos_sum = pos_no + 1, vec_add(pos_sum, boid_pos)
sum_sin, sum_cos = sum_sin + sin(obj_yaw), sum_cos + cos(obj_yaw)
lift_no, lift_sum = lift_no + 1, lift_sum + vel.y
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 not closest_pos then return end
local center = vec_divide(pos_sum, pos_no)
local lift = lift_sum / lift_no
local angle_sin, angle_cos
local radius = self.tracking_range * 0.5 or 4
local dist_factor = (radius - vec_dist(pos, closest_pos)) / radius
local alignment = atan2(sum_sin, sum_cos)
local seperation = dir2yaw(vec_dir(closest_pos, pos))
local cohesion = dir2yaw(vec_dir(pos, center))
if dist_factor > 0.9 then
seperation = interp_rad(alignment, seperation, 0.5)
angle_sin, angle_cos = sin(seperation), cos(seperation)
else
angle_sin, angle_cos = sin(cohesion), cos(cohesion)
end
local angle = atan2(angle_sin + sin(alignment), angle_cos + cos(alignment))
local dir = yaw2dir(angle)
dir.y = lift
return vector.normalize(dir), boids
end
-- Deprecated
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
table.insert(members, object)
end
end
if #members > 1 then
for _, object in ipairs(members) do
local ent = object and object:get_luaentity()
if ent then
ent._movement_data.boids = members
end
end
end
return members
end
function creatura.get_boid_angle(self, _boids, range)
local pos = self.object:get_pos()
local boids = _boids 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 vel = boid:get_velocity()
if boid ~= self.object
and (abs(vel.x) > 0.1
or abs(vel.z) > 0.1) then
local boid_pos = boid:get_pos()
table.insert(positions, boid_pos)
table.insert(lifts, vec_normal(vel).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 = yaw + -(dir2yaw(dir2closest) - yaw)
local cohesion = dir2yaw(dir2center)
local params = {alignment}
if dist_2d(pos, closest_pos) < (self.boid_seperation or self.width * 3) then
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
return average_angle(params), average(vert_params)
end

280
mods/creatura/doc.txt Normal file
View file

@ -0,0 +1,280 @@
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`, position of `target`
Utilities
---------
* `creatura.is_valid(mob)`
* Returns false if object doesn't exist, otherwise returns ObjectRef/PlayerRef
* `mob`: Luaentity, ObjectRef, or PlayerRef
* `creatura.is_alive(mob)`
* Returns false if object doesn't exist or is dead, otherwise returns ObjectRef/PlayerRef
* `mob`: Luaentity, ObjectRef, or PlayerRef
Environment access
------------------
* `creatura.get_node_height_from_def(name)`
-- Returns total height of nodebox
-- `name`: Itemstring/Name of node
* `creatura.get_node_def(node)`
-- Returns definition of node
-- `node`: Itemstring/Name of node or position
* `creatura.get_ground_level(pos, max_diff)`
* Returns first position above walkable node within `max_diff`
* `creatura.is_pos_moveable(pos, width, height)`
* Returns true if a box with specified `width` and `height` can fit at `pos`
* `width` should be the largest side of the collision box
* Check from bottom of box
* `creatura.fast_ray_sight(pos1, pos2, water)`
* Checks for line of sight between `pos1 and `pos2`
* Returns bool
* Returns distance to obstruction
* `creatura.sensor_floor(self, range, water)`
* Finds distance to ground from bottom of entities hitbox
* Returns distance to ground or `range` if no ground is found
* `range`: Maximum range
* `water`: If false, water will not be counted as ground
* `creatura.sensor_ceil(self, range, water)`
* Finds distance to ceiling from top of entities hitbox
* Returns distance to ceiling or `range` if no ceiling is found
* `range`: Maximum range
* `water`: If false, water will not be counted as ceiling
* `creatura.get_nearby_player(self)`
* Finds player within `self.tracking_range`
* Returns PlayerRef or nil
* `creatura.get_nearby_players(self)`
* Finds players within `self.tracking_range`
* Returns table of PlayerRefs or empty table
* `creatura.get_nearby_object(self, name)`
* Finds object within `self.tracking_range`
* Returns ObjectRef or nil
* `name`: Name of object to search for
* `creatura.get_nearby_objects(self, name)`
* Finds objects within `self.tracking_range`
* Returns table of ObjectRefs or empty table
* `name`: Name of object to search for
Global Mob API
--------------
* `creatura.default_water_physics(self)`
* Bouyancy and Drag physics used by default on all Mobs
* `creatura.default_vitals(self)`
* Vitals used by default on all Mobs
* Handles suffocation, drowning, fire damage, and fall damage
* `creatura.drop_items(self)`
* Drops items from `self.drops`
* `creatura.basic_punch_func(self, puncher, time_from_last_punch, tool_capabilities, direction, damage)`
* Deals damage
* Applies knockback
* Visualy and audibly indicates damage
Pathfinding
-----------
Creatura's pathfinder uses the A* algorithm for speed, as well as Theta* for decent performance and more natural looking paths.
Both pathfinders will carry out pathfinding over multiple server steps to reduce lag spikes which does result in the path not
being returned immediately, so your code will have to account for this.
The maximum amount of time the pathfinder can spend per-step (in microseconds) can be adjusted in settings.
* `creatura.pathfinder.find_path(self, pos1, pos2, get_neighbors)`
* Finds a path from `pos1` to `pos2`
* `get_neighbors` is a function used to find valid neighbors
* `creatura.pathfinder.get_neighbors_fly` and `creatura.pathfinder.get_neighbors_swim` are bundled by default
* `creatura.pathfinder.find_path_theta(self, pos1, pos2, get_neighbors)`
* Finds a path from `pos1` to `pos2`
* Returns a path with arbitrary angles for natural looking paths at the expense of performance
* `get_neighbors` is a function used to find valid neighbors
* `creatura.pathfinder.get_neighbors_fly` and `creatura.pathfinder.get_neighbors_swim` are bundled by default
Spawning
--------
NOTE: Globalstep spawning from early versions of the API likely won't recieve much/any support going forward. Use ABM Spawning instead.
* `creatura.register_abm_spawn(name, def)`
* `name` of the mob to spawn
* `def` is a table of spawn parameters
* `chance` is the chance of a mob spawning every `interval`
* (a `chance` of 30 and `interval` of 60 would mean a 1 in 30 chance of a mob spawning every 60 seconds)
* `chance_on_load` same as `chance` but for LBM spawning (when a chunk is loaded for the first time)
* `interval` is how often (in seconds) a spawn attempt will happen
* `min_height` is the minimum height that a spawn attempt can happen at
* a `min_height` of 0 would mean the mob cannot spawn below a y coordinate of 0
* `max_height` is the maximum height that a spawn attempt can happen at
* a `max_height` of 128 would mean the mob cannot spawn above a y coordinate of 128
* `min_time` is the minimum time a mob can spawn at
* `max_time` is the maximum time a mob can spawn at
* set `min_time` to 19500 and `max_time` to 4500 to only spawn at night and swap the numbers to only spawn at day
* `min_light` is the minimum light level a mob can spawn at
* `max_light` is the maximum light level a mob can spawn at
* `min_group` is the lowest number of mobs to spawn in a group at a time
* value of 3 means the mob will always spawn with at least 3 mobs together
* `max_group` is the highest number of mobs to spawn in a group at a time
* `block_protected` will block spawning mobs in protected areas if set to true
* `biomes` is a table of biomes the mob can spawn in
* `nodes` is a table of nodes the mob can spawn in/on
* `neighbors` is a table of nodes that must be adjacent to the spawn position
* ex: set to `{"groups:tree"}` to force the mob to spawn next to a tree
* `spawn_on_load` will spawn mobs when a chunk generates if set to true
* `spawn_in_nodes` will spawn mobs inside the node rather than above if set to true
* set this to true for mobs that spawn in water
* `spawn_cap` is the maximum amount of the mob that can spawn within active block range

25
mods/creatura/init.lua Normal file
View file

@ -0,0 +1,25 @@
creatura = {}
local path = minetest.get_modpath("creatura")
dofile(path.."/api.lua")
dofile(path.."/pathfinding.lua")
dofile(path.."/pathfinder_deprecated.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")

677
mods/creatura/methods.lua Normal file
View file

@ -0,0 +1,677 @@
-------------
-- Methods --
-------------
local pi = math.pi
local abs = math.abs
local ceil = math.ceil
local max = math.max
local random = math.random
local atan2 = math.atan2
local sin = math.sin
local cos = math.cos
local function diff(a, b) -- Get difference between 2 angles
return atan2(sin(b - a), 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_add = vector.add
local vec_normal = vector.normalize
local vec_len = vector.length
local vec_dist = vector.distance
local vec_dir = vector.direction
local vec_dot = vector.dot
local vec_multi = vector.multiply
local vec_sub = vector.subtract
local yaw2dir = minetest.yaw_to_dir
local dir2yaw = minetest.dir_to_yaw
--[[local function debugpart(pos, time, tex)
minetest.add_particle({
pos = pos,
texture = tex or "creatura_particle_red.png",
expirationtime = time or 0.1,
glow = 16,
size = 24
})
end]]
---------------------
-- Local Utilities --
---------------------
local get_node_def = creatura.get_node_def
--local get_node_height = creatura.get_node_height_from_def
function creatura.get_collision(self, dir, range)
local pos, yaw = self.object:get_pos(), self.object:get_yaw()
if not pos then return end
local width, height = self.width or 0.5, self.height or 1
dir = dir or yaw2dir(yaw)
pos.x = pos.x + dir.x * width
pos.z = pos.z + dir.z * width
local cos_yaw = cos(yaw)
local sin_yaw = sin(yaw)
local width_i = width / ceil(width)
local height_i = height / ceil(height)
local pos_x, pos_y, pos_z = pos.x, pos.y, pos.z
local dir_x, dir_y, dir_z = dir.x, dir.y, dir.z
local pos2 = {x = pos_x, y = pos_y, z = pos_z}
local collision
pos.y = pos.y + height * 0.5
range = range or 4
local low_score
for _ = 0, range do
if collision then return collision end
pos_x = pos_x + dir_x
pos_y = pos_y + dir_y
pos_z = pos_z + dir_z
pos2.y = pos_y
for x = -width, width, width_i do
pos2.x = cos_yaw * ((pos_x + x) - pos_x) + pos_x
pos2.z = sin_yaw * ((pos_x + x) - pos_x) + pos_z
for y = height, 0, -height_i do
if y < self.stepheight or 1.1 then break end
pos2.y = pos_y + y
if get_node_def(pos2).walkable then
local score = abs(pos.y - pos2.y) * vec_dot(dir, vec_dir(pos, pos2))
if not low_score
or score < low_score then
low_score = score
collision = pos2
end
end
end
end
end
end
creatura.get_collision_ranged = creatura.get_collision
local get_collision = creatura.get_collision
local function get_avoidance_dir(self)
local pos = self.object:get_pos()
if not pos then return end
local _, col_pos = get_collision(self)
if col_pos then
local vel = self.object:get_velocity()
vel.y = 0
local vel_len = vec_len(vel) * (1 + (self.step_delay or 0))
local ahead = vec_add(pos, vec_normal(vel))
local avoidance_force = vec_sub(ahead, col_pos)
avoidance_force.y = 0
avoidance_force = vec_multi(vec_normal(avoidance_force), (vel_len > 1 and vel_len) or 1)
return vec_dir(pos, vec_add(ahead, avoidance_force))
end
end
local function get_collision_single(pos, water)
local pos2 = {x = pos.x, y = pos.y, z = pos.z}
local n_def = get_node_def(pos2)
if n_def.walkable
or (water and (n_def.groups.liquid or 0) > 0) then
pos2.y = pos.y + 1
n_def = get_node_def(pos2)
local col_max = n_def.walkable or (water and (n_def.groups.liquid or 0) > 0)
pos2.y = pos.y - 1
local col_min = col_max and (n_def.walkable or (water and (n_def.groups.liquid or 0) > 0))
if col_min then
return pos
else
pos2.y = pos.y + 1
return pos2
end
end
end
function creatura.get_avoidance_lift(self, pos2, range)
range = ceil(max(range or 1, 0.5))
local height_half = (self.height or 1) * 0.5
local center_y = pos2.y + height_half
local check_pos = {x = pos2.x, y = center_y, z = pos2.z}
-- Find ceiling and floor collisions
local def
local ceil_pos
local floor_pos
for i = 1, range, 0.5 do -- 0.5 increment increases accuracy
if ceil_pos and floor_pos then break end
check_pos.y = center_y + i
def = creatura.get_node_def(check_pos)
if not ceil_pos
and (def.walkable
or minetest.get_item_group(def.name, "liquid") > 0) then
ceil_pos = check_pos
end
check_pos.y = center_y - i
def = creatura.get_node_def(check_pos)
if not floor_pos
and (def.walkable
or minetest.get_item_group(def.name, "liquid") > 0) then
floor_pos = check_pos
end
end
-- Calculate direction to average point of collisions
check_pos.y = center_y
local offset = {x = 0, y = height_half + range, z = 0}
if not ceil_pos then ceil_pos = vec_add(check_pos, offset) end
if not floor_pos then floor_pos = vec_sub(check_pos, offset) end
local dist_up = ceil_pos.y - center_y
local dist_down = floor_pos.y - center_y
local altitude = (dist_up + dist_down) / 2
return ((check_pos.y + altitude) - center_y) / range * 2
end
function creatura.get_avoidance_lift_aquatic(self, pos2, range)
range = ceil(max(range or 1, 0.5))
local height_half = (self.height or 1) * 0.5
local center_y = pos2.y + height_half
local check_pos = {x = pos2.x, y = center_y, z = pos2.z}
-- Find ceiling and floor collisions
local ceil_pos
local floor_pos
for i = 1, range, 0.5 do -- 0.5 increment increases accuracy
if ceil_pos and floor_pos then break end
check_pos.y = center_y + i
if not ceil_pos
and minetest.get_item_group(creatura.get_node_def(check_pos).name, "liquid") < 1 then
ceil_pos = check_pos
end
check_pos.y = center_y - i
if not floor_pos
and minetest.get_item_group(creatura.get_node_def(check_pos).name, "liquid") < 1 then
floor_pos = check_pos
end
end
-- Calculate direction to average point of collisions
check_pos.y = center_y
local offset = {x = 0, y = height_half + range, z = 0}
if not ceil_pos then ceil_pos = vec_add(check_pos, offset) end
if not floor_pos then floor_pos = vec_sub(check_pos, offset) end
local dist_up = ceil_pos.y - center_y
local dist_down = floor_pos.y - center_y
local altitude = (dist_up + dist_down) / 2
return ((check_pos.y + altitude) - center_y) / range * 2
end
----------------------------
-- Context Based Steering --
----------------------------
local steer_directions = {
vec_normal({x = 1, y = 0, z = 0}),
vec_normal({x = 1, y = 0, z = 1}),
vec_normal({x = 0, y = 0, z = 1}),
vec_normal({x = -1, y = 0, z = 0}),
vec_normal({x = -1, y = 0, z = -1}),
vec_normal({x = 0, y = 0, z = -1}),
vec_normal({x = 1, y = 0, z = -1}),
vec_normal({x = -1, y = 0, z = 1})
}
-- Context Methods
function creatura.get_context_default(self, goal, steer_dir, interest, danger, range)
local pos = self.object:get_pos()
if not pos then return end
local width, height = self.width or 0.5, self.height or 1
local y_offset = math.min(self.stepheight or 1.1, height)
pos.y = pos.y + y_offset
local collision
local ray = minetest.raycast(pos, vec_add(pos, vec_multi(steer_dir, width + range)), false, false)
local pointed = ray:next()
if pointed
and pointed.type == "node"
and creatura.get_node_def(pointed.under).walkable then
collision = pointed.under
end
if collision then
local dir2goal = vec_normal(vec_dir(pos, goal))
local dir2col = vec_normal(vec_dir(pos, collision))
local dist2col = vec_dist(pos, collision) - width
local dot_score = vec_dot(dir2col, dir2goal)
local dist_score = (range - dist2col) / range
interest = interest - dot_score
danger = dist_score
end
return interest, danger
end
function creatura.get_context_large(self, goal, steer_dir, interest, danger, range)
local pos = self.object:get_pos()
if not pos then return end
local width, height = self.width or 0.5, self.height or 1
local y_offset = math.min(self.stepheight or height)
pos.y = pos.y + y_offset
local collision = creatura.get_collision(self, steer_dir, range)
if collision then
local dir2goal = vec_normal(vec_dir(pos, goal))
local dir2col = vec_normal(vec_dir(pos, collision))
local dist2col = vec_dist(pos, collision) - width
local dot_score = vec_dot(dir2col, dir2goal)
local dist_score = (range - dist2col) / range
interest = interest - dot_score
danger = dist_score
end
return interest, danger
end
function creatura.get_context_small(self, goal, steer_dir, interest, danger, range)
local pos = self.object:get_pos()
if not pos then return end
pos = vector.round(pos)
local width = self.width or 0.5
local collision = get_collision_single(vec_add(pos, steer_dir))
if collision then
local dir2goal = vec_normal(vec_dir(pos, goal))
local dir2col = vec_normal(vec_dir(pos, collision))
local dist2col = vec_dist(pos, collision) - width
local dot_score = vec_dot(dir2col, dir2goal)
local dist_score = (range - dist2col) / range
interest = interest - dot_score
danger = dist_score
end
return interest, danger
end
function creatura.get_context_small_aquatic(self, goal, steer_dir, interest, danger, range)
local pos = self.object:get_pos()
if not pos then return end
pos = vector.round(pos)
local width = self.width or 0.5
local pos2 = vec_add(pos, steer_dir)
local collision = minetest.get_item_group(get_node_def(pos2).name, "liquid") < 1 and pos2
if collision then
local dir2goal = vec_normal(vec_dir(pos, goal))
local dir2col = vec_normal(vec_dir(pos, collision))
local dist2col = vec_dist(pos, collision) - width
local dot_score = vec_dot(dir2col, dir2goal)
local dist_score = (range - dist2col) / range
interest = interest - dot_score
danger = dist_score
end
return interest, danger
end
-- Calculate Steering
function creatura.calc_steering(self, goal, get_context, range)
if not goal then return end
get_context = get_context or creatura.get_context_default
local pos, yaw = self.object:get_pos(), self.object:get_yaw()
if not pos or not yaw then return end
range = math.max(range or 2, 2)
local dir2goal = vec_normal(vec_dir(pos, goal))
local output_dir = {x = 0, y = dir2goal.y, z = 0}
-- Cached variables
local dir
for _, _dir in ipairs(steer_directions) do
dir = {x = _dir.x, y = dir2goal.y, z = _dir.z}
local score = vec_dot(dir2goal, dir)
local interest = clamp(score, 0, 1)
local danger = 0
if interest > 0 then -- Direction is within 90 degrees of goal
interest, danger = get_context(self, goal, dir, interest, danger, range)
end
score = interest - danger
output_dir = vector.add(output_dir, vector.multiply(dir, score))
end
return vec_normal(output_dir)
end
-- DEPRECATED
function creatura.get_context_steering(self, goal, range, water)
local context = creatura.get_context_default
local width, height = self.width, self.height
if width > 0.5
or height > 1 then
context = creatura.get_context_large
elseif water then
context = creatura.get_context_small_aquatic
end
return creatura.calc_steering(self, goal, context, range)
end
-------------
-- Actions --
-------------
-- Actions are more specific behaviors used
-- to compose a Utility.
-- Move
function creatura.action_move(self, pos2, timeout, method, speed_factor, anim)
local timer = timeout or 4
local function func(_self)
timer = timer - _self.dtime
self:animate(anim or "walk")
local safe = true
if _self.max_fall
and _self.max_fall > 0 then
local pos = self.object:get_pos()
if not pos then return end
safe = _self:is_pos_safe(pos2)
end
if timer <= 0
or not safe
or _self:move_to(pos2, method or "creatura:obstacle_avoidance", speed_factor or 0.5) then
return true
end
end
self:set_action(func)
end
creatura.action_walk = creatura.action_move -- Support for outdated versions
-- 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 rot = self.object:get_rotation()
local function func(_self)
if not init then
_self:animate("stand")
if random(2) < 2 then
dir = -1
end
init = true
end
rot = _self.object:get_rotation()
local goal = (pi * 0.5) * dir
local step = _self.dtime
if step > 0.5 then step = 0.5 end
zrot = zrot + (pi * dir) * step
_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
--[[local function trim_path(pos, path)
if #path < 2 then return end
local trim = false
local closest
for i = #path, 1, -1 do
if not path[i] then break end
if (closest
and vec_dist(pos, path[i]) > vec_dist(pos, path[closest]))
or trim then
table.remove(path, i)
trim = true
else
closest = i
end
end
return path
end]]
creatura.register_movement_method("creatura:pathfind_theta", function(self)
local path = {}
local steer_to
local steer_int = 0
local arrival_threshold = clamp(self.width, 0.5, 1)
self:set_gravity(-9.8)
local function func(_self, goal, speed_factor)
local pos = _self.object:get_pos()
if not pos or not goal then return end
if vec_dist(pos, goal) < arrival_threshold then
_self:halt()
return true
end
-- Calculate Movement
local turn_rate = abs(_self.turn_rate or 5)
local speed = abs(_self.speed or 2) * speed_factor or 0.5
local path_dir = #path > 0 and vec_dir(pos, path[2] or path[1])
steer_int = (steer_int > 0 and steer_int - _self.dtime) or 1 / math.max(speed, 1)
steer_to = path_dir or (steer_int <= 0 and creatura.calc_steering(_self, goal)) or steer_to
path = (#path > 0 and path) or (creatura.pathfinder.find_path_theta(_self, pos, goal) or {})
if path_dir
and ((path[2] and vec_dist(pos, path[2]) < arrival_threshold)
or vec_dist(pos, path[1]) < arrival_threshold) then
table.remove(path, 1)
end
-- Apply Movement
_self:turn_to(dir2yaw(steer_to or vec_dir(pos, goal)), turn_rate)
_self:set_forward_velocity(speed)
end
return func
end)
creatura.register_movement_method("creatura:pathfind", function(self)
local path = {}
local steer_to
local steer_int = 0
local arrival_threshold = clamp(self.width, 0.5, 1)
self:set_gravity(-9.8)
local function func(_self, goal, speed_factor)
local pos = _self.object:get_pos()
if not pos or not goal then return end
if vec_dist(pos, goal) < arrival_threshold then
_self:halt()
return true
end
-- Calculate Movement
local turn_rate = abs(_self.turn_rate or 5)
local speed = abs(_self.speed or 2) * speed_factor or 0.5
local path_dir = #path > 0 and vec_dir(pos, path[2] or path[1])
steer_int = (steer_int > 0 and steer_int - _self.dtime) or 1 / math.max(speed, 1)
steer_to = path_dir or (steer_int <= 0 and creatura.calc_steering(_self, goal)) or steer_to
path = (#path > 0 and path) or (creatura.pathfinder.find_path(_self, pos, goal) or {})
if path_dir
and ((path[2] and vec_dist(pos, path[2]) < arrival_threshold + 0.5)
or vec_dist(pos, path[1]) < arrival_threshold) then
table.remove(path, 1)
end
-- Apply Movement
_self:turn_to(dir2yaw(steer_to or vec_dir(pos, goal)), turn_rate)
_self:set_forward_velocity(speed)
end
return func
end)
-- Steering
creatura.register_movement_method("creatura:steer_small", function(self)
local steer_to
local steer_int = 0
self:set_gravity(-9.8)
local function func(_self, goal, speed_factor)
local pos = _self.object:get_pos()
if not pos or not goal then return end
if vec_dist(pos, goal) < clamp(self.width, 0.5, 1) then
_self:halt()
return true
end
-- Calculate Movement
local turn_rate = abs(_self.turn_rate or 5)
local speed = abs(_self.speed or 2) * speed_factor or 0.5
steer_int = (steer_int > 0 and steer_int - _self.dtime) or 1 / math.max(speed, 1)
steer_to = (steer_int <= 0 and creatura.calc_steering(_self, goal)) or steer_to
-- Apply Movement
_self:turn_to(dir2yaw(steer_to or vec_dir(pos, goal)), turn_rate)
_self:set_forward_velocity(speed)
end
return func
end)
creatura.register_movement_method("creatura:steer_large", function(self)
local steer_to
local steer_int = 0
self:set_gravity(-9.8)
local function func(_self, goal, speed_factor)
local pos = _self.object:get_pos()
if not pos or not goal then return end
if vec_dist(pos, goal) < clamp(self.width, 0.5, 1) then
_self:halt()
return true
end
-- Calculate Movement
local turn_rate = abs(_self.turn_rate or 5)
local speed = abs(_self.speed or 2) * speed_factor or 0.5
steer_int = (steer_int > 0 and steer_int - _self.dtime) or 1 / math.max(speed, 1)
steer_to = (steer_int <= 0 and creatura.calc_steering(_self, goal, creatura.get_context_large)) or steer_to
-- Apply Movement
_self:turn_to(dir2yaw(steer_to or vec_dir(pos, goal)), turn_rate)
_self:set_forward_velocity(speed)
end
return func
end)
creatura.register_movement_method("creatura:walk_simple", function(self)
self:set_gravity(-9.8)
local function func(_self, goal, speed_factor)
local pos = _self.object:get_pos()
if not pos or not goal then return end
if vec_dist(pos, goal) < clamp(self.width, 0.5, 1) then
_self:halt()
return true
end
-- Calculate Movement
local turn_rate = abs(_self.turn_rate or 5)
local speed = abs(_self.speed or 2) * speed_factor or 0.5
-- Apply Movement
_self:turn_to(dir2yaw(vec_dir(pos, goal)), turn_rate)
_self:set_forward_velocity(speed)
end
return func
end)
-- Deprecated
creatura.register_movement_method("creatura:context_based_steering", function(self)
local steer_to
local steer_int = 0
self:set_gravity(-9.8)
local function func(_self, goal, speed_factor)
local pos = _self.object:get_pos()
if not pos or not goal then return end
if vec_dist(pos, goal) < clamp(self.width, 0.5, 1) then
_self:halt()
return true
end
-- Calculate Movement
local turn_rate = abs(_self.turn_rate or 5)
local speed = abs(_self.speed or 2) * speed_factor or 0.5
steer_int = (steer_int > 0 and steer_int - _self.dtime) or 1 / math.max(speed, 1)
steer_to = (steer_int <= 0 and creatura.calc_steering(_self, goal, creatura.get_context_large)) or steer_to
-- Apply Movement
_self:turn_to(dir2yaw(steer_to or vec_dir(pos, goal)), turn_rate)
_self:set_forward_velocity(speed)
end
return func
end)
creatura.register_movement_method("creatura:obstacle_avoidance", function(self)
local box = clamp(self.width, 0.5, 1.5)
local steer_to
local steer_timer = 0.25
local function func(_self, goal, speed_factor)
local pos = _self.object:get_pos()
if not pos then return end
self:set_gravity(-9.8)
-- Return true when goal is reached
if vec_dist(pos, goal) < box * 1.33 then
_self:halt()
return true
end
steer_timer = (steer_timer > 0 and steer_timer - _self.dtime) or 0.25
-- Get movement direction
steer_to = (steer_timer > 0 and steer_to) or (steer_timer <= 0 and get_avoidance_dir(_self))
local goal_dir = steer_to or vec_dir(pos, goal)
pos.y = pos.y + goal_dir.y
local yaw = _self.object:get_yaw()
local goal_yaw = dir2yaw(goal_dir)
local speed = abs(_self.speed or 2) * speed_factor or 0.5
local turn_rate = abs(_self.turn_rate or 5)
-- Movement
local yaw_diff = abs(diff(yaw, goal_yaw))
if yaw_diff < pi * 0.25
or steer_to then
_self:set_forward_velocity(speed)
else
_self:set_forward_velocity(speed * 0.33)
end
_self:turn_to(goal_yaw, turn_rate)
end
return func
end)

1248
mods/creatura/mob_meta.lua Normal file

File diff suppressed because it is too large Load diff

2
mods/creatura/mod.conf Normal file
View file

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

View file

@ -0,0 +1,805 @@
-----------------
-- Pathfinding --
-----------------
local a_star_alloted_time = tonumber(minetest.settings:get("creatura_a_star_alloted_time")) or 500
local theta_star_alloted_time = tonumber(minetest.settings:get("creatura_theta_star_alloted_time")) or 700
local floor = math.floor
local abs = math.abs
local vec_dist, vec_round = vector.distance, vector.round
local moveable = creatura.is_pos_moveable
local function get_distance(start_pos, end_pos)
local distX = abs(start_pos.x - end_pos.x)
local distZ = abs(start_pos.z - end_pos.z)
if distX > distZ then
return 14 * distZ + 10 * (distX - distZ)
else
return 14 * distX + 10 * (distZ - distX)
end
end
local function get_distance_to_neighbor(start_pos, end_pos)
local distX = abs(start_pos.x - end_pos.x)
local distY = abs(start_pos.y - end_pos.y)
local distZ = abs(start_pos.z - end_pos.z)
if distX > distZ then
return (14 * distZ + 10 * (distX - distZ)) * (distY + 1)
else
return (14 * distX + 10 * (distZ - distX)) * (distY + 1)
end
end
local function is_on_ground(pos)
local ground = {
x = pos.x,
y = pos.y - 1,
z = pos.z
}
if creatura.get_node_def(ground).walkable then
return true
end
return false
end
local function vec_raise(v, n)
return {x = v.x, y = v.y + n, z = v.z}
end
local function get_line_of_sight(a, b)
local steps = floor(vec_dist(a, b))
local line = {}
for i = 0, steps do
local pos
if steps > 0 then
pos = {
x = a.x + (b.x - a.x) * (i / steps),
y = a.y + (b.y - a.y) * (i / steps),
z = a.z + (b.z - a.z) * (i / steps)
}
else
pos = a
end
table.insert(line, pos)
end
if #line < 1 then
return false
else
for i = 1, #line do
local node = minetest.get_node(line[i])
if creatura.get_node_def(node.name).walkable then
return false
end
end
end
return true
end
-- Find a path from start to goal
--[[local function debugpart(pos, time, tex)
minetest.add_particle({
pos = pos,
texture = tex or "creatura_particle_red.png",
expirationtime = time or 0.1,
glow = 6,
size = 12
})
end]]
local c_air = minetest.get_content_id("air")
local function is_pos_moveable_vm(pos, width, height, area, data)
pos = vector.round(pos)
local pos1 = {
x = pos.x - math.ceil(width),
y = pos.y,
z = pos.z - math.ceil(width)
}
local pos2 = {
x = pos.x + math.ceil(width),
y = pos.y + math.ceil(height),
z = pos.z + math.ceil(width)
}
for z = pos1.z, pos2.z do
for y = pos1.y, pos2.y do
for x = pos1.x, pos2.x do
if not area:contains(x, y, z) then return false end
local vi = area:index(x, y, z)
local c = data[vi]
if c ~= c_air then
local c_name = minetest.get_name_from_content_id(c)
if creatura.get_node_def(c_name).walkable then
return false
end
end
end
end
end
return true
end
local vm_buffer = {}
function creatura.find_lvm_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim)
climb = climb or false
fly = fly or false
swim = swim or false
if vec_dist(start, goal) > (self.tracking_range or 128) then return {} end
self._path_data.start = start
local path_neighbors = {
{x = 1, y = 0, z = 0},
{x = 1, y = 0, z = 1},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = -1, y = 0, z = -1},
{x = 0, y = 0, z = -1},
{x = 1, y = 0, z = -1}
}
if climb then
table.insert(path_neighbors, {x = 0, y = 1, z = 0})
end
if fly
or swim then
path_neighbors = {
-- Central
{x = 1, y = 0, z = 0},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = 0, y = 0, z = -1},
-- Directly Up or Down
{x = 0, y = 1, z = 0},
{x = 0, y = -1, z = 0}
}
end
local function get_neighbors(pos, width, height, tbl, open, closed, vm_area, vm_data)
local result = {}
for i = 1, #tbl do
local neighbor = vector.add(pos, tbl[i])
if not vm_area or not vm_data or not vm_area:containsp(neighbor) then return end
local can_move = (not swim and get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)) or true
if open[minetest.hash_node_position(neighbor)]
or closed[minetest.hash_node_position(neighbor)] then
can_move = false
end
if can_move then
can_move = is_pos_moveable_vm(neighbor, width, height, vm_area, vm_data)
if not fly and not swim then
if not can_move then -- Step Up
local step = vec_raise(neighbor, 1)
can_move = is_pos_moveable_vm(vec_round(step), width, height, vm_area, vm_data)
neighbor = vec_round(step)
else
local step = creatura.get_ground_level(vector.new(neighbor), 1)
if step.y < neighbor.y
and is_pos_moveable_vm(vec_round(step), width, height, vm_area, vm_data) then
neighbor = step
end
end
end
end
if vector.equals(neighbor, goal) then
can_move = true
end
if can_move
and (not swim
or creatura.get_node_def(neighbor).drawtype == "liquid") then
table.insert(result, neighbor)
end
end
return result
end
local function find_path(_start, _goal)
local us_time = minetest.get_us_time()
_start = {
x = floor(_start.x + 0.5),
y = floor(_start.y + 0.5),
z = floor(_start.z + 0.5)
}
_goal = {
x = floor(_goal.x + 0.5),
y = floor(_goal.y + 0.5),
z = floor(_goal.z + 0.5)
}
if _goal.x == _start.x
and _goal.z == _start.z then -- No path can be found
return nil
end
local vm_area = self._path_data.vm_area
local vm_data = self._path_data.vm_data
if not vm_area
or not vm_data then
local vm_center = vector.add(_start, vector.divide(vector.subtract(_goal, _start), 2))
local vm_size = vec_dist(_goal, _start)
if vm_size < 24 then vm_size = 24 end
local e1 = vector.subtract(vm_center, vm_size)
local e2 = vector.add(vm_center, vm_size)
local vm = minetest.get_voxel_manip(e1, e2)
e1, e2 = vm:read_from_map(e1, e2)
vm_area = VoxelArea:new{MinEdge=e1, MaxEdge=e2}
vm_data = vm:get_data(vm_buffer)
end
local openSet = self._path_data.open or {}
local closedSet = self._path_data.closed or {}
local start_index = minetest.hash_node_position(_start)
openSet[start_index] = {
pos = _start,
parent = nil,
gScore = 0,
fScore = get_distance(_start, _goal)
}
local count = self._path_data.count or 1
while count > 0 do
-- Initialize ID and data
local current_id, current = next(openSet)
-- Find lowest f cost
for i, v in pairs(openSet) do
if v.fScore < current.fScore then
current_id = i
current = v
end
end
-- Add lowest fScore to closedSet and remove from openSet
openSet[current_id] = nil
closedSet[current_id] = current
self._path_data.open = openSet
self._path_data.closedSet = closedSet
local current_start = vec_round(self._path_data.start)
if closedSet[minetest.hash_node_position(current_start)] then
start_index = minetest.hash_node_position(current_start)
end
-- Reconstruct path if end is reached
if ((is_on_ground(_goal)
or fly)
and current_id == minetest.hash_node_position(_goal))
or (not fly
and not is_on_ground(_goal)
and math.abs(_goal.x - current.pos.x) < 1.1
and math.abs(_goal.z - current.pos.z) < 1.1) then
local path = {}
local fail_safe = 0
for _ in pairs(closedSet) do
fail_safe = fail_safe + 1
end
repeat
if not closedSet[current_id] then return end
table.insert(path, closedSet[current_id].pos)
current_id = closedSet[current_id].parent
until current_id == start_index or #path >= fail_safe
if not closedSet[current_id] then self._path_data = {} return nil end
table.insert(path, closedSet[current_id].pos)
local reverse_path = {}
repeat table.insert(reverse_path, table.remove(path)) until #path == 0
self._path_data = {}
return reverse_path
end
count = count - 1
local adjacent = get_neighbors(
current.pos,
obj_width,
obj_height,
path_neighbors,
openSet,
closedSet,
vm_area,
vm_data
)
-- Go through neighboring nodes
if not adjacent or #adjacent < 1 then self._path_data = {} return {} end
for i = 1, #adjacent do
local neighbor = {
pos = adjacent[i],
parent = current_id,
gScore = 0,
fScore = 0
}
local temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos)
local new_gScore = 0
if openSet[minetest.hash_node_position(neighbor.pos)] then
new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore
end
if (temp_gScore < new_gScore
or not openSet[minetest.hash_node_position(neighbor.pos)])
and not closedSet[minetest.hash_node_position(neighbor.pos)] then
if not openSet[minetest.hash_node_position(neighbor.pos)] then
count = count + 1
end
local hCost = get_distance_to_neighbor(neighbor.pos, _goal)
neighbor.gScore = temp_gScore
neighbor.fScore = temp_gScore + hCost
openSet[minetest.hash_node_position(neighbor.pos)] = neighbor
end
end
if minetest.get_us_time() - us_time > a_star_alloted_time then
self._path_data = {
start = _start,
open = openSet,
closed = closedSet,
count = count,
vm_area = vm_area,
vm_data = vm_data
}
return {}
end
if count > (max_open or 100) then
self._path_data = {}
return
end
end
self._path_data = {}
return nil
end
return find_path(start, goal)
end
function creatura.find_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim)
climb = climb or false
fly = fly or false
swim = swim or false
start = self._path_data.start or start
self._path_data.start = start
local path_neighbors = {
{x = 1, y = 0, z = 0},
{x = 1, y = 0, z = 1},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = -1, y = 0, z = -1},
{x = 0, y = 0, z = -1},
{x = 1, y = 0, z = -1}
}
if climb then
table.insert(path_neighbors, {x = 0, y = 1, z = 0})
end
if fly
or swim then
path_neighbors = {
-- Central
{x = 1, y = 0, z = 0},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = 0, y = 0, z = -1},
-- Directly Up or Down
{x = 0, y = 1, z = 0},
{x = 0, y = -1, z = 0}
}
end
local function get_neighbors(pos, width, height, tbl, open, closed)
local result = {}
for i = 1, #tbl do
local neighbor = vector.add(pos, tbl[i])
local can_move = (not swim and get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)) or true
if open[minetest.hash_node_position(neighbor)]
or closed[minetest.hash_node_position(neighbor)] then
can_move = false
end
if can_move then
can_move = moveable(neighbor, width, height)
if not fly and not swim then
if not can_move then -- Step Up
local step = vec_raise(neighbor, 1)
can_move = moveable(vec_round(step), width, height)
neighbor = vec_round(step)
else
local step = creatura.get_ground_level(vector.new(neighbor), 1)
if step.y < neighbor.y
and moveable(vec_round(step), width, height) then
neighbor = step
end
end
end
end
if vector.equals(neighbor, goal) then
can_move = true
end
if can_move
and (not swim
or creatura.get_node_def(neighbor).drawtype == "liquid") then
table.insert(result, neighbor)
end
end
return result
end
local function find_path(_start, _goal)
local us_time = minetest.get_us_time()
_start = {
x = floor(_start.x + 0.5),
y = floor(_start.y + 0.5),
z = floor(_start.z + 0.5)
}
_goal = {
x = floor(_goal.x + 0.5),
y = floor(_goal.y + 0.5),
z = floor(_goal.z + 0.5)
}
if _goal.x == _start.x
and _goal.z == _start.z then -- No path can be found
return nil
end
local openSet = self._path_data.open or {}
local closedSet = self._path_data.closed or {}
local start_index = minetest.hash_node_position(_start)
openSet[start_index] = {
pos = _start,
parent = nil,
gScore = 0,
fScore = get_distance(_start, _goal)
}
local count = self._path_data.count or 1
while count > 0 do
if minetest.get_us_time() - us_time > a_star_alloted_time then
self._path_data = {
start = _start,
open = openSet,
closed = closedSet,
count = count
}
return
end
-- Initialize ID and data
local current_id, current = next(openSet)
-- Find lowest f cost
for i, v in pairs(openSet) do
if v.fScore < current.fScore then
current_id = i
current = v
end
end
-- Add lowest fScore to closedSet and remove from openSet
openSet[current_id] = nil
closedSet[current_id] = current
self._path_data.open = openSet
self._path_data.closedSet = closedSet
-- Reconstruct path if end is reached
if ((is_on_ground(_goal)
or fly)
and current_id == minetest.hash_node_position(_goal))
or (not fly
and not is_on_ground(_goal)
and _goal.x == current.pos.x
and _goal.z == current.pos.z) then
local path = {}
local fail_safe = 0
for _ in pairs(closedSet) do
fail_safe = fail_safe + 1
end
repeat
if not closedSet[current_id] then return end
table.insert(path, closedSet[current_id].pos)
current_id = closedSet[current_id].parent
until current_id == start_index or #path >= fail_safe
if not closedSet[current_id] then self._path_data = {} return nil end
table.insert(path, closedSet[current_id].pos)
local reverse_path = {}
repeat table.insert(reverse_path, table.remove(path)) until #path == 0
self._path_data = {}
return reverse_path
end
count = count - 1
local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet)
-- Go through neighboring nodes
for i = 1, #adjacent do
local neighbor = {
pos = adjacent[i],
parent = current_id,
gScore = 0,
fScore = 0
}
local temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos)
local new_gScore = 0
if openSet[minetest.hash_node_position(neighbor.pos)] then
new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore
end
if (temp_gScore < new_gScore
or not openSet[minetest.hash_node_position(neighbor.pos)])
and not closedSet[minetest.hash_node_position(neighbor.pos)] then
if not openSet[minetest.hash_node_position(neighbor.pos)] then
count = count + 1
end
local hCost = get_distance_to_neighbor(neighbor.pos, _goal)
neighbor.gScore = temp_gScore
neighbor.fScore = temp_gScore + hCost
openSet[minetest.hash_node_position(neighbor.pos)] = neighbor
end
end
if count > (max_open or 100) then
self._path_data = {}
return
end
end
self._path_data = {}
return nil
end
return find_path(start, goal)
end
------------
-- Theta* --
------------
function creatura.find_theta_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim)
climb = climb or false
fly = fly or false
swim = swim or false
start = self._path_data.start or start
self._path_data.start = start
local path_neighbors = {
{x = 1, y = 0, z = 0},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = 0, y = 0, z = -1},
}
if climb then
table.insert(path_neighbors, {x = 0, y = 1, z = 0})
end
if fly
or swim then
path_neighbors = {
-- Central
{x = 1, y = 0, z = 0},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = 0, y = 0, z = -1},
-- Directly Up or Down
{x = 0, y = 1, z = 0},
{x = 0, y = -1, z = 0}
}
end
local function get_neighbors(pos, width, height, tbl, open, closed)
local result = {}
for i = 1, #tbl do
local neighbor = vector.add(pos, tbl[i])
if neighbor.y == pos.y
and not fly
and not swim then
neighbor = creatura.get_ground_level(neighbor, 1)
end
local can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)
if swim then
can_move = true
end
if not moveable(vec_raise(neighbor, -0.49), width, height) then
can_move = false
if neighbor.y == pos.y
and moveable(vec_raise(neighbor, 0.51), width, height) then
neighbor = vec_raise(neighbor, 1)
can_move = true
end
end
if vector.equals(neighbor, goal) then
can_move = true
end
if open[minetest.hash_node_position(neighbor)]
or closed[minetest.hash_node_position(neighbor)] then
can_move = false
end
if can_move
and ((is_on_ground(neighbor)
or (fly or swim))
or (neighbor.x == pos.x
and neighbor.z == pos.z
and climb))
and (not swim
or creatura.get_node_def(neighbor).drawtype == "liquid") then
table.insert(result, neighbor)
end
end
return result
end
local function find_path(_start, _goal)
local us_time = minetest.get_us_time()
_start = {
x = floor(_start.x + 0.5),
y = floor(_start.y + 0.5),
z = floor(_start.z + 0.5)
}
_goal = {
x = floor(_goal.x + 0.5),
y = floor(_goal.y + 0.5),
z = floor(_goal.z + 0.5)
}
if _goal.x == _start.x
and _goal.z == _start.z then -- No path can be found
return nil
end
local openSet = self._path_data.open or {}
local closedSet = self._path_data.closed or {}
local start_index = minetest.hash_node_position(_start)
openSet[start_index] = {
pos = _start,
parent = nil,
gScore = 0,
fScore = get_distance(_start, _goal)
}
local count = self._path_data.count or 1
while count > 0 do
if minetest.get_us_time() - us_time > theta_star_alloted_time then
self._path_data = {
start = _start,
open = openSet,
closed = closedSet,
count = count
}
return
end
-- Initialize ID and data
local current_id, current = next(openSet)
-- Find lowest f cost
for i, v in pairs(openSet) do
if v.fScore < current.fScore then
current_id = i
current = v
end
end
-- Add lowest fScore to closedSet and remove from openSet
openSet[current_id] = nil
closedSet[current_id] = current
-- Reconstruct path if end is reached
if (is_on_ground(_goal)
and current_id == minetest.hash_node_position(_goal))
or (not is_on_ground(_goal)
and _goal.x == current.pos.x
and _goal.z == current.pos.z) then
local path = {}
local fail_safe = 0
for _ in pairs(closedSet) do
fail_safe = fail_safe + 1
end
repeat
if not closedSet[current_id] then return end
table.insert(path, closedSet[current_id].pos)
current_id = closedSet[current_id].parent
until current_id == start_index or #path >= fail_safe
if not closedSet[current_id] then self._path_data = {} return nil end
table.insert(path, closedSet[current_id].pos)
local reverse_path = {}
repeat table.insert(reverse_path, table.remove(path)) until #path == 0
self._path_data = {}
return reverse_path
end
count = count - 1
local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet)
-- Go through neighboring nodes
for i = 1, #adjacent do
local neighbor = {
pos = adjacent[i],
parent = current_id,
gScore = 0,
fScore = 0
}
if not openSet[minetest.hash_node_position(neighbor.pos)]
and not closedSet[minetest.hash_node_position(neighbor.pos)] then
local current_parent = closedSet[current.parent] or closedSet[start_index]
if not current_parent then
current_parent = openSet[current.parent] or openSet[start_index]
end
if current_parent
and get_line_of_sight(current_parent.pos, neighbor.pos) then
local temp_gScore = current_parent.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos)
local new_gScore = 999
if openSet[minetest.hash_node_position(neighbor.pos)] then
new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore
end
if temp_gScore < new_gScore then
local hCost = get_distance_to_neighbor(neighbor.pos, _goal)
neighbor.gScore = temp_gScore
neighbor.fScore = temp_gScore + hCost
neighbor.parent = minetest.hash_node_position(current_parent.pos)
if openSet[minetest.hash_node_position(neighbor.pos)] then
openSet[minetest.hash_node_position(neighbor.pos)] = nil
end
openSet[minetest.hash_node_position(neighbor.pos)] = neighbor
count = count + 1
end
else
local temp_gScore = current.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos)
local new_gScore = 999
if openSet[minetest.hash_node_position(neighbor.pos)] then
new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore
end
if temp_gScore < new_gScore then
local hCost = get_distance_to_neighbor(neighbor.pos, _goal)
neighbor.gScore = temp_gScore
neighbor.fScore = temp_gScore + hCost
if openSet[minetest.hash_node_position(neighbor.pos)] then
openSet[minetest.hash_node_position(neighbor.pos)] = nil
end
openSet[minetest.hash_node_position(neighbor.pos)] = neighbor
count = count + 1
end
end
end
end
if count > (max_open or 100) then
self._path_data = {}
return
end
end
self._path_data = {}
return nil
end
return find_path(start, goal)
end

View file

@ -0,0 +1,627 @@
-----------------
-- Pathfinding --
-----------------
local a_star_alloted_time = tonumber(minetest.settings:get("creatura_a_star_alloted_time")) or 500
local theta_star_alloted_time = tonumber(minetest.settings:get("creatura_theta_star_alloted_time")) or 700
creatura.pathfinder = {}
local max_open = 300
-- Math
local floor = math.floor
local abs = math.abs
local vec_add, vec_dist, vec_new, vec_round = vector.add, vector.distance, vector.new, vector.round
local function vec_raise(v, n)
return {x = v.x, y = v.y + n, z = v.z}
end
-- Heuristic
local function get_distance(start_pos, end_pos)
local distX = abs(start_pos.x - end_pos.x)
local distZ = abs(start_pos.z - end_pos.z)
if distX > distZ then
return 14 * distZ + 10 * (distX - distZ)
else
return 14 * distX + 10 * (distZ - distX)
end
end
local function get_distance_to_neighbor(start_pos, end_pos)
local distX = abs(start_pos.x - end_pos.x)
local distY = abs(start_pos.y - end_pos.y)
local distZ = abs(start_pos.z - end_pos.z)
if distX > distZ then
return (14 * distZ + 10 * (distX - distZ)) * (distY + 1)
else
return (14 * distX + 10 * (distZ - distX)) * (distY + 1)
end
end
-- Blocked Movement Checks
local is_blocked = creatura.is_blocked
local function get_line_of_sight(a, b)
local steps = floor(vec_dist(a, b))
local line = {}
for i = 0, steps do
local pos
if steps > 0 then
pos = {
x = a.x + (b.x - a.x) * (i / steps),
y = a.y + (b.y - a.y) * (i / steps),
z = a.z + (b.z - a.z) * (i / steps)
}
else
pos = a
end
table.insert(line, pos)
end
if #line < 1 then
return false
else
for i = 1, #line do
local node = minetest.get_node(line[i])
if creatura.get_node_def(node.name).walkable then
return false
end
end
end
return true
end
local function is_on_ground(pos)
local ground = {
x = pos.x,
y = pos.y - 1,
z = pos.z
}
if creatura.get_node_def(ground).walkable then
return true
end
return false
end
-- Neighbor Check Grids
local neighbor_grid = {
{x = 1, y = 0, z = 0},
{x = 1, y = 0, z = 1},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = -1, y = 0, z = -1},
{x = 0, y = 0, z = -1},
{x = 1, y = 0, z = -1}
}
local neighbor_grid_climb = {
{x = 1, y = 0, z = 0},
{x = 1, y = 0, z = 1},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = -1, y = 0, z = -1},
{x = 0, y = 0, z = -1},
{x = 1, y = 0, z = -1},
{x = 0, y = 1, z = 0},
{x = 0, y = -1, z = 0}
}
local neighbor_grid_3d = {
-- Central
{x = 1, y = 0, z = 0},
{x = 0, y = 0, z = 1},
{x = -1, y = 0, z = 0},
{x = 0, y = 0, z = -1},
-- Directly Up or Down
{x = 0, y = 1, z = 0},
{x = 0, y = -1, z = 0}
}
-- Get Neighbors
local function get_neighbors(pos, width, height, open, closed, parent, evaluated)
local result = {}
local neighbor
local can_move
local hashed_pos
local step
for i = 1, #neighbor_grid do
neighbor = vec_add(pos, neighbor_grid[i])
can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)
hashed_pos = minetest.hash_node_position(neighbor)
if parent
and vec_dist(parent, neighbor) < vec_dist(pos, neighbor) then
can_move = false
end
if open[hashed_pos]
or closed[hashed_pos]
or evaluated[hashed_pos] then
can_move = false
elseif can_move then
can_move = not is_blocked(neighbor, width, height)
if not can_move then -- Step Up
step = vec_raise(neighbor, 1)
can_move = not is_blocked(vec_round(step), width, height)
neighbor = vec_round(step)
else
step = creatura.get_ground_level(vec_new(neighbor), 1)
if step.y < neighbor.y
and not is_blocked(vec_round(step), width, height) then
neighbor = step
end
end
end
if can_move then
table.insert(result, neighbor)
end
evaluated[hashed_pos] = true
end
return result
end
function creatura.pathfinder.get_neighbors_climb(pos, width, height, open, closed)
local result = {}
local neighbor
local can_move
local hashed_pos
local step
for i = 1, #neighbor_grid_climb do
neighbor = vec_add(pos, neighbor_grid_climb[i])
can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)
hashed_pos = minetest.hash_node_position(neighbor)
if open[hashed_pos]
or closed[hashed_pos] then
can_move = false
elseif can_move then
can_move = not is_blocked(neighbor, width, height)
if not can_move then -- Step Up
step = vec_raise(neighbor, 1)
can_move = not is_blocked(vec_round(step), width, height)
neighbor = vec_round(step)
elseif i < 9 then
step = creatura.get_ground_level(vec_new(neighbor), 1)
if step.y < neighbor.y
and not is_blocked(vec_round(step), width, height) then
neighbor = step
end
end
end
if can_move then
table.insert(result, neighbor)
end
end
return result
end
function creatura.pathfinder.get_neighbors_fly(pos, width, height, open, closed, parent)
local result = {}
local neighbor
local can_move
local hashed_pos
for i = 1, #neighbor_grid_3d do
neighbor = vec_add(pos, neighbor_grid_3d[i])
can_move = get_line_of_sight({x = pos.x, y = pos.y, z = pos.z}, neighbor)
hashed_pos = minetest.hash_node_position(neighbor)
if parent
and vec_dist(parent, neighbor) < vec_dist(pos, neighbor) then
can_move = false
end
if open[hashed_pos]
or closed[hashed_pos] then
can_move = false
elseif can_move then
can_move = not is_blocked(neighbor, width, height)
end
if can_move then
table.insert(result, neighbor)
end
end
return result, true
end
function creatura.pathfinder.get_neighbors_swim(pos, width, height, open, closed, parent)
local result = {}
local neighbor
local can_move
local hashed_pos
for i = 1, #neighbor_grid_3d do
neighbor = vec_add(pos, neighbor_grid_3d[i])
can_move = get_line_of_sight({x = pos.x, y = pos.y, z = pos.z}, neighbor)
hashed_pos = minetest.hash_node_position(neighbor)
if (parent
and vec_dist(parent, neighbor) < vec_dist(pos, neighbor))
or creatura.get_node_def(neighbor).drawtype ~= "liquid" then
can_move = false
end
if open[hashed_pos]
or closed[hashed_pos] then
can_move = false
elseif can_move then
can_move = not is_blocked(neighbor, width, height)
end
if can_move then
table.insert(result, neighbor)
end
end
return result, true
end
-- A*
function creatura.pathfinder.find_path(self, pos1, pos2, neighbor_func)
local us_time = minetest.get_us_time()
local check_vertical = false
neighbor_func = neighbor_func or get_neighbors
local start = self._path_data.start or {
x = floor(pos1.x + 0.5),
y = floor(pos1.y + 0.5),
z = floor(pos1.z + 0.5)
}
local goal = {
x = floor(pos2.x + 0.5),
y = floor(pos2.y + 0.5),
z = floor(pos2.z + 0.5)
}
self._path_data.start = start
if goal.x == start.x
and goal.z == start.z then -- No path can be found
self._path_data = {}
return
end
local openSet = self._path_data.open or {}
local closedSet = self._path_data.closed or {}
local evaluated = {}
local start_index = minetest.hash_node_position(start)
openSet[start_index] = {
pos = start,
parent = nil,
gScore = 0,
fScore = get_distance(start, goal)
}
local count = self._path_data.count or 1
local current_id, current
local adjacent
local neighbor
local temp_gScore
local new_gScore
local hCost
local hashed_pos
local parent_open
local parent_closed
while count > 0 do
-- Initialize ID and data
current_id, current = next(openSet)
-- Find lowest f cost
for i, v in pairs(openSet) do
if v.fScore < current.fScore then
current_id = i
current = v
end
end
if not current_id then self._path_data = {} return end -- failsafe
-- Add lowest fScore to closedSet and remove from openSet
openSet[current_id] = nil
closedSet[current_id] = current
if ((check_vertical or is_on_ground(goal))
and current_id == minetest.hash_node_position(goal))
or ((not check_vertical and not is_on_ground(goal))
and goal.x == current.pos.x
and goal.z == current.pos.z) then
local path = {}
local fail_safe = 0
for _ in pairs(closedSet) do
fail_safe = fail_safe + 1
end
repeat
if not closedSet[current_id] then self._path_data = {} return end
table.insert(path, closedSet[current_id].pos)
current_id = closedSet[current_id].parent
until current_id == start_index or #path >= fail_safe
if not closedSet[current_id] then self._path_data = {} return nil end
table.insert(path, closedSet[current_id].pos)
local reverse_path = {}
repeat table.insert(reverse_path, table.remove(path)) until #path == 0
self._path_data = {}
return reverse_path
end
parent_open = openSet[current.parent]
parent_closed = closedSet[current.parent]
adjacent, check_vertical = neighbor_func(
current.pos,
self.width,
self.height,
openSet,
closedSet,
(parent_closed and parent_closed.pos) or (parent_open and parent_open.pos),
evaluated
)
-- Fly, Swim, and Climb all return true for check_vertical to properly check if goal has been reached
-- Go through neighboring nodes
for i = 1, #adjacent do
neighbor = {
pos = adjacent[i],
parent = current_id,
gScore = 0,
fScore = 0
}
temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos)
new_gScore = 0
hashed_pos = minetest.hash_node_position(neighbor.pos)
if openSet[hashed_pos] then
new_gScore = openSet[hashed_pos].gScore
end
if (temp_gScore < new_gScore
or not openSet[hashed_pos])
and not closedSet[hashed_pos] then
if not openSet[hashed_pos] then
count = count + 1
end
hCost = get_distance_to_neighbor(neighbor.pos, goal)
neighbor.gScore = temp_gScore
neighbor.fScore = temp_gScore + hCost
openSet[hashed_pos] = neighbor
end
end
if minetest.get_us_time() - us_time > a_star_alloted_time then
self._path_data = {
start = start,
open = openSet,
closed = closedSet,
count = count
}
return
end
if count > (max_open or 100) then
self._path_data = {}
return
end
end
end
-- Theta*
function creatura.pathfinder.find_path_theta(self, pos1, pos2, neighbor_func)
local us_time = minetest.get_us_time()
local check_vertical = false
neighbor_func = neighbor_func or get_neighbors
local start = self._path_data.start or {
x = floor(pos1.x + 0.5),
y = floor(pos1.y + 0.5),
z = floor(pos1.z + 0.5)
}
local goal = {
x = floor(pos2.x + 0.5),
y = floor(pos2.y + 0.5),
z = floor(pos2.z + 0.5)
}
self._path_data.start = start
if goal.x == start.x
and goal.z == start.z then -- No path can be found
return
end
local openSet = self._path_data.open or {}
local closedSet = self._path_data.closed or {}
local evaluated = {}
local start_index = minetest.hash_node_position(start)
openSet[start_index] = {
pos = start,
parent = nil,
gScore = 0,
fScore = get_distance(start, goal)
}
local count = self._path_data.count or 1
local current_id, current
local current_parent
local adjacent
local neighbor
local temp_gScore
local new_gScore
local hCost
local hashed_pos
local parent_open
local parent_closed
while count > 0 do
-- Initialize ID and data
current_id, current = next(openSet)
-- Find lowest f cost
for i, v in pairs(openSet) do
if v.fScore < current.fScore then
current_id = i
current = v
end
end
if not current_id then return end -- failsafe
-- Add lowest fScore to closedSet and remove from openSet
openSet[current_id] = nil
closedSet[current_id] = current
if ((check_vertical or is_on_ground(goal))
and current_id == minetest.hash_node_position(goal))
or ((not check_vertical and not is_on_ground(goal))
and goal.x == current.pos.x
and goal.z == current.pos.z) then
local path = {}
local fail_safe = 0
for _ in pairs(closedSet) do
fail_safe = fail_safe + 1
end
repeat
if not closedSet[current_id] then return end
table.insert(path, closedSet[current_id].pos)
current_id = closedSet[current_id].parent
until current_id == start_index or #path >= fail_safe
if not closedSet[current_id] then self._path_data = {} return nil end
table.insert(path, closedSet[current_id].pos)
local reverse_path = {}
repeat table.insert(reverse_path, table.remove(path)) until #path == 0
self._path_data = {}
return reverse_path
end
parent_open = openSet[current.parent]
parent_closed = closedSet[current.parent]
adjacent, check_vertical = neighbor_func(
current.pos,
self.width,
self.height,
openSet,
closedSet,
(parent_closed and parent_closed.pos) or (parent_open and parent_open.pos),
evaluated
)
-- Fly, Swim, and Climb all return true for check_vertical to properly check if goal has been reached
-- Go through neighboring nodes
for i = 1, #adjacent do
neighbor = {
pos = adjacent[i],
parent = current_id,
gScore = 0,
fScore = 0
}
hashed_pos = minetest.hash_node_position(neighbor.pos)
if not openSet[hashed_pos]
and not closedSet[hashed_pos] then
current_parent = closedSet[current.parent] or closedSet[start_index]
if not current_parent then
current_parent = openSet[current.parent] or openSet[start_index]
end
if current_parent
and get_line_of_sight(current_parent.pos, neighbor.pos) then
temp_gScore = current_parent.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos)
new_gScore = 999
if openSet[hashed_pos] then
new_gScore = openSet[hashed_pos].gScore
end
if temp_gScore < new_gScore then
hCost = get_distance_to_neighbor(neighbor.pos, goal)
neighbor.gScore = temp_gScore
neighbor.fScore = temp_gScore + hCost
neighbor.parent = minetest.hash_node_position(current_parent.pos)
openSet[hashed_pos] = neighbor
count = count + 1
end
else
temp_gScore = current.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos)
new_gScore = 999
if openSet[hashed_pos] then
new_gScore = openSet[hashed_pos].gScore
end
if temp_gScore < new_gScore then
hCost = get_distance_to_neighbor(neighbor.pos, goal)
neighbor.gScore = temp_gScore
neighbor.fScore = temp_gScore + hCost
openSet[hashed_pos] = neighbor
count = count + 1
end
end
end
end
if minetest.get_us_time() - us_time > theta_star_alloted_time then
self._path_data = {
start = start,
open = openSet,
closed = closedSet,
count = count
}
return
end
if count > (max_open or 100) then
self._path_data = {}
return
end
end
end

View file

@ -0,0 +1,32 @@
# 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) int 10
# Allows Mobs to spawn during chunk generation (If dependent mods use spawn_on_gen)
creatura_mapgen_spawning (Mapgen Spawning) bool true
# How many chunks are generated before a Mob can spawn
creatura_mapgen_spawn_interval (Mapgen Spawning Interval) int 64
# How many Mobs can be a in a Mapblock before ABM spawning is blocked
creatura_mapblock_limit (Max Mobs per Mapblock) int 12
# How many Mobs can be within Active Block Send Range of an attempted spawn before stopping attempt
creatura_abr_limit (Max Mobs within ABR) int 24
# Minimum distance to a player for ABM Spawning
creatura_min_abm_dist (Minimum ABM Spawning Distance) int 32
# Allows Mobs to spawn in protected areas
creatura_protected_spawn (Protected Area Spawning) bool true
# 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) int 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) int 700

Binary file not shown.

Binary file not shown.

Binary file not shown.

573
mods/creatura/spawning.lua Normal file
View file

@ -0,0 +1,573 @@
--------------
-- Spawning --
--------------
creatura.registered_mob_spawns = {}
creatura.registered_on_spawns = {}
-- Math --
local abs = math.abs
local ceil = math.ceil
local pi = math.pi
local random = math.random
local min = math.min
local vec_add, vec_dist, vec_sub = vector.add, vector.distance, vector.subtract
-- Utility Functions --
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
local function table_contains(tbl, val)
for _, v in pairs(tbl) do
if v == val then
return true
end
end
return false
end
local function pos_meets_params(pos, def)
if not minetest.find_nodes_in_area(pos, pos, def.nodes) then return false end
if not minetest.find_node_near(pos, 1, def.neighbors) then return false end
return true
end
local function can_spawn(pos, width, height)
local pos2
local w_iter = width / ceil(width)
for y = 0, height, height / ceil(height) do
for z = -width, width, w_iter do
for x = -width, width, w_iter do
pos2 = {x = pos.x + x, y = pos.y + y, z = pos.z + z}
local def = creatura.get_node_def(pos2)
if def.walkable then return false end
end
end
end
return true
end
local function do_on_spawn(pos, obj)
local name = obj and obj:get_luaentity().name
if not name then return end
local spawn_functions = creatura.registered_on_spawns[name] or {}
if #spawn_functions > 0 then
for _, func in ipairs(spawn_functions) do
func(obj:get_luaentity(), pos)
if not obj:get_yaw() then break end
end
end
end
----------------
-- Spawn Item --
----------------
local creative = minetest.settings:get_bool("creative_mode")
function creatura.register_spawn_item(name, def)
local inventory_image
if not def.inventory_image
and ((def.col1 and def.col2)
or (def.hex_primary and def.hex_secondary)) then
local primary = def.col1 or def.hex_primary
local secondary = def.col2 or def.hex_secondary
local base = "(creatura_spawning_crystal_primary.png^[multiply:#" .. primary .. ")"
local spots = "(creatura_spawning_crystal_secondary.png^[multiply:#" .. secondary .. ")"
inventory_image = base .. "^" .. spots
end
local mod_name = name:split(":")[1]
local mob_name = name:split(":")[2]
def.description = def.description or "Spawn " .. format_name(name)
def.inventory_image = def.inventory_image or inventory_image
def.on_place = function(itemstack, player, pointed_thing)
-- If the player right-clicks something like a chest or item frame then
-- run the node's on_rightclick callback
local under = pointed_thing.under
local node = minetest.get_node(under)
local node_def = minetest.registered_nodes[node.name]
if node_def and node_def.on_rightclick and
not (player and player:is_player() and
player:get_player_control().sneak) then
return node_def.on_rightclick(under, node, player, itemstack,
pointed_thing) or itemstack
end
-- Otherwise spawn the mob
local pos = minetest.get_pointed_thing_position(pointed_thing, true)
if minetest.is_protected(pos, player and player:get_player_name() or "") then return end
local mobdef = minetest.registered_entities[name]
local spawn_offset = abs(mobdef.collisionbox[2])
pos.y = (pos.y - 0.49) + spawn_offset
if def.antispam then
local objs = minetest.get_objects_in_area(vec_sub(pos, 0.51), vec_add(pos, 0.51))
for _, obj in ipairs(objs) do
if obj
and obj:get_luaentity()
and obj:get_luaentity().name == name then
return
end
end
end
local object = minetest.add_entity(pos, name)
if object then
object:set_yaw(random(0, pi * 2))
object:get_luaentity().last_yaw = object:get_yaw()
if def.on_spawn then
def.on_spawn(object:get_luaentity(), player)
end
end
if not minetest.is_creative_enabled(player:get_player_name())
or def.consume_in_creative then
itemstack:take_item()
return itemstack
end
end
minetest.register_craftitem(def.itemstring or (mod_name .. ":spawn_" .. mob_name), def)
end
function creatura.register_on_spawn(name, func)
if not creatura.registered_on_spawns[name] then
creatura.registered_on_spawns[name] = {}
end
table.insert(creatura.registered_on_spawns[name], func)
end
--------------
-- Spawning --
--------------
--[[creatura.register_abm_spawn("mymod:mymob", {
chance = 3000,
interval = 30,
min_height = 0,
max_height = 128,
min_light = 1,
max_light = 15,
min_group = 1,
max_group = 4,
nodes = {"group:soil", "group:stone"},
neighbors = {"air"},
spawn_on_load = false,
spawn_in_nodes = false,
spawn_cap = 5
})]]
local protected_spawn = minetest.settings:get_bool("creatura_protected_spawn", true)
local abr = (tonumber(minetest.get_mapgen_setting("active_block_range")) or 4) * 16
local max_per_block = tonumber(minetest.settings:get("creatura_mapblock_limit")) or 12
local max_in_abr = tonumber(minetest.settings:get("creatura_abr_limit")) or 24
local min_abm_dist = min(abr / 2, tonumber(minetest.settings:get("creatura_min_abm_dist")) or 32)
local mobs_spawn = minetest.settings:get_bool("mobs_spawn") ~= false
local mapgen_mobs = {}
function creatura.register_abm_spawn(mob, def)
local chance = def.chance or 3000
local interval = def.interval or 30
local min_height = def.min_height or 0
local max_height = def.max_height or 128
local min_time = def.min_time or 0
local max_time = def.max_time or 24000
local min_light = def.min_light or 1
local max_light = def.max_light or 15
local min_group = def.min_group or 1
local max_group = def.max_group or 4
local block_protected = def.block_protected_spawn or false
local biomes = def.biomes or {}
local nodes = def.nodes or {"group:soil", "group:stone"}
local neighbors = def.neighbors or {"air"}
local spawn_on_load = def.spawn_on_load or false
local spawn_in_nodes = def.spawn_in_nodes or false
local spawn_cap = def.spawn_cap or 5
local function spawn_func(pos, aocw)
if not mobs_spawn then
return
end
if not spawn_in_nodes then
pos.y = pos.y + 1
end
if (not protected_spawn
or block_protected)
and minetest.is_protected(pos, "") then
return
end
local tod = (minetest.get_timeofday() or 0) * 24000
local bounds_in = tod >= min_time and tod <= max_time
local bounds_ex = tod >= max_time or tod <= min_time
if (max_time > min_time and not bounds_in)
or (min_time > max_time and not bounds_ex) then
return
end
local light = minetest.get_node_light(pos) or 7
if light > max_light
or light < min_light then
return
end
if aocw
and aocw >= max_per_block then
return
end
if biomes
and #biomes > 0 then
local biome_id = minetest.get_biome_data(pos).biome
local biome_name = minetest.get_biome_name(biome_id)
local is_spawn_biome = false
for _, biome in ipairs(biomes) do
if biome:match("^" .. biome_name) then
is_spawn_biome = true
break
end
end
if not is_spawn_biome then return end
end
local mob_count = 0
local plyr_found = false
local objects = minetest.get_objects_inside_radius(pos, abr)
for _, object in ipairs(objects) do
local ent = object:get_luaentity()
if ent
and ent.name == mob then
mob_count = mob_count + 1
if mob_count > spawn_cap
or mob_count > max_in_abr then
return
end
end
if object:is_player() then
plyr_found = true
if vec_dist(pos, object:get_pos()) < min_abm_dist then
return
end
end
end
if not plyr_found then
return
end
local mob_def = minetest.registered_entities[mob]
local mob_width = mob_def.collisionbox[4]
local mob_height = mob_def.collisionbox[5]
if not can_spawn(pos, mob_width, mob_height) then
return
end
local group_size = random(min_group or 1, max_group or 1)
local obj
if group_size > 1 then
local offset
local spawn_pos
for _ = 1, group_size do
offset = ceil(mob_width)
spawn_pos = creatura.get_ground_level({
x = pos.x + random(-offset, offset),
y = pos.y,
z = pos.z + random(-offset, offset)
}, 3)
if not can_spawn(spawn_pos, mob_width, mob_height) then
spawn_pos = pos
end
obj = minetest.add_entity(spawn_pos, mob)
do_on_spawn(spawn_pos, obj)
end
else
obj = minetest.add_entity(pos, mob)
do_on_spawn(pos, obj)
end
minetest.log("action",
"[Creatura] [ABM Spawning] Spawned " .. group_size .. " " .. mob .. " at " .. minetest.pos_to_string(pos))
end
minetest.register_abm({
label = mob .. " spawning",
nodenames = nodes,
neighbors = neighbors,
interval = interval,
chance = chance,
min_y = min_height,
max_y = max_height,
catch_up = false,
action = function(pos, _, _, aocw)
spawn_func(pos, aocw)
end
})
if spawn_on_load then
table.insert(mapgen_mobs, mob)
end
creatura.registered_mob_spawns[mob] = {
chance = def.chance or 3000,
interval = def.interval or 30,
min_height = def.min_height or 0,
max_height = def.max_height or 128,
min_time = def.min_time or 0,
max_time = def.max_time or 24000,
min_light = def.min_light or 1,
max_light = def.max_light or 15,
min_group = def.min_group or 1,
max_group = def.max_group or 4,
block_protected = def.block_protected_spawn or false,
biomes = def.biomes or {},
nodes = def.nodes or {"group:soil", "group:stone"},
neighbors = def.neighbors or {"air"},
spawn_on_load = def.spawn_on_load or false,
spawn_in_nodes = def.spawn_in_nodes or false,
spawn_cap = def.spawn_cap or 5
}
end
----------------
-- DEPRECATED --
----------------
-- Mapgen --
minetest.register_node("creatura:spawn_node", {
drawtype = "airlike",
groups = {not_in_creative_inventory = 1},
walkable = false
})
local mapgen_spawning = false
local mapgen_spawning_int = tonumber(minetest.settings:get("creatura_mapgen_spawn_interval")) or 64
if mapgen_spawning then
local chunk_delay = 0
local c_air = minetest.get_content_id("air")
local c_spawn = minetest.get_content_id("creatura:spawn_node")
minetest.register_on_generated(function(minp, maxp)
if chunk_delay > 0 then chunk_delay = chunk_delay - 1 end
local meta_queue = {}
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
local data = vm:get_data()
local min_x, max_x = minp.x, maxp.x
local min_y, max_y = minp.y, maxp.y
local min_z, max_z = minp.z, maxp.z
local def
local center
local current_biome
local spawn_biomes
local current_pos
for _, mob_name in ipairs(mapgen_mobs) do
local mob_spawned = false
def = creatura.registered_mob_spawns[mob_name]
center = {
x = min_x + (max_x - min_x) * 0.5,
y = min_y + (max_y - min_y) * 0.5,
z = min_z + (max_z - min_z) * 0.5
}
current_biome = minetest.get_biome_name(minetest.get_biome_data(center).biome)
spawn_biomes = def.biomes
if not mob_spawned
and (not spawn_biomes
or table_contains(spawn_biomes, current_biome)) then
for z = min_z + 8, max_z - 7, 8 do
if mob_spawned then break end
for x = min_x + 8, max_x - 7, 8 do
if mob_spawned then break end
for y = min_y, max_y do
local vi = area:index(x, y, z)
if data[vi] == c_air
or data[vi] == c_spawn then
break
end
-- Check if position is outside of vertical bounds
if y > def.max_height
or y < def.min_height then
break
end
current_pos = vector.new(x, y, z)
-- Check if position has required nodes
if not pos_meets_params(current_pos, def) then
break
end
if def.spawn_in_nodes then
-- Add Spawn Node to Map
data[vi] = c_spawn
local group_size = random(def.min_group or 1, def.max_group or 1)
table.insert(meta_queue, {pos = current_pos, mob = mob_name, cluster = group_size})
mob_spawned = true
break
elseif data[area:index(x, y + 1, z)] == c_air then
vi = area:index(x, y + 1, z)
current_pos = vector.new(x, y + 1, z)
-- Add Spawn Node to Map
data[vi] = c_spawn
local group_size = random(def.min_group or 1, def.max_group or 1)
table.insert(meta_queue, {pos = current_pos, mob = mob_name, cluster = group_size})
mob_spawned = true
break
end
end
end
end
end
end
if #meta_queue > 0 then
vm:set_data(data)
vm:write_to_map()
for _, unset_meta in ipairs(meta_queue) do
local pos = unset_meta.pos
local mob = unset_meta.mob
local cluster = unset_meta.cluster
local meta = minetest.get_meta(pos)
meta:set_string("mob", mob)
meta:set_int("cluster", cluster)
end
chunk_delay = mapgen_spawning_int
end
end)
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 plyr_found = false
local objects = minetest.get_objects_inside_radius(pos, abr)
for _, object in ipairs(objects) do
if object:is_player() then
plyr_found = true
break
end
end
if not plyr_found then return end
local meta = minetest.get_meta(pos)
local name = meta:get_string("mob") or ""
if name == "" then minetest.remove_node(pos) return end
local amount = meta:get_int("cluster")
local obj
if amount > 0 then
for _ = 1, amount do
obj = minetest.add_entity(pos, name)
do_on_spawn(pos, obj)
end
minetest.log("action",
"[Creatura] Spawned " .. amount .. " " .. name .. " at " .. minetest.pos_to_string(pos))
else
obj = minetest.add_entity(pos, name)
do_on_spawn(pos, obj)
minetest.log("action",
"[Creatura] Spawned a " .. name .. " at " .. minetest.pos_to_string(pos))
end
minetest.remove_node(pos)
end,
})
end
function creatura.register_mob_spawn(name, def)
local spawn_def = {
chance = def.chance or 5,
min_height = def.min_height or 0,
max_height = def.max_height or 128,
min_time = def.min_time or 0,
max_time = def.max_time or 24000,
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_on_load = def.spawn_on_gen or false,
spawn_in_nodes = def.spawn_in_nodes or false,
spawn_cap = def.spawn_cap or 5,
--send_debug = def.send_debug or false
}
--creatura.registered_mob_spawns[name] = spawn_def
creatura.register_abm_spawn(name, spawn_def)
end
function creatura.register_spawn_egg(name, col1, col2, inventory_image)
if col1 and col2 then
local base = "(creatura_spawning_crystal_primary.png^[multiply:#" .. col1 .. ")"
local spots = "(creatura_spawning_crystal_secondary.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 = abs(mobdef.collisionbox[2])
local pos = minetest.get_pointed_thing_position(pointed_thing, true)
pos.y = (pos.y - 0.4) + spawn_offset
local object = minetest.add_entity(pos, name)
if object then
object:set_yaw(random(1, 6))
object:get_luaentity().last_yaw = object:get_yaw()
end
if not creative then
itemstack:take_item()
return itemstack
end
end
})
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 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B