diff --git a/api.lua b/api.lua index d6b1d4f..c3655a1 100644 --- a/api.lua +++ b/api.lua @@ -121,9 +121,11 @@ function creatura.get_node_height_from_def(name) 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 = minetest.get_node(node).name + node = get_node(node).name end local def = minetest.registered_nodes[node] or default_node_def if def.walkable @@ -133,45 +135,28 @@ function creatura.get_node_def(node) -- Node can be name or pos return def end -function creatura.get_ground_level(pos, max_diff) - local pos2 = pos -- Prevent modifying table that shouldn't be changed - pos2.y = math.floor(pos2.y - 0.49) - local node = minetest.get_node(pos2) - local node_under = minetest.get_node({ - x = pos2.x, - y = pos2.y - 1, - z = pos2.z - }) - local walkable = creatura.get_node_def(node_under.name).walkable and not creatura.get_node_def(node.name).walkable - if walkable then - return pos2 - end - if not creatura.get_node_def(node_under.name).walkable then - for _ = 1, max_diff do - pos2.y = pos2.y - 1 - node = minetest.get_node(pos2) - node_under = minetest.get_node({ - x = pos2.x, - y = pos2.y - 1, - z = pos2.z - }) - walkable = creatura.get_node_def(node_under.name).walkable and not creatura.get_node_def(node.name).walkable - if walkable then break end - end - else - for _ = 1, max_diff do - pos2.y = pos2.y + 1 - node = minetest.get_node(pos2) - node_under = minetest.get_node({ - x = pos2.x, - y = pos2.y - 1, - z = pos2.z - }) - walkable = creatura.get_node_def(node_under.name).walkable and not creatura.get_node_def(node.name).walkable - if walkable then break 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 i = 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 - return pos2 + if not get_node_def(under).walkable then + for i = 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) @@ -192,7 +177,7 @@ function creatura.is_pos_moveable(pos, width, height) local ray = minetest.raycast(pos3, pos4, false, false) for pointed_thing in ray do if pointed_thing.type == "node" then - local name = minetest.get_node(pointed_thing.under).name + local name = get_node(pointed_thing.under).name if creatura.get_node_def(name).walkable then return false end diff --git a/methods.lua b/methods.lua index 3b6f522..01db6c2 100644 --- a/methods.lua +++ b/methods.lua @@ -14,25 +14,27 @@ 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 +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_add = vector.add +local vec_sub = vector.subtract local yaw2dir = minetest.yaw_to_dir local dir2yaw = minetest.dir_to_yaw ---[[local function debugpart(pos, time, tex) +local function debugpart(pos, time, tex) minetest.add_particle({ pos = pos, texture = tex or "creatura_particle_red.png", @@ -40,7 +42,7 @@ local dir2yaw = minetest.dir_to_yaw glow = 16, size = 16 }) -end]] +end --------------------- -- Local Utilities -- @@ -101,35 +103,50 @@ end]] return false end]] -function creatura.get_collision_ranged(self, range) - local yaw = self.object:get_yaw() - local pos = self.object:get_pos() +local get_node_def = creatura.get_node_def +local get_node_height = creatura.get_node_height_from_def + +function creatura.get_collision_ranged(self, dir, range) + local pos, yaw = self.object:get_pos(), self.object:get_yaw() if not pos then return end - local width = self.width - local height = self.height + local width = self.width + 0.1 + local height = self.height + self.stepheight pos.y = pos.y + 0.01 - local m_dir = vec_normal(yaw2dir(yaw)) - m_dir.x, m_dir.z = m_dir.x * 0.5, m_dir.z * 0.5 - local ahead = vec_add(pos, vec_multi(m_dir, width + 0.5)) + dir = vec_normal(dir or yaw2dir(yaw)) + yaw = dir2yaw(dir) + local ahead = vec_add(pos, vec_multi(dir, width)) -- Loop + local cos_yaw = cos(yaw) + local sin_yaw = sin(yaw) local pos_x, pos_y, pos_z = ahead.x, ahead.y, ahead.z + local dir_x, dir_y, dir_z = dir.x, dir.y, dir.z + local dist + local collision for i = 0, range or 4 do - pos_x = pos_x + m_dir.x * i - pos_y = pos_y + m_dir.y * i - pos_z = pos_z + m_dir.z * i + pos_x = pos_x + dir_x + pos_y = pos_y + dir_y + pos_z = pos_z + dir_z + local pos2 for x = -width, width, width / ceil(width) do + pos2 = { + x = cos_yaw * ((pos_x + x) - pos_x) + pos_x, + y = pos_y, + z = sin_yaw * ((pos_x + x) - pos_x) + pos_z + } for y = 0, height, height / ceil(height) do - local pos2 = { - x = cos(yaw) * ((pos_x + x) - pos_x) + pos_x, - y = pos.y + y, - z = sin(yaw) * ((pos_x + x) - pos_x) + pos_z - } - if pos2.y - pos.y > (self.stepheight or 1.1) - and creatura.get_node_def(pos2).walkable then - return true, pos2 + pos2.y = pos_y + y + local dist2 = vec_dist(pos, pos2) + if not dist + or dist2 < dist then + if pos2.y - pos_y > (self.stepheight or 1.1) + and get_node_def(pos2).walkable then + collision = pos2 + dist = dist2 + end end end end + if collision then return true, collision end end return false end @@ -144,16 +161,19 @@ function creatura.get_collision(self) local m_dir = vec_normal(yaw2dir(yaw)) local ahead = vec_add(pos, vec_multi(m_dir, width + 1)) -- 1 node out from edge of box -- Loop + local cos_yaw = cos(yaw) + local sin_yaw = sin(yaw) local pos_x, pos_z = ahead.x, ahead.z for x = -width, width, width / ceil(width) do + local pos2 = { + x = cos_yaw * ((pos_x + x) - pos_x) + pos_x, + y = pos.y, + z = sin_yaw * ((pos_x + x) - pos_x) + pos_z + } for y = 0, height, height / ceil(height) do - local pos2 = { - x = cos(yaw) * ((pos_x + x) - pos_x) + pos_x, - y = pos.y + y, - z = sin(yaw) * ((pos_x + x) - pos_x) + pos_z - } + pos2.y = pos.y + y if pos2.y - pos.y > (self.stepheight or 1.1) - and creatura.get_node_def(pos2).walkable then + and get_node_def(pos2).walkable then return true, pos2 end end @@ -179,6 +199,65 @@ local function get_avoidance_dir(self) end end +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}) +} + +function creatura.get_context_steering(self, goal, range) + local pos, vel = self.object:get_pos(), self.object:get_velocity() + if not pos then return end + local heading = vec_normal(vel) + local dir2goal = vec_normal(vec_dir(pos, goal)) + local output_dir = {x = 0, y = dir2goal.y, z = 0} + range = range or 8 + if range < 2 then range = 2 end + for _, _dir in ipairs(steer_directions) do + local dir = table.copy(_dir) + local score = vec_dot(dir2goal, dir) + local interest = clamp(score, 0, 1) + local danger = 0 + if interest >= 0 then + local width = self.width + local height = self.height + local collision + if width <= 0.5 + and height <= 1 then + local pos2 = vec_add(pos, dir) + local pos2_name = minetest.get_node(pos2).name + collision = get_node_height(pos2_name) > self.stepheight and pos2 + if not collision then + local above = {x = pos2.x, y = pos2.y + 1, z = pos2.z} + collision = get_node_def(above).walkable and pos2 + end + else + local s_range = range * clamp(interest, 0.5, 1) + _, collision = creatura.get_collision_ranged(self, dir, s_range) + end + if collision then + local dir2col = vec_dir(pos, collision) + local dist2col = vec_dist(pos, collision) - self.width + local ahead = vec_add(pos, vec_multi(heading, self.width + dist2col)) + local avd_force = vec_normal(vec_sub(ahead, collision)) + dir.y = avd_force.y / 4 + local dot_weight = vec_dot(vec_normal(dir2col), dir) + local dist_weight = (range - dist2col) / range + interest = interest - dot_weight + danger = dist_weight + end + end + score = clamp(interest - danger, 0, 1) + output_dir = vector.add(output_dir, vector.multiply(dir, score)) + end + return output_dir +end + ------------- -- Actions -- ------------- @@ -414,4 +493,27 @@ creatura.register_movement_method("creatura:obstacle_avoidance", function(self) _self:turn_to(goal_yaw, turn_rate) end return func +end) + +creatura.register_movement_method("creatura:context_based_steering", function(self) + local steer_to + local steer_timer = 0.25 + self:set_gravity(-9.8) + local function func(_self, goal, speed_factor) + local pos = _self.object:get_pos() + if not pos then return end + if vec_dist(pos, goal) < clamp(self.width, 0.5, 1) then + _self:halt() + return true + end + -- Calculate Movement + steer_timer = (steer_timer > 0 and steer_timer - self.dtime) or 0.25 + steer_to = (steer_timer <= 0 and creatura.get_context_steering(self, goal, 2)) or steer_to + local speed = abs(_self.speed or 2) * speed_factor or 0.5 + local turn_rate = abs(_self.turn_rate or 5) + -- Apply Movement + _self:set_forward_velocity(speed) + _self:turn_to(dir2yaw(steer_to or vec_dir(pos, goal)), turn_rate) + end + return func end) \ No newline at end of file diff --git a/mob_meta.lua b/mob_meta.lua index a06ea3c..f6fa6d3 100644 --- a/mob_meta.lua +++ b/mob_meta.lua @@ -438,7 +438,7 @@ function mob:is_pos_safe(pos) for i = 1, self.max_fall or 3 do local fall_pos = { x = pos.x, - y = floor(mob_pos.y + 0.5) - i, + y = floor(pos.y + 0.5) - i, z = pos.z } if creatura.get_node_def(fall_pos).walkable then @@ -695,6 +695,8 @@ end function mob:clear_action() self._action = {} + self._movement_data.goal = nil + self._movement_data.func = nil end function mob:set_utility(func) @@ -1142,12 +1144,13 @@ function mob:_execute_utilities() and is_alive then for i = 1, #self.utility_stack do local utility = self.utility_stack[i].utility + local util_data = self._utility_data local get_score = self.utility_stack[i].get_score local step_delay = self.utility_stack[i].step_delay local score, args = get_score(self) - if self._utility_data.utility - and utility == self._utility_data.utility - and self._utility_data.score > 0 + if util_data.utility + and utility == util_data.utility + and util_data.score > 0 and score <= 0 then self._utility_data = { utility = nil, @@ -1155,9 +1158,10 @@ function mob:_execute_utilities() step_delay = nil, score = 0 } + util_data = self._utility_data end if score > 0 - and score >= self._utility_data.score + and score >= util_data.score and score >= loop_data.score then loop_data = { utility = utility, diff --git a/spawning.lua b/spawning.lua index b299a0a..9b13932 100644 --- a/spawning.lua +++ b/spawning.lua @@ -493,23 +493,16 @@ function creatura.register_abm_spawn(mob, def) local function spawn_func(pos, _, _, aocw) - if spawn_on_load - and random(chance) > 1 then - return - end - - if spawn_on_load - and not minetest.find_node_near(pos, 1, neighbors) then - return - end - - if pos.y > max_height - or pos.y < min_height then - return - end - if not spawn_in_nodes then - pos = vec_raise(pos, 1) + pos.y = pos.y + 1 + end + + if spawn_on_load -- Manual checks for LBMs + and (random(chance) > 1 + or not minetest.find_node_near(pos, 1, neighbors) + or pos.y > max_height + or pos.y < min_height) then + return end local light = minetest.get_node_light(pos) or 7 @@ -530,9 +523,7 @@ function creatura.register_abm_spawn(mob, def) local objects = minetest.get_objects_inside_radius(pos, abr) for _, object in ipairs(objects) do - local ent = object and object:get_luaentity() - local is_plyr = object and object:is_player() - plyr_found = plyr_found or is_plyr + local ent = object:get_luaentity() if ent and ent.name == mob then mob_count = mob_count + 1 @@ -540,6 +531,7 @@ function creatura.register_abm_spawn(mob, def) return end end + plyr_found = plyr_found or object:is_player() end if not plyr_found then @@ -550,8 +542,6 @@ function creatura.register_abm_spawn(mob, def) local mob_width = mob_def.collisionbox[4] local mob_height = math.max(0, mob_def.collisionbox[5] - mob_def.collisionbox[2]) - pos.y = pos.y - 0.49 - if not creatura.is_pos_moveable(pos, mob_width, mob_height) then return end @@ -560,10 +550,11 @@ function creatura.register_abm_spawn(mob, def) if group_size > 1 then for _ = 1, group_size do + local offset = math.ceil(mob_width) local spawn_pos = creatura.get_ground_level({ - x = pos.x + random(-mob_width, mob_width), + x = pos.x + random(-offset, offset), y = pos.y, - z = pos.x + random(-mob_width, mob_width), + z = pos.x + random(-offset, offset), }, 3) if not creatura.is_pos_moveable(spawn_pos, mob_width, mob_height) then spawn_pos = pos @@ -589,11 +580,11 @@ function creatura.register_abm_spawn(mob, def) end end end + minetest.log("action", "[Creatura] [ABM Spawning] Spawned " .. group_size .. " " .. mob .. " at " .. minetest.pos_to_string(pos)) - end if spawn_on_load then @@ -613,6 +604,8 @@ function creatura.register_abm_spawn(mob, def) neighbors = neighbors, interval = interval, chance = chance, + min_y = min_height, + max_y = max_height, catch_up = false, action = function(pos, ...) spawn_func(pos, ...)