--------------- -- Behaviors -- --------------- -- Math -- local abs = math.abs local atan2 = math.atan2 local cos = math.cos local floor = math.floor local pi = math.pi local sin = math.sin local rad = math.rad local random = math.random 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 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 -- Vector -- local vec_dir = vector.direction local vec_dist = vector.distance local vec_divide = vector.divide local vec_len = vector.length local vec_normal = vector.normalize local vec_round = vector.round local vec_sub = vector.subtract local vec_add = vector.add local vec_multi = vector.multiply local dir2yaw = minetest.dir_to_yaw local yaw2dir = minetest.yaw_to_dir ----------------- -- Local Tools -- ----------------- local farming_enabled = minetest.get_modpath("farming") and farming.registered_plants if farming_enabled then minetest.register_on_mods_loaded(function() for name, def in pairs(minetest.registered_nodes) do local item_string = name:sub(1, #name - 2) local item_name = item_string:split(":")[2] if farming.registered_plants[item_string] or farming.registered_plants[item_name] then def.groups.crop = 2 end minetest.register_node(":" .. name, def) end end) end local animate_player = {} if minetest.get_modpath("default") and minetest.get_modpath("player_api") then animate_player = player_api.set_animation elseif minetest.get_modpath("mcl_player") then animate_player = mcl_player.set_animation end 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 collide, col_pos = get_collision(self) if collide then local vel = self.object:get_velocity() local ahead = vec_add(pos, vec_normal(self.object:get_velocity())) local avoidance_force = vector.subtract(ahead, col_pos) avoidance_force.y = 0 local vel_len = vec_len(vel) 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 add_break_particle(pos) pos = vec_round(pos) local def = creatura.get_node_def(pos) local texture = (def.tiles and def.tiles[1]) or def.inventory_image texture = texture .. "^[resize:8x8" minetest.add_particlespawner({ amount = 6, time = 0.1, minpos = { x = pos.x, y = pos.y - 0.49, z = pos.z }, maxpos = { x = pos.x, y = pos.y - 0.49, z = pos.z }, 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.5, minsize = 1, maxsize = 2, collisiondetection = true, vertical = false, texture = texture, }) end local function add_eat_particle(self, item_name) local pos, yaw = self.object:get_pos(), self.object:get_yaw() if not pos then return end local head = self.head_data local offset_h = (head and head.pivot_h) or self.width local offset_v = (head and head.pivot_v) or self.height local head_pos = { x = pos.x + sin(yaw) * -offset_h, y = pos.y + offset_v, z = pos.z + cos(yaw) * offset_h } local def = minetest.registered_items[item_name] local image = def.inventory_image if def.tiles then image = def.tiles[1].name or def.tiles[1] end if image then local crop = "^[sheet:4x4:" .. random(4) .. "," .. random(4) minetest.add_particlespawner({ pos = head_pos, time = 0.5, amount = 12, collisiondetection = true, collision_removal = true, vel = {min = {x = -1, y = 1, z = -1}, max = {x = 1, y = 2, z = 1}}, acc = {x = 0, y = -9.8, z = 0}, size = {min = 1, max = 2}, texture = image .. crop }) end end local function get_group_positions(self) local objects = creatura.get_nearby_objects(self, self.name) local group = {} for _, object in ipairs(objects) do local obj_pos = object and object:get_pos() if obj_pos then table.insert(group, obj_pos) end end return group end local function reset_attack_vals(self) self.punch_cooldown = 0 self.target = nil end -------------- -- Movement -- -------------- creatura.register_movement_method("animalia:fly_simple", function(self) local box = clamp(self.width, 0.5, 1.5) self:set_gravity(0) local function func(_self, goal, speed_factor) local pos = _self.object:get_pos() if not pos then return end -- Return true when goal is reached if vec_dist(pos, goal) < box * 1.33 then _self:halt() return true end -- Get movement direction local goal_dir = vec_dir(pos, goal) 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 then _self:set_forward_velocity(speed) else _self:set_forward_velocity(speed * 0.33) end self:set_vertical_velocity(speed * goal_dir.y) _self:turn_to(goal_yaw, turn_rate) if _self.touching_ground or _self.in_liquid then _self.object:add_velocity({x = 0, y = 2, z = 0}) end end return func end) creatura.register_movement_method("animalia:fly_obstacle_avoidance", function(self) local steer_to local steer_timer = 0.25 local init_dist self:set_gravity(0) local function func(_self, goal, speed_factor) local pos = _self.object:get_pos() if not pos then return end local dist = vec_dist(pos, goal) init_dist = dist if dist < clamp(self.width, 0.5, 1) + 1.5 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, 4, true)) or steer_to local speed = abs(_self.speed or 2) * speed_factor or 0.5 local turn_rate = abs(_self.turn_rate or 5) local dist_weight = (init_dist - dist) / init_dist -- Apply Movement local dir = (steer_to or vec_dir(pos, goal)) speed = speed - (speed * 0.75) * dist_weight turn_rate = turn_rate + turn_rate * dist_weight _self:set_forward_velocity(speed) _self:set_vertical_velocity(speed * dir.y) _self:turn_to(dir2yaw(dir), turn_rate) end return func end) creatura.register_movement_method("animalia:swim_simple", function(self) local box = clamp(self.width, 0.5, 1.5) self:set_gravity(0) local function func(_self, goal, speed_factor) local pos = _self.object:get_pos() if not pos then return end -- Return true when goal is reached if vec_dist(pos, goal) < box * 1.33 then _self:halt() return true end -- Get movement direction local goal_dir = vec_dir(pos, goal) 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 then _self:set_forward_velocity(speed) else _self:set_forward_velocity(speed * 0.66) end self:set_vertical_velocity(speed * goal_dir.y) _self:turn_to(goal_yaw, turn_rate) end return func end) creatura.register_movement_method("animalia:swim_obstacle_avoidance", function(self) local box = clamp(self.width, 0.5, 1.5) local steer_to local steer_timer = 0.25 self:set_gravity(0) local function func(_self, goal, speed_factor) local pos = _self.object:get_pos() if not pos then return end -- Return true when goal is reached if vec_dist(pos, goal) < box * 1.33 then _self:halt() return true end steer_timer = steer_timer - self.dtime if steer_timer <= 0 then steer_to = get_avoidance_dir(_self) end -- Get movement direction local goal_dir = vec_dir(pos, goal) if steer_to then steer_to.y = goal_dir.y goal_dir = steer_to end 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:set_vertical_velocity(speed * goal_dir.y) _self:turn_to(goal_yaw, turn_rate) end return func end) ------------- -- Actions -- ------------- function animalia.action_pursue(self, target, timeout, method, speed_factor, anim) local timer = timeout or 4 local goal local function func(_self) local target_alive, line_of_sight, tgt_pos = _self:get_target(target) if not target_alive then return true end goal = goal or tgt_pos 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(goal) end if line_of_sight and vec_dist(goal, tgt_pos) > 3 then goal = tgt_pos end if timer <= 0 or not safe or _self:move_to(goal, method or "creatura:obstacle_avoidance", speed_factor or 0.5) then return true end end self:set_action(func) end function animalia.action_pursue_glide(self, target, timeout, method, speed_factor, anim) local timer = timeout or 4 local goal local speed_x = speed_factor local function func(_self) local target_alive, line_of_sight, tgt_pos = _self:get_target(target) if not target_alive then return true end goal = goal or tgt_pos timer = timer - _self.dtime self:animate(anim or "walk") if line_of_sight and vec_dist(goal, tgt_pos) > 3 then goal = tgt_pos end local vel = self.object:get_velocity() if vel.y < 0 and speed_x < speed_factor + 0.5 then speed_x = speed_x + self.dtime * 0.5 end if vel.y >= 0 and speed_x > speed_factor then speed_x = speed_x - self.dtime * 0.25 end if timer <= 0 or _self:move_to(goal, method or "animalia:fly_obstacle_avoidance", speed_x) then return true end end self:set_action(func) end function animalia.action_flight_attack(self, target, timeout) local anim = self.animations["fly_punch"] local anim_len = (anim.range.y - anim.range.x) / anim.speed local anim_time = 0 local timer = timeout or 12 local cooldown = 0 local speed_x = 0.5 local goal local function func(_self) local pos = _self.stand_pos if timer <= 0 then return true end local target_alive, los, tgt_pos = _self:get_target(target) if not target_alive then return true end local dist = vec_dist(pos, tgt_pos) if dist > 32 then return true end local vel = self.object:get_velocity() if vel.y < 0 and speed_x < 1 then speed_x = speed_x + self.dtime * 0.5 end if vel.y >= 0 and speed_x > 0.5 then speed_x = speed_x - self.dtime end if anim_time > 0 then _self:animate("fly_punch") anim_time = anim_time - _self.dtime else if speed_x > 0.6 then _self:animate("glide") else _self:animate("fly") end end if cooldown > 0 then goal = goal or _self:get_wander_pos_3d(3, 6, nil, 1) cooldown = cooldown - _self.dtime else goal = nil cooldown = 0 end if goal and _self:move_to(goal, "animalia:fly_obstacle_avoidance", speed_x) then goal = nil end if not goal and _self:move_to(tgt_pos, "animalia:fly_obstacle_avoidance", speed_x) then if dist < _self.width + 1 then _self:punch_target(target) cooldown = timeout / 3 anim_time = anim_len end end timer = timer - _self.dtime end self:set_action(func) end function animalia.action_move_flock(self, pos2, timeout, method, speed_factor, anim, boid_steer) local old_boids = (self._movement_data and self._movement_data.boids) or {} local boids = (#old_boids > 2 and old_boids) or creatura.get_boid_members(self.object:get_pos(), 12, self.name) local timer = timeout or 4 local boid_pos2 local function func(_self) local pos = self.object:get_pos() if not pos then return end -- Tick down timer timer = timer - _self.dtime -- Check if goal is safe local safe = true local max_fall = (_self.max_fall or 0) > 0 and _self.max_fall if max_fall then safe = _self:is_pos_safe(pos2) end -- Boid calculation if #boids > 2 then local boid_yaw, boid_pitch = creatura.get_boid_angle(self, boids, 12) if boid_yaw then local dir2pos = vec_dir(pos, pos2) local yaw2pos = minetest.dir_to_yaw(dir2pos) boid_yaw = interp_rad(boid_yaw, yaw2pos, 0.3) local boid_dir = minetest.yaw_to_dir(boid_yaw) boid_dir.y = boid_pitch boid_pos2 = vec_add(pos, vec_multi(boid_dir, 4)) if max_fall then boid_pos2 = creatura.get_ground_level(boid_pos2, 2) end end end local steer_to = boid_steer and get_avoidance_dir(self) if steer_to then boid_pos2 = { x = pos.x + steer_to.x * 2, y = pos2.y, z = pos.z + steer_to.z * 2 } end -- Main movement if timer <= 0 or not safe or _self:move_to(boid_pos2 or pos2, method or "creatura:obstacle_avoidance", speed_factor or 0.5) then return true end self:animate(anim or "walk") end self:set_action(func) end function animalia.action_move_boid(self, pos2, timeout, method, speed_factor, anim, water) local timer = timeout or 4 local goal local function func(_self) local pos, vel = self.object:get_pos(), self.object:get_velocity() if not pos then return end local move_data = self._movement_data or {} -- Tick down timer timer = timer - _self.dtime -- Check if goal is safe local safe = true local max_fall = (_self.max_fall or 0) > 0 and _self.max_fall if max_fall then safe = _self:is_pos_safe(goal) end -- Boid calculation local boid_dir, boids = move_data.boid_dir or creatura.get_boid_dir(self) if boid_dir then local heading = vec_dir(pos, pos2) boid_dir.y = boid_dir.y + heading.y / 2 goal = vec_add(pos, vec_multi(boid_dir, self.width + 1)) if max_fall then goal = creatura.get_ground_level(goal, 2) end if boids then for _, obj in ipairs(boids) do local ent = obj and obj:get_luaentity() if ent then ent._movement_data.boid_dir = boid_dir end end end else goal = pos2 end local steer_dir = creatura.get_context_steering(self, goal, 1, water) goal = (steer_dir and vec_add(pos, steer_dir)) or goal -- Main movement if timer <= 0 or not safe or _self:move_to(goal, method or "creatura:obstacle_avoidance", speed_factor or 0.5) then return true end self:animate(anim or "walk") self._movement_data.boid_dir = nil end self:set_action(func) end function animalia.action_float(self, time, anim) local timer = time local function func(_self) _self:set_gravity(-0.14) _self:halt() _self:animate(anim or "foat") timer = timer - _self.dtime if timer <= 0 then return true end end self:set_action(func) end function animalia.action_cling(self, time) local timer = time local pos = self.object:get_pos() if not pos then return end pos.y = pos.y + 1 if not creatura.get_node_def(pos).walkable then self:forget("home_position") self.home_position = nil return end local function func(_self) _self:set_gravity(0) _self:halt() _self:set_vertical_velocity(1) _self:set_forward_velocity(0) _self:animate("cling") timer = timer - _self.dtime if timer <= 0 then return true end end self:set_action(func) end function animalia.action_punch(self, target) local jump_init = false local timeout = 2 local function func(_self) local tgt_alive, _, tgt_pos = _self:get_target(target) if not tgt_alive then return true end local pos = _self.object:get_pos() if not pos then return end local dir = vec_dir(pos, tgt_pos) if not jump_init and _self.touching_ground then _self.object:add_velocity({x = dir.x * 3, y = 2, z = dir.z * 3}) jump_init = true end timeout = timeout - _self.dtime if timeout <= 0 then return true end local dist = vec_dist(pos, tgt_pos) if dist < _self.width + 1 then _self:punch_target(target) local knockback = minetest.calculate_knockback( target, self.object, 1.0, {damage_groups = {fleshy = self.damage}}, dir, 2.0, self.damage ) target:add_velocity({x = dir.x * knockback, y = dir.y * knockback, z = dir.z * knockback}) return true end end self:set_action(func) end function animalia.action_punch_aoe(self, target) local punch_init = false local anim = self.animations["punch_aoe"] local anim_len = (anim.range.y - anim.range.x) / anim.speed local timeout = anim_len local function func(_self) local tgt_alive, _, tgt_pos = _self:get_target(target) if not tgt_alive then return true end local pos = _self.object:get_pos() if not pos then return end _self:halt() _self:animate("punch_aoe") local dist = vec_dist(pos, tgt_pos) timeout = timeout - _self.dtime if not punch_init and dist < _self.width + 1 and timeout < anim_len * 0.5 then _self:punch_target(target) punch_init = true end if timeout <= 0 then _self:animate("stand") return true end end self:set_action(func) end --------------- -- Utilities -- --------------- 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() if not pos then return end 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) 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() if not pos then return end 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 vec_dist(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) -- Wandering creatura.register_utility("animalia:wander", function(self) local move_chance = self.move_chance or 5 local idle_duration = self.idle_time or 4 local center = self.object:get_pos() if not center then return end local move = self.wander_action or creatura.action_move local function func(_self) if not _self:get_action() then local pos2 = _self:get_wander_pos(2, 3) if random(move_chance) < 2 and vec_dist(pos2, center) < _self.tracking_range * 0.5 then move(_self, pos2, 2) else creatura.action_idle(_self, random(idle_duration), "stand") end end end self:set_utility(func) end) creatura.register_utility("animalia:wander_group", function(self) local move_chance = self.move_chance or 3 local idle_duration = self.idle_time or 3 local center = self.object:get_pos() if not center then return end local group_tick = 500 local move = self.wander_action or animalia.action_move_boid local function func(_self) group_tick = group_tick - 1 if group_tick <= 0 then local pos = _self.object:get_pos() if not pos then return end local grp_pos = get_group_positions(_self) center = animalia.get_average_pos(grp_pos) or pos group_tick = 500 end if not _self:get_action() then local pos2 = _self:get_wander_pos(2, 3) if random(move_chance) < 2 and vec_dist(pos2, center) < _self.tracking_range * 0.5 then move(_self, pos2, 2, "creatura:obstacle_avoidance", 0.5, "walk", true) else creatura.action_idle(_self, random(idle_duration)) end end end self:set_utility(func) end) creatura.register_utility("animalia:wander_skittish", function(self) local move_chance = self.move_chance or 3 local idle_duration = self.idle_time or 3 local center = self.object:get_pos() if not center then return end local plyr_tick = 500 local move_dir local move = self.wander_action or creatura.action_move local function func(_self) plyr_tick = plyr_tick - 1 if plyr_tick <= 0 then local pos = _self.object:get_pos() if not pos then return true end local plyr = creatura.get_nearby_player(_self) local plyr_alive, los, plyr_pos = _self:get_target(plyr) if plyr_alive and los then move_dir = vec_dir(plyr_pos, pos) end plyr_tick = 500 end if not _self:get_action() then local pos2 = _self:get_wander_pos(2, 3, move_dir) if random(move_chance) < 2 and vec_dist(pos2, center) < _self.tracking_range * 0.5 then move(_self, pos2, 2) move_dir = nil else creatura.action_idle(_self, random(idle_duration)) end end end self:set_utility(func) end) creatura.register_utility("animalia:aerial_wander", function(self) local center = self.object:get_pos() if not center then return end local height_tick = 0 local move = self.wander_action or creatura.action_move local function func(_self) local pos = self.object:get_pos() if not pos then return end height_tick = height_tick - 1 if height_tick <= 0 then local dist2floor = creatura.sensor_floor(self, 2, true) center.y = center.y + (2 - dist2floor) height_tick = 30 end if not _self:get_action() then local move_dir = (vec_dist(pos, center) > 8 and vec_dir(pos, center)) or nil local pos2 = _self:get_wander_pos_3d(2, 5, move_dir) move(_self, pos2, 3, "animalia:fly_simple", 1, "fly", true) end end self:set_utility(func) end) creatura.register_utility("animalia:fly_to_roost", function(self) local home = self.home_position local roost = self.roost_action or creatura.action_idle local is_home = self.is_roost or function(pos, home_pos) if abs(pos.x - home_pos.x) < 0.5 and abs(pos.z - home_pos.z) < 0.5 and abs(pos.y - home_pos.y) < 0.75 then return true end return false end local function func(_self) local pos = _self.object:get_pos() if not pos then return end if not home then return true end if not _self:get_action() then if is_home(pos, home) then roost(_self, 1, "stand") return end creatura.action_move(_self, home, 3, "animalia:fly_obstacle_avoidance", 1, "fly") end end self:set_utility(func) end) creatura.register_utility("animalia:fly_to_land", function(self) local landed = false local function func(_self) if not _self:get_action() then if landed then return true end if _self.touching_ground then creatura.action_idle(_self, 0.5, "stand") landed = true else local pos2 = _self:get_wander_pos_3d(3, 6) if pos2 then local dist2floor = creatura.sensor_floor(_self, 10, true) pos2.y = pos2.y - dist2floor creatura.action_move(_self, pos2, 3, "animalia:fly_simple", 0.6, "fly") end end end end self:set_utility(func) end) creatura.register_utility("animalia:aquatic_wander_school", function(self) local center = self.object:get_pos() if not center then return end local center_tick = 0 local water_nodes = minetest.find_nodes_in_area(vec_sub(center, 4), vec_add(center, 4), {"group:water"}) local function func(_self) if #water_nodes < 1 then return true end if #water_nodes < 10 then center_tick = center_tick - 1 if center_tick <= 0 then center_tick = 30 end center = self.object:get_pos() if not center then return end water_nodes = minetest.find_nodes_in_area(vec_sub(center, 4), vec_add(center, 4), {"group:water"}) end if not _self:get_action() then animalia.action_move_boid(_self, water_nodes[random(#water_nodes)], 3, "animalia:swim_simple", 1, "swim") end end self:set_utility(func) end) creatura.register_utility("animalia:aquatic_wander", function(self) local center = self.object:get_pos() if not center then return end local center_tick = 0 local move_chance = 3 local idle_duration = 3 local water_nodes = minetest.find_nodes_in_area(vec_sub(center, 4), vec_add(center, 4), {"group:water"}) local function func(_self) if #water_nodes < 1 then return true end if #water_nodes < 10 then center_tick = center_tick - 1 if center_tick <= 0 then center_tick = 30 end center = self.object:get_pos() if not center then return end water_nodes = minetest.find_nodes_in_area(vec_sub(center, 4), vec_add(center, 4), {"group:water"}) end if not _self:get_action() then if random(move_chance) < 2 then creatura.action_move(_self, water_nodes[random(#water_nodes)], 3, "animalia:swim_obstacle_avoidance", 0.5, "swim") else animalia.action_float(_self, random(idle_duration), "float") end end end self:set_utility(func) end) -- Environment Interaction creatura.register_utility("animalia:eat_turf", function(self) local action_init = false local function func(_self) local pos = _self.object:get_pos() if not pos then return end local yaw = _self.object:get_yaw() local dir = vec_normal(yaw2dir(yaw)) local turf_pos = { x = pos.x + dir.x * _self.width, y = pos.y - 0.5, z = pos.z + dir.z * _self.width } if not _self:get_action() then if action_init then return true end for name, sub_name in pairs(_self.consumable_nodes) do if minetest.get_node(turf_pos).name == name then add_break_particle(turf_pos) minetest.set_node(turf_pos, {name = sub_name}) _self.collected = _self:memorize("collected", false) creatura.action_idle(_self, 1, "eat") action_init = true break end end if not action_init then return true end end end self:set_utility(func) end) creatura.register_utility("animalia:eat_bug", function(self, bug) local timer = 0.2 local action_init = false local function func(_self) local pos = _self.object:get_pos() if not pos then return end if not bug then return true end local dist = vec_dist(pos, bug) local dir = vec_dir(pos, bug) local frame = floor(dist * 10) if not _self:get_action() then if dist > 1 then local pos2 = vec_add(bug, vec_multi(vec_normal(vec_dir(bug, pos)), 0.25)) creatura.action_move(_self, pos2, 1) else animalia.move_head(_self, dir2yaw(dir), dir.y) creatura.action_idle(_self, 0.1, "tongue_" .. frame) action_init = true end end if action_init then timer = timer - _self.dtime if timer <= 0 then minetest.remove_node(bug) return true end end end self:set_utility(func) end) creatura.register_utility("animalia:run_to_pos", function(self, pos2, timeout) timeout = timeout or 3 local function func(_self) local pos = _self.object:get_pos() if not pos then return end if not pos2 then return true end if not _self:get_action() then local anim = (_self.animations["run"] and "run") or "walk" creatura.action_move(_self, pos2, 2, "creatura:obstacle_avoidance", 1, anim) end timeout = timeout - _self.dtime if timeout <= 0 then return true end end self:set_utility(func) end) -- Object Interaction creatura.register_utility("animalia:follow_player", function(self, player, force) local width = self.width local function func(_self) local pos = _self.object:get_pos() if not pos then return end local plyr_alive, _, plyr_pos = _self:get_target(player) if not plyr_alive or (not _self:follow_wielded_item(player) and not force) then return true end local dist = vec_dist(pos, plyr_pos) if not _self:get_action() then if dist > width + 1 then animalia.action_pursue(_self, player, 3, "creatura:context_based_steering", 0.75) else creatura.action_idle(_self, 1) end end end self:set_utility(func) end) creatura.register_utility("animalia:flee_from_target", function(self, target) local los_timeout = 3 local function func(_self) local pos = _self.object:get_pos() if not pos then return end local tgt_alive, los, tgt_pos = _self:get_target(target) if not tgt_alive then self._puncher = nil return true end if not los then los_timeout = los_timeout - _self.dtime else los_timeout = 3 end if los_timeout <= 0 then self._puncher = nil return true end local dist = vec_dist(pos, tgt_pos) if dist > _self.tracking_range then self._puncher = nil return true end if not _self:get_action() then local flee_dir = vec_dir(tgt_pos, pos) local pos2 = _self:get_wander_pos(2, 3, flee_dir) local anim = (_self.animations["run"] and "run") or "walk" creatura.action_move(_self, pos2, 2, "creatura:context_based_steering", 1, anim) end end self:set_utility(func) end) creatura.register_utility("animalia:flee_from_target_defend", function(self, target) local los_timeout = 3 local function func(_self) local pos = _self.object:get_pos() if not pos then return end local tgt_alive, los, tgt_pos = _self:get_target(target) if not tgt_alive then self._puncher = nil return true end if not los then los_timeout = los_timeout - _self.dtime else los_timeout = 3 end if los_timeout <= 0 then self._puncher = nil return true end local dist = vec_dist(pos, tgt_pos) if dist > _self.tracking_range then self._puncher = nil return true end if not _self:get_action() then local flee_dir = vec_dir(tgt_pos, pos) local pos2 = _self:get_wander_pos(2, 3, flee_dir) local anim = (_self.animations["run"] and "run") or "walk" if dist > _self.width + 0.5 then creatura.action_move(_self, pos2, 2, "creatura:obstacle_avoidance", 1, anim) else animalia.action_punch_aoe(self, target) end end end self:set_utility(func) end) creatura.register_utility("animalia:tame_horse", function(self) local center = self.object:get_pos() local trust = 5 local player = self.rider local player_props = player and player:get_properties() if not player_props then return end local player_size = player_props.visual_size local mob_size = self.visual_size local adj_size = { x = player_size.x / mob_size.x, y = player_size.y / mob_size.y } if player_size.x ~= adj_size.x then player:set_properties({ visual_size = adj_size }) end local function func(_self) local pos = _self.object:get_pos() if not pos then return end if not _self.rider or not creatura.is_alive(_self.rider) then return true end player = _self.rider animate_player(player, "sit", 30) if _self:timer(1) then player_props = player and player:get_properties() if player_props.visual_size.x ~= adj_size.x then player:set_properties({ visual_size = adj_size }) end end if not _self:get_action() then if random(6) < 2 then creatura.action_idle(_self, 0.5, "punch_aoe") else local dir = vec_dist(pos, center) > 8 and vec_dir(pos, center) local pos2 = _self:get_wander_pos(2, 4, dir) creatura.action_move(_self, pos2, 3, "creatura:obstacle_avoidance", 1, "run") end end local yaw = _self.object:get_yaw() local plyr_yaw = player:get_look_horizontal() if abs(diff(yaw, plyr_yaw)) < pi * 0.25 then trust = trust + _self.dtime else trust = trust - _self.dtime * 0.5 end local min_pos = {x = pos.x, y = pos.y + 2, z = pos.z} local max_pos = {x = pos.x, y = pos.y + 2, z = pos.z} if trust <= 0 then animalia.mount(_self, player) animalia.particle_spawner(pos, "creatura_particle_red.png", "float", min_pos, max_pos) return true end if trust >= 10 then _self.owner = self:memorize("owner", player:get_player_name()) animalia.protect_from_despawn(_self) animalia.mount(_self, player) animalia.particle_spawner(pos, "creatura_particle_green.png", "float", min_pos, max_pos) return true end if not player or player:get_player_control().sneak then animalia.mount(_self, player) return true end end self:set_utility(func) end) creatura.register_utility("animalia:attack_target", function(self, target) local width = self.width local punch_init = false local function func(_self) local pos = _self.object:get_pos() if not pos then return end local tgt_alive, _, tgt_pos = _self:get_target(target) if not tgt_alive then reset_attack_vals(self) return true end local dist = vec_dist(pos, tgt_pos) if dist > self.tracking_range then reset_attack_vals(self) return true end local punch_cooldown = self.punch_cooldown or 0 if punch_cooldown > 0 then punch_cooldown = punch_cooldown - self.dtime end self.punch_cooldown = punch_cooldown if punch_cooldown <= 0 and dist < width + 1 and not punch_init then punch_init = true animalia.action_punch(_self, target) self.punch_cooldown = 1.5 end if not _self:get_action() then if punch_init then reset_attack_vals(self) return true end animalia.action_pursue(_self, target, 3, "creatura:pathfind", 0.75) end end self:set_utility(func) end) creatura.register_utility("animalia:glide_attack_target", function(self, target) local hidden_timer = 1 local attack_init = false local function func(_self) local pos, yaw = _self.object:get_pos(), _self.object:get_yaw() if not pos then return end local target_alive, los, tgt_pos = _self:get_target(target) if not target_alive then _self._target = nil return true end if not _self:get_action() then if attack_init then return true end local dist = vec_dist(pos, tgt_pos) if dist > 14 then creatura.action_move(_self, tgt_pos, 3, "animalia:fly_obstacle_avoidance", 0.5, "fly") else animalia.action_flight_attack(_self, target, 12) end end end self:set_utility(func) end) creatura.register_utility("animalia: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 _self.breeding then return true end local pos = _self.object:get_pos() if not pos then return end local tgt_pos = mate:get_pos() if not tgt_pos then return end local dist = vec_dist(pos, tgt_pos) if dist < _self.width + 0.5 then breeding_time = breeding_time + _self.dtime end if breeding_time > 2 then local mate_ent = mate:get_luaentity() _self.breeding = self:memorize("breeding", false) _self.breeding_cooldown = _self:memorize("breeding_cooldown", 300) mate_ent.breeding = mate_ent:memorize("breeding", false) mate_ent.breeding_cooldown = mate_ent:memorize("breeding_cooldown", 300) local minp = vector.subtract(pos, 1) local maxp = vec_add(pos, 1) animalia.particle_spawner(pos, "heart.png", "float", minp, maxp) for _ = 1, _self.birth_count or 1 do if _self.add_child then _self:add_child(mate) else 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 return true end if not _self:get_action() then creatura.action_move(_self, tgt_pos) end end self:set_utility(func) end) creatura.register_utility("animalia:fly_to_food", function(self, food_item) local eat_init = false local function func(_self) local pos, tgt_pos = _self.object:get_pos(), food_item and food_item:get_pos() if not pos then return end if not tgt_pos and not eat_init then return true end local dist = vec_dist(pos, tgt_pos or pos) if dist < 1 and not eat_init then eat_init = true local food_ent = food_item:get_luaentity() local stack = ItemStack(food_ent.itemstring) if stack and stack:get_count() > 1 then stack:take_item() food_ent.itemstring = stack:to_string() else food_item:remove() end self.object:get_yaw(dir2yaw(vec_dir(pos, tgt_pos))) add_eat_particle(self, "animalia:rat_raw") creatura.action_idle(_self, 1, "eat") self.eat_cooldown = 60 if self.on_eat_drop then self:on_eat_drop() end end if not _self:get_action() then if eat_init then return true end creatura.action_move(_self, tgt_pos, 3, "animalia:fly_simple", 1, "fly") end end self:set_utility(func) end) creatura.register_utility("animalia:walk_to_food", function(self, food_item) local eat_init = false local function func(_self) local pos, tgt_pos = _self.object:get_pos(), food_item and food_item:get_pos() if not pos then return end if not tgt_pos and not eat_init then return true end local dist = vec_dist(pos, tgt_pos or pos) if dist < 1 and not eat_init then eat_init = true local food_ent = food_item:get_luaentity() local stack = ItemStack(food_ent.itemstring) if stack and stack:get_count() > 1 then stack:take_item() food_ent.itemstring = stack:to_string() else food_item:remove() end self.object:get_yaw(dir2yaw(vec_dir(pos, tgt_pos))) add_eat_particle(self, "animalia:rat_raw") local anim = (self.animations["eat"] and "eat") or "stand" creatura.action_idle(_self, 1, anim) self.eat_cooldown = 60 if self.on_eat_drop then self:on_eat_drop() end end if not _self:get_action() then if eat_init then return true end creatura.action_move(_self, tgt_pos, 3, "creatura:context_based_steering", 0.5, "walk") end end self:set_utility(func) end) -- Pest Behavior creatura.register_utility("animalia:eat_crop", function(self) local center = self.object:get_pos() if not center then return end local width = self.width local timeout = 8 local nodes = minetest.find_nodes_in_area(vec_sub(center, 6), vec_add(center, 6), "group:crop") or {} local pos2 local anim_init = false local function func(_self) local pos = _self.object:get_pos() if not pos then return end if #nodes < 1 then return true end if not _self:get_action() then if anim_init then return true end pos2 = pos2 or nodes[random(#nodes)] local node_name = minetest.get_node(pos2).name local dist = vec_dist(pos, pos2) if dist < width + 0.5 then local new_name = node_name:sub(1, #node_name - 1) .. (tonumber(node_name:sub(-1)) or 2) - 1 local new_def = minetest.registered_nodes[new_name] if not new_def then return true end local p2 = new_def.place_param2 or 1 minetest.set_node(pos2, {name = new_name, param2 = p2}) add_eat_particle(self, new_name) anim_init = true creatura.action_idle(_self, 0.5, "eat") else creatura.action_move(_self, pos2, 4, "creatura:context_based_steering") end end timeout = timeout - _self.dtime if timeout <= 0 then return true end end self:set_utility(func) end) local function take_food_from_chest(self, pos) local inv = minetest.get_inventory({type = "node", pos = pos}) if inv and inv:get_list("main") then for i, stack in ipairs(inv:get_list("main")) do local item_name = stack:get_name() local def = minetest.registered_items[item_name] for group in pairs(def.groups) do if group:match("food_") then stack:take_item() inv:set_stack("main", i, stack) add_eat_particle(self, item_name) return true end end end end end creatura.register_utility("animalia:steal_from_chest", function(self) local center = self.object:get_pos() if not center then return end local width = self.width local timeout = 8 local nodes = minetest.find_nodes_with_meta(vec_sub(center, 6), vec_add(center, 6)) or {} local pos2 for _, node_pos in ipairs(nodes) do local meta = minetest.get_meta(node_pos) if meta:get_string("owner") == "" then local inv = minetest.get_inventory({type = "node", pos = node_pos}) if inv and inv:get_list("main") then pos2 = node_pos end end end local anim_init = false local function func(_self) local pos = _self.object:get_pos() if not pos then return end if not pos2 then return true end if not _self:get_action() then if anim_init then return true end local dist = vec_dist(pos, pos2) if dist < width + 1.1 then anim_init = true if take_food_from_chest(self, pos2) then creatura.action_idle(_self, 0.5, "eat") end else creatura.action_move(_self, pos2, 2, "creatura:context_based_steering") end end timeout = timeout - _self.dtime if timeout <= 0 then return true end end self:set_utility(func) end) -- Domesticated Behavior creatura.register_utility("animalia:stay", function(self) local function func(_self) local order = _self.order or "wander" if order ~= "sit" then return true end if not _self:get_action() then creatura.action_idle(_self, 1, "sit") end end self:set_utility(func) end) creatura.register_utility("animalia:destroy_nearby_vessel", function(self) local width = self.width local timeout = 8 local nodes local glass_vessels = {"vessels:glass_bottle", "vessels:drinking_glass"} local pos2 local function func(_self) local pos = _self.object:get_pos() if not pos then return end nodes = nodes or minetest.find_nodes_in_area(vec_sub(pos, 6), vec_add(pos, 6), glass_vessels) or {} if #nodes < 1 then return true end if not _self:get_action() then pos2 = pos2 or nodes[random(#nodes)] local dist = vec_dist(pos, pos2) if dist < width + 0.5 then creatura.action_idle(_self, 0.7, "smack") minetest.remove_node(pos2) minetest.add_item(pos2, "vessels:glass_fragments") return true else creatura.action_move(_self, pos2, 4, "creatura:pathfind") end end timeout = timeout - self.dtime if timeout <= 0 then return true end end self:set_utility(func) end) creatura.register_utility("animalia:play_with_player", function(self, player) local play_init = false local width = self.width local function func(_self) local pos = _self.object:get_pos() if not pos then return end local plyr_alive, _, plyr_pos = _self:get_target(player) if not plyr_alive or _self.trust_cooldown > 0 then return true end local dist = vec_dist(pos, plyr_pos) if dist < width + 0.5 and not play_init then creatura.action_idle(_self, 0.5, "play") _self.object:add_velocity({x = 0, y = 2, z = 0}) animalia.particle_spawner(pos, "heart.png", "float") animalia.add_trust(_self, player, 1) play_init = true end if not _self:get_action() then if play_init then return true end animalia.action_pursue(_self, player, 1, "creatura:obstacle_avoidance") end end self:set_utility(func) end) creatura.register_utility("animalia:bother_player", function(self, player) local width = self.width local play_init = false local timeout = 5 local function func(_self) local pos = _self.object:get_pos() if not pos then return end local plyr_alive, _, plyr_pos = _self:get_target(player) if not plyr_alive then return true end local dist = vec_dist(pos, plyr_pos) if not _self:get_action() then if play_init then return true end if dist > width then animalia.action_pursue(_self, player, 3, "creatura:pathfind", 0.75) else creatura.action_idle(_self, 0.5, "play") play_init = true end end timeout = timeout - _self.dtime if timeout <= 0 then return true end end self:set_utility(func) end) creatura.register_utility("animalia:mount_horse", function(self, player) local player_props = player and player:get_properties() if not player_props then return end local player_size = player_props.visual_size local mob_size = self.visual_size local adj_size = { x = player_size.x / mob_size.x, y = player_size.y / mob_size.y } if player_size.x ~= adj_size.x then player:set_properties({ visual_size = adj_size }) end local function func(_self) if not creatura.is_alive(player) then return true end local anim = "stand" local speed_x = 0 local tyaw = player:get_look_horizontal() local control = player:get_player_control() local vel = _self.object:get_velocity() if not tyaw then return end animate_player(player, "sit", 30) if _self:timer(1) then player_props = player and player:get_properties() if player_props.visual_size.x ~= adj_size.x then player:set_properties({ visual_size = adj_size }) end end if control.up then speed_x = 1 anim = "walk" if control.aux1 then speed_x = 1.5 anim = "run" end end if control.jump and _self.touching_ground and vel.y < 1 then _self.object:add_velocity({ x = 0, y = _self.jump_power, z = 0 }) elseif not _self.touching_ground then speed_x = speed_x * 0.75 end if not _self.touching_ground and not _self.in_liquid and vel.y > 0 then anim = "rear" end local yaw = self.object:get_yaw() if abs(yaw - tyaw) > 0.1 then _self:turn_to(tyaw, _self.turn_rate) end _self:set_forward_velocity(_self.speed * speed_x) _self:animate(anim) if control.sneak or not _self.rider then animalia.mount(_self, player) return true end end self:set_utility(func) end) -- Misc 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) -- Global Utilities animalia.global_utils = { ["basic_follow"] = { utility = "animalia:follow_player", get_score = function(self) local lasso_tgt = self._lassod_to local lasso = type(lasso_tgt) == "string" and minetest.get_player_by_name(lasso_tgt) local force = lasso and lasso ~= false local player = (force and lasso) or creatura.get_nearby_player(self) if player and (self:follow_wielded_item(player) or force) then return 0.4, {self, player, force} end return 0 end }, ["basic_flee"] = { utility = "animalia:flee_from_target", get_score = function(self) local puncher = self._puncher if puncher and puncher:get_pos() then return 0.6, {self, puncher} end self._puncher = nil return 0 end } }