write something there
This commit is contained in:
commit
b4b6c08f4f
8546 changed files with 309825 additions and 0 deletions
15
mods/creatura/.luacheckrc
Normal file
15
mods/creatura/.luacheckrc
Normal 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
21
mods/creatura/LICENSE
Normal 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
556
mods/creatura/api.lua
Normal 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
232
mods/creatura/boids.lua
Normal 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
280
mods/creatura/doc.txt
Normal 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
25
mods/creatura/init.lua
Normal 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
677
mods/creatura/methods.lua
Normal 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
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
2
mods/creatura/mod.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
name = creatura
|
||||
description = A performant, semi-modular mob API
|
805
mods/creatura/pathfinder_deprecated.lua
Normal file
805
mods/creatura/pathfinder_deprecated.lua
Normal 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
|
627
mods/creatura/pathfinding.lua
Normal file
627
mods/creatura/pathfinding.lua
Normal 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
|
32
mods/creatura/settingtypes.txt
Normal file
32
mods/creatura/settingtypes.txt
Normal 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
|
BIN
mods/creatura/sounds/creatura_hit_1.ogg
Normal file
BIN
mods/creatura/sounds/creatura_hit_1.ogg
Normal file
Binary file not shown.
BIN
mods/creatura/sounds/creatura_hit_2.ogg
Normal file
BIN
mods/creatura/sounds/creatura_hit_2.ogg
Normal file
Binary file not shown.
BIN
mods/creatura/sounds/creatura_hit_3.ogg
Normal file
BIN
mods/creatura/sounds/creatura_hit_3.ogg
Normal file
Binary file not shown.
573
mods/creatura/spawning.lua
Normal file
573
mods/creatura/spawning.lua
Normal 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
|
BIN
mods/creatura/textures/creatura_particle_green.png
Normal file
BIN
mods/creatura/textures/creatura_particle_green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
BIN
mods/creatura/textures/creatura_particle_red.png
Normal file
BIN
mods/creatura/textures/creatura_particle_red.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.5 KiB |
BIN
mods/creatura/textures/creatura_smoke_particle.png
Normal file
BIN
mods/creatura/textures/creatura_smoke_particle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
BIN
mods/creatura/textures/creatura_spawning_crystal_outline.png
Normal file
BIN
mods/creatura/textures/creatura_spawning_crystal_outline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5 KiB |
BIN
mods/creatura/textures/creatura_spawning_crystal_primary.png
Normal file
BIN
mods/creatura/textures/creatura_spawning_crystal_primary.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
mods/creatura/textures/creatura_spawning_crystal_secondary.png
Normal file
BIN
mods/creatura/textures/creatura_spawning_crystal_secondary.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 698 B |
Loading…
Add table
Add a link
Reference in a new issue