diff --git a/behaviors.lua b/behaviors.lua new file mode 100644 index 0000000..9d6f0bb --- /dev/null +++ b/behaviors.lua @@ -0,0 +1,1663 @@ +--------------- +-- Behaviors -- +--------------- + +-- Math -- + +local abs = math.abs +local random = math.random +local ceil = math.ceil +local floor = math.floor +local rad = math.rad + +local function average(t) + local sum = 0 + for _,v in pairs(t) do -- Get the sum of all numbers in t + sum = sum + v + end + return sum / #t +end + +local function clamp(val, min, max) + if val < min then + val = min + elseif max < val then + val = max + end + return val +end + +-- Vector Math -- + +local vec_dist = vector.distance +local vec_dir = vector.direction +local vec_sub = vector.subtract +local vec_add = vector.add +local vec_multi = vector.multiply +local vec_normal = vector.normalize + +local function vec_raise(v, n) + return {x = v.x, y = v.y + n, z = v.z} +end + +local yaw2dir = minetest.yaw_to_dir +local dir2yaw = minetest.dir_to_yaw + +-------------- +-- Settings -- +-------------- + +------------ +-- Tables -- +------------ + +local is_flyable = {} +local is_liquid = {} +local is_solid = {} + +minetest.register_on_mods_loaded(function() + for name in pairs(minetest.registered_nodes) do + if name ~= "air" and name ~= "ignore" then + if minetest.registered_nodes[name].walkable + or minetest.registered_nodes[name].drawtype == "liquid" then + is_flyable[name] = true + if minetest.registered_nodes[name].walkable then + is_solid[name] = true + else + is_liquid[name] = true + end + end + end + end +end) + +--------------------- +-- Local Utilities -- +--------------------- + +local moveable = creatura.is_pos_moveable +local fast_ray_sight = creatura.fast_ray_sight + +local function get_ground_level(pos2, max_height) + local node = minetest.get_node(pos2) + local node_under = minetest.get_node({ + x = pos2.x, + y = pos2.y - 1, + z = pos2.z + }) + local height = 0 + local walkable = is_solid[node_under.name] and not is_solid[node.name] + if walkable then + return pos2 + elseif not walkable then + if not is_solid[node_under.name] then + while not is_solid[node_under.name] + and height < max_height do + pos2.y = pos2.y - 1 + node_under = minetest.get_node({ + x = pos2.x, + y = pos2.y - 1, + z = pos2.z + }) + height = height + 1 + end + else + while is_solid[node.name] + and height < max_height do + pos2.y = pos2.y + 1 + node = minetest.get_node(pos2) + height = height + 1 + end + end + return pos2 + end +end + +local function get_ceiling_positions(pos, range) + local walkable = minetest.find_nodes_in_area( + {x = pos.x + range, y = pos.y + range, z = pos.z + range}, + {x = pos.x - range, y = pos.y, z = pos.z - range}, + animalia.walkable_nodes + ) + if #walkable < 1 then return {} end + local output = {} + for i = 1, #walkable do + local i_pos = walkable[i] + local under = { + x = i_pos.x, + y = i_pos.y - 1, + z = i_pos.z + } + if minetest.get_node(under).name == "air" + and is_solid[minetest.get_node(i_pos).name] then + table.insert(output, i_pos) + end + end + return output +end + +local function get_obstacle_avoidance(self, lift) + local pos = self.object:get_pos() + local yaw = self.object:get_yaw() + pos.y = pos.y + self.stepheight + local vel = self.object:get_velocity() + local vel_len = abs(vector.length(vel)) + if vel_len < 1.5 then + vel_len = 1.5 + end + local dir = yaw2dir(yaw) + dir.y = lift + local outset = vec_add(pos, vec_multi(dir, vel_len)) + local pos2 + local obstacle = false + if not fast_ray_sight(pos, outset) then + pos2 = vec_add(pos, vec_multi(dir, -vel_len)) + obstacle = true + end + return pos2, obstacle +end + +local function get_wander_pos_3d(self, range) + local outset = random(range or 4) + local pos = self.object:get_pos() + local move_dir = { + x = random(-10, 10) * 0.1, + z = random(-10, 10) * 0.1, + y = random(-10, 10) * 0.1 + } + local pos2 = vec_add(pos, vec_multi(vec_normal(move_dir), random(1, outset))) + local sight, dist = fast_ray_sight(pos, pos2, true) + if not sight then + pos2 = vec_add(pos, vec_multi(vec_normal(move_dir), dist - 0.5)) + end + return pos2 +end + +local function get_boid_members(pos, radius, name, texture_no) + 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 + and object:get_luaentity().texture_no == texture_no then + object:get_luaentity().boid_heading = rad(random(360)) + table.insert(members, object) + end + end + return members +end + +---------------------- +-- Movement Methods -- +---------------------- + +local function movement_fly(self, pos2) + -- Initial Properties + local pos = self.object:get_pos() + local turn_rate = self.turn_rate or 10 + local speed = self.speed or 2 + self:animate("fly") + self:set_gravity(0) + -- Collision Avoidance + local temp_goal = self._movement_data.temp_goal + local obstacle = self._movement_data.obstacle or false + if not temp_goal + or self:pos_in_box(temp_goal, self.width) then + self._movement_data.temp_goal, self._movement_data.obstacle = get_obstacle_avoidance(self, vec_dir(pos, pos2).y) + end + local neighbor = self._movement_data.temp_goal + -- Calculate Movement + local dir = vector.direction(pos, pos2) + local tyaw = minetest.dir_to_yaw(dir) + if neighbor then + local lift = dir.y + dir = vector.direction(pos, neighbor) + if not obstacle then + dir.y = lift + end + tyaw = minetest.dir_to_yaw(dir) + end + if self._path + and #self._path > 1 then + neighbor = self._path[2] + dir = vector.direction(pos, neighbor) + tyaw = minetest.dir_to_yaw(dir) + if self:pos_in_box(neighbor, self.width + 0.2) then + table.remove(self._path, 1) + end + else + self._path = creatura.find_path(self, pos, pos2, self.width, self.height, 300, false, true) + end + -- Apply Movement + self:turn_to(tyaw, turn_rate) + self:set_forward_velocity(speed) + local v_speed = speed * dir.y + local vel = self.object:get_velocity() + vel.y = vel.y + (v_speed - vel.y) * 0.2 + self:set_vertical_velocity(vel.y) + if self:pos_in_box(pos2) then + self:halt() + end +end + +creatura.register_movement_method("animalia:fly_path", movement_fly) + +local function movement_fly_waypoints(self, pos2, speed) + -- Initial Properties + local pos = self.object:get_pos() + self:animate("fly") + self:set_gravity(0) + -- Collision Avoidance + local temp_goal = self._movement_data.temp_goal + local obstacle = self._movement_data.obstacle or false + if not temp_goal + or self:pos_in_box(temp_goal, 0.4) then + self._movement_data.temp_goal, self._movement_data.obstacle = get_obstacle_avoidance(self, vec_dir(pos, pos2).y) + end + local neighbor = self._movement_data.temp_goal + -- Calculate Movement + local dir = vector.direction(pos, pos2) + local tyaw = minetest.dir_to_yaw(dir) + local turn_rate = self.turn_rate or 10 + local speed = self.speed or 2 + if neighbor then + local lift = dir.y + dir = vector.direction(pos, neighbor) + if not obstacle then + dir.y = lift + end + tyaw = minetest.dir_to_yaw(dir) + end + -- Apply Movement + self:turn_to(boid_angle or tyaw, turn_rate) + self:set_forward_velocity(speed) + local v_speed = speed * dir.y + local vel = self.object:get_velocity() + vel.y = vel.y + (v_speed - vel.y) * 0.2 + self:set_vertical_velocity(vel.y) + if self:pos_in_box(pos2) then + self:halt() + end +end + +creatura.register_movement_method("animalia:fly_waypoints", movement_fly_waypoints) + +-- Fly Obstacle Avoidance -- + +local function movement_fly_obstacle_avoidance(self, pos2, speed) + -- Initial Properties + local pos = self.object:get_pos() + local turn_rate = self.turn_rate or 10 + local speed = self.speed or 2 + self:animate("fly") + self:set_gravity(0) + -- Collision Avoidance + local temp_goal = self._movement_data.temp_goal + local obstacle = self._movement_data.obstacle or false + local timer = self._movement_data.timer + if not temp_goal + or self:pos_in_box(temp_goal, 0.4) + or (timer + and timer <= 0) then + self._movement_data.temp_goal, self._movement_data.obstacle = get_obstacle_avoidance(self, vec_dir(pos, pos2).y) + temp_goal = self._movement_data.temp_goal + obstacle = self._movement_data.obstacle or false + if temp_goal then + self._movement_data.timer = 3 + end + end + if timer then + self._movement_data.timer = self._movement_data.timer - self.dtime + end + -- Calculate Movement + local dir = vector.direction(pos, pos2) + local tyaw = minetest.dir_to_yaw(dir) + if temp_goal + and obstacle then + dir = vector.direction(pos, temp_goal) + dir.y = vec_dir(pos, pos2).y + tyaw = minetest.dir_to_yaw(dir) + end + -- Apply Movement + self:turn_to(tyaw, turn_rate) + self:set_forward_velocity(speed) + local v_speed = (speed) * dir.y + local vel = self.object:get_velocity() + vel.y = vel.y + (v_speed - vel.y) * 0.2 + self:set_vertical_velocity(vel.y) + if self:pos_in_box(pos2, 0.5) then + self:halt() + end +end + +creatura.register_movement_method("animalia:fly_obstacle_avoidance", movement_fly_obstacle_avoidance) + +-- Swimming -- + +local function movement_swim_obstacle_avoidance(self, pos2, speed) + -- Initial Properties + local pos = self.object:get_pos() + self:animate("swim") + self:set_gravity(0) + -- Collision Avoidance + local temp_goal = self._movement_data.temp_goal + local obstacle = self._movement_data.obstacle or false + local timer = self._movement_data.timer + if not temp_goal + or self:pos_in_box(temp_goal, 0.4) + or (timer + and timer <= 0) then + self._movement_data.temp_goal, self._movement_data.obstacle = get_obstacle_avoidance(self, vec_dir(pos, pos2).y) + temp_goal = self._movement_data.temp_goal + obstacle = self._movement_data.obstacle or false + if temp_goal then + self._movement_data.timer = vec_dist(pos, temp_goal) / self.speed + end + end + if timer then + self._movement_data.timer = self._movement_data.timer - self.dtime + end + -- Calculate Movement + local dir = vector.direction(pos, pos2) + local tyaw = minetest.dir_to_yaw(dir) + if temp_goal + and obstacle then + dir = vector.direction(pos, temp_goal) + tyaw = minetest.dir_to_yaw(dir) + end + -- Apply Movement + self:turn_to(tyaw, turn_rate) + self:set_forward_velocity(speed) + local v_speed = speed * dir.y + local vel = self.object:get_velocity() + vel.y = vel.y + (v_speed - vel.y) * 0.2 + if not is_liquid[minetest.get_node(vec_raise(pos, 1)).name] + and vel.y > 0 then + vel.y = 0 + end + self:set_vertical_velocity(vel.y) + if self:pos_in_box(pos2) then + self:halt() + end +end + +creatura.register_movement_method("animalia:swim_obstacle_avoidance", movement_swim_obstacle_avoidance) + +------------- +-- Actions -- +------------- + +function animalia.action_fall(self) + local function func(self) + self:animate("fall") + self:set_gravity(-1) + local vel = self.object:get_velocity() + if vel.y < -3.8 then + self:set_vertical_velocity(-0.1) + end + self._fall_start = nil + if self.touching_ground then + return true + end + end + self:set_action(func) +end + +function animalia.action_punch(self, target) + local function func(self) + if not creatura.is_alive(target) then + return true + end + local yaw = self.object:get_yaw() + local pos = self.object:get_pos() + local tpos = target:get_pos() + local dir = vector.direction(pos, tpos) + local tyaw = minetest.dir_to_yaw(dir) + self:turn_to(tyaw) + if self.touching_ground then + self:animate("leap") + local jump_vel = vec_multi(dir, self.speed) + jump_vel.y = 3 + self.object:add_velocity(jump_vel) + end + if vec_dist(pos, tpos) < 2 then + self:punch_target(target) + return true + end + end + self:set_action(func) +end + +function animalia.action_latch_to_ceil(self, time, anim) + local timer = time + local function func(self) + self:halt() + self:set_forward_velocity(0) + self:set_vertical_velocity(9) + self:set_gravity(3) + self:animate(anim or "latch") + timer = timer - self.dtime + if timer <= 0 then + return true + end + end + self:set_action(func) +end + +function animalia.action_boid_move(self, pos2, timeout, method) + local boids = get_boid_members(self.object:get_pos(), 6, self.name, self.texture_no) + local timer = timeout + local goal = pos2 + local function func(self) + local pos = self.object:get_pos() + timer = timer - self.dtime + if #boids > 2 then + local boid_angle, boid_lift = creatura.get_boid_angle(self, boids, 6) + if boid_angle then + local dir2goal = vec_dir(pos, pos2) + local yaw2goal = minetest.dir_to_yaw(dir2goal) + boid_angle = boid_angle + (yaw2goal - boid_angle) * 0.15 + local boid_dir = minetest.yaw_to_dir(boid_angle) + if boid_lift then + boid_dir.y = boid_lift + (vec_dir(pos, goal).y - boid_lift) * 0.5 + else + boid_dir.y = vec_dir(pos, goal).y + end + pos2 = vec_add(pos, vec_multi(boid_dir, 4)) + end + end + if timer <= 0 + or self:pos_in_box(pos2, 0.25) then + self:halt() + return true + end + self:move(pos2, method or "animalia:fly_obstacle_avoidance", 1) + end + self:set_action(func) +end + +function animalia.action_boid_walk(self, pos2, timeout, method, speed_factor, anim) + local boids = creatura.get_boid_members(self.object:get_pos(), 12, self.name) + local timer = timeout + local move_init = false + local goal = pos2 + local function func(self) + local pos = self.object:get_pos() + timer = timer - self.dtime + if #boids > 2 then + local boid_angle = creatura.get_boid_angle(self, boids, 12) + if boid_angle then + local dir2goal = vec_dir(pos, goal) + local yaw2goal = minetest.dir_to_yaw(dir2goal) + boid_angle = boid_angle + (yaw2goal - boid_angle) * 0.15 + local boid_dir = minetest.yaw_to_dir(boid_angle) + pos2 = get_ground_level(vec_add(pos, vec_multi(boid_dir, 4)), 2) + end + end + if not pos2 + or (move_init + and not self._movement_data.goal) then + return true + end + if timer <= 0 + or self:pos_in_box({x = goal.x, y = pos.y + 0.1, z = goal.z}) + or vec_dist(pos, goal) < 1 then + self:halt() + return true + end + self:move(pos2, method or "creatura:obstacle_avoidance", speed_factor or 1, anim or "walk") + move_init = true + end + self:set_action(func) +end + +function animalia.action_swim(self, pos, timeout, method, speed_factor, anim) + local timer = timeout or 4 + local function func(self) + timer = timer - self.dtime + if timer <= 0 + or self:pos_in_box(pos) then + self:halt() + self:set_gravity(0) + return true + end + self:move(pos, method or "animalia:swim_obstacle_avoidance", speed_factor or 0.5, anim) + self:set_gravity(0) + end + self:set_action(func) +end + +function animalia.action_horse_spin(self, speed, anim) + local tyaw = random(math.pi * 2) + local function func(self) + self:set_gravity(-9.8) + self:halt() + self:animate(anim or "stand") + self:turn_to(tyaw, speed) + if abs(tyaw - self.object:get_yaw()) < 0.1 then + return true + end + end + self:set_action(func) +end + +--------------- +-- Behaviors -- +--------------- + +------------------------ +-- Register Utilities -- +------------------------ + +-- Wander + +creatura.register_utility("animalia:wander", function(self, group) + local idle_time = 3 + local move_probability = 5 + local far_from_group = false + local group_tick = 1 + local function func(self) + local pos = self.object:get_pos() + if not self:get_action() then + local goal + local move = random(move_probability) < 2 + if self.lasso_pos + and vec_dist(pos, self.lasso_pos) > 10 then + goal = self.lasso_pos + end + if not goal + and move then + goal = self:get_wander_pos(1, 2) + end + if group + and goal + and group_tick > 3 then + local range = self.tracking_range * 0.5 + local group_positions = animalia.get_group_positions(self.name, pos, range + 1) + if #group_positions > 2 then + local center = animalia.get_average_pos(group_positions) + if center + and vec_dist(pos, center) > range * 0.33 + or vec_dist(goal, center) > range * 0.33 then + goal = center + far_from_group = true + else + far_from_group = false + end + end + group_tick = 0 + end + if (move + and goal) + or far_from_group then + creatura.action_walk(self, goal, 2, "creatura:neighbors") + else + creatura.action_idle(self, idle_time) + end + if group then + group_tick = group_tick + 1 + end + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:skittish_wander", function(self) + local idle_time = 3 + local move_probability = 3 + local force_move = false + local avoid_tick = 1 + local function func(self) + local pos = self.object:get_pos() + if not self:get_action() then + local goal + local move = random(move_probability) < 2 + if avoid_tick > 3 + and move then + local range = self.tracking_range * 0.5 + local player = creatura.get_nearby_player(self) + if player then + local target_alive, line_of_sight, player_pos = self:get_target(player) + if target_alive + and line_of_sight + and vec_dist(pos, player_pos) < 8 then + force_move = true + local dir = vec_dir(player_pos, pos) + goal = self:get_wander_pos(2, 3, dir) + end + end + avoid_tick = 0 + end + if self.lasso_pos + and vec_dist(pos, self.lasso_pos) > 10 then + goal = self.lasso_pos + end + if not goal + and move then + goal = self:get_wander_pos(4, 4) + end + if move + and goal then + creatura.action_walk(self, goal, 3, "creatura:neighbors", 0.35) + else + creatura.action_idle(self, idle_time) + end + avoid_tick = avoid_tick + 1 + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:skittish_boid_wander", function(self) + local idle_time = 3 + local move_probability = 3 + local group_tick = 0 + local force_move = false + local function func(self) + local pos = self.object:get_pos() + local goal + if self:timer(3) then + local range = self.tracking_range * 0.5 + local group_positions = animalia.get_group_positions(self.name, pos, range + 1) + if #group_positions > 2 then + local center = animalia.get_average_pos(group_positions) + if center + and vec_dist(pos, center) > range then + goal = center + force_move = true + else + force_move = false + end + else + force_move = false + end + group_tick = 2 + local player = creatura.get_nearby_player(self) + if player then + local target_alive, line_of_sight, player_pos = self:get_target(player) + if target_alive + and line_of_sight + and vec_dist(pos, player_pos) < 8 then + force_move = true + local dir = vec_dir(player_pos, pos) + goal = self:get_wander_pos(2, 3, dir) + end + end + end + if not self:get_action() then + local move = random(move_probability) < 2 + if self.lasso_pos + and vec_dist(pos, self.lasso_pos) > 10 then + goal = self.lasso_pos + end + if not goal + and move then + goal = self:get_wander_pos(4, 4) + end + if move + and goal then + animalia.action_boid_walk(self, goal, 6, "creatura:neighbors", 0.35) + else + creatura.action_idle(self, idle_time) + end + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:boid_wander", function(self, group) + local idle_time = 3 + local move_probability = 5 + local group_tick = 1 + local function func(self) + local pos = self.object:get_pos() + if not self:get_action() then + local goal + local move = random(move_probability) < 2 + if self.lasso_pos + and vec_dist(pos, self.lasso_pos) > 10 then + goal = self.lasso_pos + end + if not goal + and move then + goal = self:get_wander_pos(1, 2) + end + if group + and goal + and group_tick > 3 then + local range = self.tracking_range * 0.5 + local group_positions = animalia.get_group_positions(self.name, pos, range + 1) + if #group_positions > 2 then + local center = animalia.get_average_pos(group_positions) + if center + and vec_dist(pos, center) > range * 0.33 + or vec_dist(goal, center) > range * 0.33 then + goal = center + far_from_group = true + else + far_from_group = false + end + end + group_tick = 0 + end + if (move + or far_from_group) + and goal then + animalia.action_boid_walk(self, goal, 6, "creatura:neighbors", 0.35) + else + creatura.action_idle(self, idle_time) + end + group_tick = group_tick + 1 + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:wander_water_surface", function(self) + local idle_time = 3 + local move_probability = 3 + local function func(self) + if not self.in_liquid then return true end + local pos = self.object:get_pos() + local random_goal = get_wander_pos_3d(self, 2) + if not self:get_action() then + if self.lasso_pos + and vec_dist(pos, self.lasso_pos) > 10 then + random_goal = self.lasso_pos + end + if random(move_probability) < 2 + and random_goal then + animalia.action_swim(self, random_goal) + else + creatura.action_idle(self, idle_time, "float") + end + end + self:set_gravity(0) + end + self:set_utility(func) +end) + +-- "Eat" nodes + +creatura.register_utility("animalia:eat_from_turf", function(self) + local action_init = false + local function func(self) + local pos = self.object:get_pos() + local look_dir = yaw2dir(self.object:get_yaw()) + local under = vec_add(pos, vec_multi(look_dir, self.width)) + under.y = pos.y - 0.5 + if not action_init then + for i, node in ipairs(self.consumable_nodes) do + if node.name == minetest.get_node(under).name then + minetest.set_node(under, {name = node.replacement}) + local def = minetest.registered_nodes[node.name] + local texture = def.tiles[1] + texture = texture .. "^[resize:8x8" + minetest.add_particlespawner({ + amount = 6, + time = 0.1, + minpos = vector.new( + pos.x - 0.5, + pos.y + 0.1, + pos.z - 0.5 + ), + maxpos = vector.new( + pos.x + 0.5, + pos.y + 0.1, + pos.z + 0.5 + ), + minvel = {x=-1, y=1, z=-1}, + maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=-5, z=0}, + maxacc = {x=0, y=-9, z=0}, + minexptime = 1, + maxexptime = 1, + minsize = 1, + maxsize = 2, + collisiondetection = true, + vertical = false, + texture = texture, + }) + self.gotten = false + self:memorize("gotten", self.gotten) + if not self:get_action() then + creatura.action_idle(self, 1, "eat") + action_init = true + end + break + elseif i == #self.consumable_nodes then + return true + end + end + elseif not self:get_action() then + return true + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:eat_bug_nodes", function(self) + local timer = 0.2 + local pos = self.object:get_pos() + local food = minetest.find_nodes_in_area(vec_sub(pos, 1.5), vec_add(pos, 1.5), self.follow) + local function func(self) + pos = self.object:get_pos() + if food[1] then + local dist = vector.distance(pos, food[1]) + local dir = vec_dir(pos, food[1]) + local frame = floor(dist * 10) + self:turn_to(dir2yaw(dir)) + if frame < 15 + and frame > 1 then + animalia.move_head(self, dir2yaw(dir), dir.y) + creatura.action_idle(self, 0.1, "tongue_" .. frame) + timer = timer - self.dtime + elseif not self:get_action() then + local pos2 = vec_add(food[1], vec_multi(vec_normal(vec_dir(food[1], pos)), 0.25)) + creatura.action_walk(self, pos2) + end + else + return true + end + if timer <= 0 + and food[1] then + minetest.remove_node(food[1]) + return true + end + end + self:set_utility(func) +end) + +-- Escape Water + +creatura.register_utility("animalia:swim_to_land", function(self) + local init = false + local tpos = nil + local function func(self) + if not init then + for i = 1, 359, 15 do + local yaw = rad(i) + local dir = yaw2dir(yaw) + tpos = animalia.find_collision(self, dir) + if tpos then + local node = minetest.get_node({x = tpos.x, y = tpos.y + 1, z = tpos.z}) + if node.name == "air" then + break + else + tpos = nil + end + end + end + init = true + end + if tpos then + local pos = self.object:get_pos() + local yaw = self.object:get_yaw() + local tyaw = minetest.dir_to_yaw(vec_dir(pos, tpos)) + if abs(tyaw - yaw) > 0.1 then + self:turn_to(tyaw, 12) + end + self:set_gravity(-9.8) + self:set_forward_velocity(self.speed * 0.66) + self:animate("walk") + if vector.distance(pos, tpos) < 1 + or (not self.in_liquid + and self.touching_ground) then + return true + end + else + self.liquid_recovery_cooldown = 5 + return true + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:flop", function(self) + local function func(self) + if self.in_liquid then + return true + end + if not self:get_action() then + creatura.action_idle(self, 0.1, "flop") + end + self:set_vertical_velocity(0) + self:set_gravity(-9.8) + end + self:set_utility(func) +end) + +-- Player Interaction + +creatura.register_utility("animalia:flee_from_player", function(self, player, range) + range = range or self.tracking_range + local function func(self) + local target_alive, line_of_sight, tpos = self:get_target(player) + if not target_alive then return true end + local pos = self.object:get_pos() + local dir = vec_dir(pos, tpos) + local escape_pos = vec_add(pos, vec_multi(vec_add(dir, {x = random(-10, 10) * 0.1, y = 0, z = random(-10, 10) * 0.1}), -3)) + if not self:get_action() then + escape_pos = get_ground_level(escape_pos, 1) + if self.lasso_pos + and vec_dist(pos, self.lasso_pos) > 10 then + escape_pos = self.lasso_pos + end + creatura.action_walk(self, escape_pos, 2, "creatura:obstacle_avoidance", 1, "run") + end + if vec_dist(pos, tpos) > range then + return true + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:boid_flee_from_player", function(self, player, group) + local mobs_in_group = animalia.get_group(self) + if group then + if #mobs_in_group > 0 then + for i = 1, #mobs_in_group do + local mob = mobs_in_group[i] + mob:get_luaentity():initiate_utility("animalia:boid_flee_from_player", mob:get_luaentity(), player) + mob:get_luaentity():set_utility_score(1) + end + end + end + local function func(self) + local target_alive, line_of_sight, tpos = self:get_target(player) + if not target_alive then return true end + local pos = self.object:get_pos() + local dir = vec_dir(pos, tpos) + local escape_pos = vec_add(pos, vec_multi(vec_add(dir, {x = random(-10, 10) * 0.1, y = 0, z = random(-10, 10) * 0.1}), -3)) + if not self:get_action() then + escape_pos = get_ground_level(escape_pos, 1) + if self.lasso_pos + and vec_dist(pos, self.lasso_pos) > 10 then + escape_pos = self.lasso_pos + end + animalia.action_boid_walk(self, escape_pos, 6, "creatura:obstacle_avoidance", 1, "run") + end + if vec_dist(pos, tpos) > self.tracking_range + (#mobs_in_group or 0) then + return true + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:flee_to_water", function(self) + local function func(self) + local pos = self.object:get_pos() + local water = minetest.find_nodes_in_area_under_air(vec_sub(pos, 3), vec_add(pos, 3), {"default:water_source"}) + if water[1] + and vec_dist(pos, water[1]) < 0.5 then + return true + end + if water[1] + and not self:get_action() then + creatura.action_walk(self, water[1]) + else + return true + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:follow_player", function(self, player, force) + local function func(self) + local player_alive, line_of_sight, tpos = self:get_target(player) + -- Return if player is dead, not holding food, or behavior isn't forced + if not player_alive + or (not self:follow_wielded_item(player) + and not force) then + return true + end + local pos = self.object:get_pos() + local dist = vec_dist(pos, tpos) + if dist > self.tracking_range then + return true + end + if not self:get_action() then + if dist > self:get_hitbox(self)[4] + 1.5 then + creatura.action_walk(self, tpos, 6, "creatura:pathfind") + else + creatura.action_idle(self, 0.1, "stand") + end + end + self.head_tracking = player + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:sporadic_flee", function(self) + local timer = 18 + self:clear_action() + if group then + local mobs_in_group = animalia.get_group(self) + if #mobs_in_group > 0 then + for i = 1, #mobs_in_group do + local mob = mobs_in_group[i] + animalia.bh_flee(mob:get_luaentity()) + end + end + end + local function func(self) + local pos = self.object:get_pos() + local random_goal = { + x = pos.x + random(-4, 4), + y = pos.y, + z = pos.z + random(-4, 4) + } + if not self:get_action() then + random_goal = get_ground_level(random_goal, 1) + local node = minetest.get_node(random_goal) + if minetest.registered_nodes[node.name].drawtype == "liquid" + or minetest.registered_nodes[node.name].walkable then + return + end + if self.lasso_pos + and vec_dist(pos, self.lasso_pos) > 10 then + random_goal = self.lasso_pos + end + self._movement_data.speed = self.speed * 1.5 + creatura.action_walk(self, random_goal, 4, "creatura:obstacle_avoidance", 1.5) + end + timer = timer - self.dtime + if timer <= 0 then + return true + end + end + self:set_utility(func) +end) + +-- Mob Interaction + +creatura.register_utility("animalia:mammal_breed", function(self) + local mate = animalia.get_nearby_mate(self, self.name) + if not mate then self.breeding = false return end + local breeding_time = 0 + local function func(self) + if not creatura.is_alive(mate) then + return true + end + local pos = self:get_center_pos() + local tpos = mate:get_pos() + local dist = vec_dist(pos, tpos) - abs(self:get_hitbox(self)[4]) + if dist < 1.75 then + breeding_time = breeding_time + self.dtime + end + if breeding_time >= 2 then + if self.gender == "female" then + for i = 1, self.birth_count or 1 do + local object = minetest.add_entity(pos, self.name) + local ent = object:get_luaentity() + ent.growth_scale = 0.7 + animalia.initialize_api(ent) + animalia.protect_from_despawn(ent) + end + end + self.breeding = false + self.breeding_cooldown = 300 + self:memorize("breeding", self.breeding) + self:memorize("breeding_time", self.breeding_time) + self:memorize("breeding_cooldown", self.breeding_cooldown) + local minp = vector.subtract(pos, 1) + local maxp = vec_add(pos, 1) + animalia.particle_spawner(pos, "heart.png", "float", minp, maxp) + return true + end + if not self:get_action() then + creatura.action_walk(self, tpos) + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:horse_breed", function(self) + local mate = animalia.get_nearby_mate(self, self.name) + if not mate then self.breeding = false return end + local breeding_time = 0 + local function func(self) + if not creatura.is_alive(mate) then + return true + end + local pos = self:get_center_pos() + local tpos = mate:get_pos() + local dist = vec_dist(pos, tpos) - abs(self:get_hitbox(self)[4]) + if dist < 1.75 then + breeding_time = breeding_time + self.dtime + end + if breeding_time >= 2 then + if self.gender == "female" then + local object = minetest.add_entity(pos, self.name) + object:get_luaentity().growth_scale = 0.7 + local ent = object:get_luaentity() + local tex_no = self.texture_no + if random(2) < 2 then + tex_no = mate:get_luaentity().texture_no + end + ent:memorize("texture_no", tex_no) + ent:memorize("speed", random(mate:get_luaentity().speed, self.speed)) + ent:memorize("jump_power", random(mate:get_luaentity().jump_power, self.jump_power)) + ent:memorize("max_hp", random(mate:get_luaentity().max_hp, self.max_hp)) + ent.speed = ent:recall("speed") + ent.jump_power = ent:recall("jump_power") + ent.max_hp = ent:recall("max_hp") + animalia.initialize_api(ent) + animalia.protect_from_despawn(ent) + end + self.breeding = false + self.breeding_cooldown = 300 + self:memorize("breeding", self.breeding) + self:memorize("breeding_time", self.breeding_time) + self:memorize("breeding_cooldown", self.breeding_cooldown) + local minp = vector.subtract(pos, 1) + local maxp = vec_add(pos, 1) + animalia.particle_spawner(pos, "heart.png", "float", minp, maxp) + return true + end + if not self:get_action() then + creatura.action_walk(self, tpos) + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:bird_breed", function(self) + local mate = animalia.get_nearby_mate(self, self.name) + if not mate then self.breeding = false return end + local breeding_time = 0 + local function func(self) + if not creatura.is_alive(mate) then + return true + end + local pos = self:get_center_pos() + local tpos = mate:get_pos() + local dist = vec_dist(pos, tpos) - abs(self:get_hitbox(self)[4]) + if dist < 1.75 then + breeding_time = breeding_time + self.dtime + end + if breeding_time >= 2 then + if self.gender == "female" then + minetest.add_particlespawner({ + amount = 6, + time = 0.25, + minpos = {x = pos.x - 7/16, y = pos.y - 5/16, z = pos.z - 7/16}, + maxpos = {x = pos.x + 7/16, y = pos.y - 5/16, z = pos.z + 7/16}, + minvel = vector.new(-1, 2, -1), + maxvel = vector.new(1, 5, 1), + minacc = vector.new(0, -9.81, 0), + maxacc = vector.new(0, -9.81, 0), + collisiondetection = true, + texture = "animalia_egg_fragment.png", + }) + for i = 1, self.birth_count or 1 do + local object = minetest.add_entity(pos, self.name) + local ent = object:get_luaentity() + ent.growth_scale = 0.7 + animalia.initialize_api(ent) + animalia.protect_from_despawn(ent) + end + end + self.breeding = false + self.breeding_cooldown = 300 + self:memorize("breeding", self.breeding) + self:memorize("breeding_time", self.breeding_time) + self:memorize("breeding_cooldown", self.breeding_cooldown) + local minp = vector.subtract(pos, 1) + local maxp = vec_add(pos, 1) + animalia.particle_spawner(pos, "heart.png", "float", minp, maxp) + return true + end + if not self:get_action() then + creatura.action_walk(self, tpos) + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:attack", function(self, target, group) + local punch_init = false + if group then + local allies = creatura.get_nearby_entities(self, self.name) + if #allies > 0 then + for i = 1, #allies do + allies[i]:get_luaentity():initiate_utility("animalia:attack", allies[i]:get_luaentity(), target) + allies[i]:get_luaentity():set_utility_score(1) + end + end + end + local function func(self) + local target_alive, line_of_sight, tpos = self:get_target(target) + if not target_alive then + return true + end + local pos = self.object:get_pos() + local dist = vec_dist(pos, tpos) + if not self:get_action() then + if punch_init then return true end + --if dist > self:get_hitbox(self)[4] then + creatura.action_walk(self, tpos, 6, "creatura:theta_pathfind", 1) + --end + end + if dist <= self:get_hitbox(self)[4] + 1 + and not punch_init then + animalia.action_punch(self, target) + punch_init = true + end + end + self:set_utility(func) +end) + +-- Flight + +creatura.register_utility("animalia:aerial_flock", function(self, scale) + local range = ceil(8 * scale) + local function func(self) + if self:timer(2) + and self.stamina <= 0 then + local boids = creatura.get_boid_members(self.object:get_pos(), 6, self.name) + if #boids > 1 then + for i = 1, #boids do + local boid = boids[i] + local ent = boid:get_luaentity() + ent.stamina = ent:memorize("stamina", 0) + ent.is_landed = ent:memorize("is_landed", true) + end + end + end + local dist2floor = creatura.sensor_floor(self, 2, true) + local dist2ceil = creatura.sensor_ceil(self, 2, true) + if self.in_liquid then + dist2floor = 0 + dist2ceil = 2 + end + if dist2floor < 2 + and dist2ceil < 2 then + self.is_landed = true + return true + end + if not self:get_action() + or (dist2floor < 2 + or dist2ceil < 2) then + local pos = self.object:get_pos() + local pos2 = self:get_wander_pos_3d(1, range) + if dist2ceil < 2 then + pos2.y = pos.y - 1 + end + if dist2floor < 2 then + pos2.y = pos.y + 1 + end + if self.in_liquid then + pos2.y = pos.y + 2 + end + animalia.action_boid_move(self, pos2, 2) + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:aerial_swarm", function(self, scale) + local function func(self) + if self:timer(2) + and self.stamina <= 0 then + local boids = creatura.get_boid_members(self.object:get_pos(), 6, self.name) + if #boids > 1 then + for i = 1, #boids do + local boid = boids[i] + local ent = boid:get_luaentity() + ent.stamina = ent:memorize("stamina", 0) + ent.is_landed = ent:memorize("is_landed", true) + end + end + end + local dist2floor = creatura.sensor_floor(self, 2, true) + local dist2ceil = creatura.sensor_ceil(self, 2, true) + if self.in_liquid then + dist2floor = 0 + dist2ceil = 2 + end + if not self:get_action() + or (dist2floor < 2 + or dist2ceil < 2) then + local pos = self.object:get_pos() + local pos2 = self:get_wander_pos_3d(1, 3) + if dist2floor < 2 then + pos2.y = pos.y + 1 + end + if dist2ceil < 2 then + pos2.y = pos.y - 1 + end + animalia.action_boid_move(self, pos2, 2) + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:land", function(self, scale) + local function func(self) + if self.touching_ground then return true end + local _, node = creatura.sensor_floor(self, 3, true) + if node and is_liquid[node.name] then self.is_landed = false return true end + scale = scale or 1 + local width = self.width + local pos = self.object:get_pos() + local pos2 + if self:timer(1) then + local offset = random(2 * scale, 3 * scale) + if random(2) < 2 then + offset = offset * -1 + end + pos2 = { + x = pos.x + offset, + y = pos.y, + z = pos.z + offset + } + pos2.y = pos2.y - (3 * scale) + end + if not self:get_action() + and pos2 then + self:animate("fly") + creatura.action_walk(self, pos2, 2, "animalia:fly_path", 1) + end + end + self:set_utility(func) +end) + +-- Swimming + +creatura.register_utility("animalia:schooling", function(self) + local pos = self.object:get_pos() + local water = minetest.find_nodes_in_area(vector.subtract(pos, 5), vector.add(pos, 5), {"group:water"}) + local function func(self) + if not self:get_action() then + if #water < 1 then return true end + local iter = random(#water) + local pos2 = water[iter] + table.remove(water, iter) + animalia.action_boid_move(self, pos2, 2, "animalia:swim_obstacle_avoidance") + end + end + self:set_utility(func) +end) + +-- Resist Fall + +creatura.register_utility("animalia:resist_fall", function(self) + local function func(self) + if not self:get_action() then + animalia.action_fall(self) + end + if self.touching_ground + or self.in_liquid then + creatura.action_idle(self, "stand") + self:set_gravity(-9.8) + return true + end + end + self:set_utility(func) +end) + +-- Die + +creatura.register_utility("animalia:die", function(self) + local timer = 1.5 + local init = false + local function func(self) + if not init then + self:play_sound("death") + creatura.action_fallover(self) + init = true + end + timer = timer - self.dtime + if timer <= 0 then + local pos = self.object:get_pos() + minetest.add_particlespawner({ + amount = 8, + time = 0.25, + minpos = {x = pos.x - 0.1, y = pos.y, z = pos.z - 0.1}, + maxpos = {x = pos.x + 0.1, y = pos.y + 0.1, z = pos.z + 0.1}, + minacc = {x = 0, y = 2, z = 0}, + maxacc = {x = 0, y = 3, z = 0}, + minvel = {x = random(-1, 1), y = -0.25, z = random(-1, 1)}, + maxvel = {x = random(-2, 2), y = -0.25, z = random(-2, 2)}, + minexptime = 0.75, + maxexptime = 1, + minsize = 4, + maxsize = 4, + texture = "creatura_smoke_particle.png", + animation = { + type = 'vertical_frames', + aspect_w = 4, + aspect_h = 4, + length = 1, + }, + glow = 1 + }) + creatura.drop_items(self) + self.object:remove() + end + end + self:set_utility(func) +end) + +-- Cat Exclusive Behaviors + +creatura.register_utility("animalia:find_and_break_glass_vessels", function(self) + local timer = 12 + local pos = self.object:get_pos() + local pos2 = nil + local nodes = minetest.find_nodes_in_area( + vector.subtract(pos, 8), + vec_add(pos, 8), + {"vessels:glass_bottle", "vessels:drinking_glass"} + ) + if #nodes > 0 then + pos2 = nodes[1] + end + local func = function(self) + if not pos2 then + return + end + pos = self.object:get_pos() + if not self:get_action() then + creatura.action_walk(self, pos2, 6, "pathfind") + end + if vector.distance(pos, pos2) <= 0.5 then + creatura.action_idle(self, 0.7, "smack") + minetest.remove_node(pos2) + minetest.add_item(pos2, "vessels:glass_fragments") + if minetest.get_node(pos2).name == "air" then + return true + end + end + timer = timer - self.dtime + if timer < 0 then return true end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:walk_ahead_of_player", function(self, player) + if not player then return end + local timer = 8 + local func = function(self) + if not creatura.is_alive(player) then + return true + end + local pos = self.object:get_pos() + local tpos = player:get_pos() + local dir = player:get_look_dir() + tpos.x = tpos.x + dir.x + tpos.z = tpos.z + dir.z + self.status = self:memorize("status", "following") + local dist = vec_dist(pos, tpos) + if dist > self.view_range then + self.status = self:memorize("status", "") + return true + end + if not self:get_action() then + if vec_dist(pos, tpos) > self.width + 0.5 then + creatura.action_walk(self, tpos, 6, "pathfind", 0.75) + else + creatura.action_idle(self, 0.1, "stand") + end + end + timer = timer - self.dtime + if timer < 0 then self.status = self:memorize("status", "") return true end + end + self:set_utility(func) +end) + +-- Bat Exclusive Behaviors + +creatura.register_utility("animalia:return_to_home", function(self) + local init = false + local tpos = nil + local function func(self) + if not self.home_position then return true end + local pos = self.object:get_pos() + local pos2 = self.home_position + if not self:get_action() then + creatura.action_walk(self, vec_raise(pos2, -1), 6, "animalia:fly_path", 1) + end + local dist = vec_dist(pos, pos2) + if dist < 2 then + if is_solid[minetest.get_node(vec_raise(pos, 1)).name] then + creatura.action_idle(self, 1, "latch") + self:set_gravity(9.8) + self.object:set_velocity({x = 0, y = 0, z = 0}) + end + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:find_home", function(self) + local init = false + local tpos = nil + local pos = self.object:get_pos() + local range = self.tracking_range + local ceiling = get_ceiling_positions(pos, range / 2) + local iter = 1 + local function func(self) + if not ceiling[1] then + return true + else + iter = random(#ceiling) + end + pos = self.object:get_pos() + if not self:get_action() then + local pos2 = get_wander_pos_3d(self, range) + local dist2floor = creatura.sensor_floor(self, 5, true) + local dist2ceil = creatura.sensor_ceil(self, 5, true) + if dist2floor < 4 then + pos2.y = pos.y + 2 + elseif dist2ceil < 4 then + pos2.y = pos.y - 1 + end + animalia.action_boid_move(self, pos2, 2) + end + if ceiling[iter] then + local pos2 = { + x = ceiling[iter].x, + y = ceiling[iter].y - 1, + z = ceiling[iter].z + } + local line_of_sight = fast_ray_sight(pos, pos2) + if line_of_sight then + self.home_position = self:memorize("home_position", ceiling[iter]) + return true + end + end + if self:timer(1) then + iter = iter + 1 + if iter > #ceiling then + return true + end + end + end + self:set_utility(func) +end) + +-- Horse Exclusive Behaviors + +creatura.register_utility("animalia:horse_breaking", function(self) + local timer = 18 + self:clear_action() + local function func(self) + if not self:get_action() then + animalia.action_horse_spin(self, random(4, 6), "stand") + end + timer = timer - self.dtime + if timer <= 0 then + return true + end + end + self:set_utility(func) +end) + +-- Tamed Animal Orders + +creatura.register_utility("animalia:sit", function(self) + local function func(self) + if self.order ~= "sit" then + return true + end + if not self:get_action() then + creatura.action_idle(self, 0.1, "sit") + end + end + self:set_utility(func) +end) + +creatura.register_utility("animalia:mount", function(self, player) + local function func(self) + if not creatura.is_alive(player) then + return true + end + local anim = "stand" + local control = player:get_player_control() + local speed_factor = 0 + local vel = self.object:get_velocity() + if control.up then + speed_factor = 1 + if control.aux1 then + speed_factor = 1.5 + end + end + if control.jump + and self.touching_ground then + self.object:add_velocity({ + x = 0, + y = self.jump_power + (abs(self._movement_data.gravity) * 0.33), + z = 0 + }) + elseif not self.touching_ground then + speed_factor = speed_factor * 0.5 + end + local total_speed = vector.length(vel) + if total_speed > 0.2 then + anim = "walk" + if control.aux1 then + anim = "run" + end + if not self.touching_ground + and not self.in_liquid + and vel.y > 0 then + anim = "rear_constant" + end + end + self:turn_to(player:get_look_horizontal()) + self:set_forward_velocity(self.speed * speed_factor) + self:animate(anim) + if control.sneak + or not self.rider then + animalia.mount(self, player) + return true + end + end + self:set_utility(func) +end) \ No newline at end of file