------------- ---- API ---- ------------- -- Ver 0.1 -- local hitbox = mob_core.get_hitbox local find_string = mob_core.find_val ---------- -- Math -- ---------- local random = math.random local min = math.min local pi = math.pi local abs = math.abs local ceil = math.ceil local floor = math.floor local function diff(a, b) -- Get difference between 2 angles return math.atan2(math.sin(b - a), math.cos(b - a)) end local function round(x) -- Round to nearest multiple of 0.5 return x + 0.5 - (x + 0.5) % 1 end local function clamp(num, min, max) if num < min then num = min elseif num > max then num = max end return num end local yaw2dir = minetest.yaw_to_dir local dir2yaw = minetest.dir_to_yaw local vec_dist = vector.distance local vec_dir = vector.direction local function dist_2d(pos1, pos2) local a = vector.new(pos1.x, 0, pos1.z) local b = vector.new(pos2.x, 0, pos2.z) return vec_dist(a, b) end local function get_average_pos(vectors) local sum = {x = 0, y = 0, z = 0} for _, vec in pairs(vectors) do sum = vector.add(sum, vec) end local avg = vector.divide(sum, #vectors) avg.x = math.floor(avg.x) + 0.5 avg.y = math.floor(avg.y) + 0.5 avg.z = math.floor(avg.z) + 0.5 return avg end local function pos_to_neighbor(self, pos2) local pos = self.object:get_pos() local dir = vector.direction(pos, pos2) local neighbor = self._neighbors[mobkit.dir2neighbor(dir)] local vec = { x = neighbor.x, y = 0, z = neighbor.z } return vector.add(pos, vec) end local function get_random_offset(pos, range) if not pos then return nil end range = (range * 0.5) or 3 local pos_x = pos.x + random(-range, range) local pos_z = pos.z + random(-range, range) local node = nil for i = -1, 1 do i_pos = vector.new(pos_x, pos.y + i, pos_z) if minetest.registered_nodes[minetest.get_node(vector.new(i_pos.x, i_pos.y - 1, i_pos.z)).name].walkable then node = i_pos break end end if not node then return nil end return node end local is_movable = mob_core.is_moveable ------------------- -- API Functions -- ------------------- function animalia.is_prey(name1, name2) if name1 == name2 then return 0 end local def1 = minetest.registered_entities[name1] local def2 = minetest.registered_entities[name2] if find_string(paleotest.mobkit_mobs, name2) and def1.follow and def2.drops then for _, drop in ipairs(def2.drops) do if drop.name and find_string(def1.follow, drop.name) and def1.prey_params["height"] >= get_height(name2) and def1.prey_params["health"] >= def2.max_hp then -- Mob is prey return 2 end end -- Mob can be attacked return 1 end -- Not a mobkit mob return 0 end function animalia.particle_spawner(pos, texture, type, min_pos, max_pos) type = type or "float" min_pos = min_pos or { x = pos.x - 2, y = pos.y - 2, z = pos.z - 2, } max_pos = max_pos or { x = pos.x + 2, y = pos.y + 2, z = pos.z + 2, } if type == "float" then minetest.add_particlespawner({ amount = 16, time = 0.25, minpos = min_pos, maxpos = max_pos, minvel = {x = 0, y = 0.2, z = 0}, maxvel = {x = 0, y = 0.25, z = 0}, minexptime = 0.75, maxexptime = 1, minsize = 4, maxsize = 4, texture = texture, glow = 1, }) elseif type == "splash" then minetest.add_particlespawner({ amount = 6, time = 0.25, minpos = {x = pos.x - 7/16, y = pos.y + 0.6, z = pos.z - 7/16}, maxpos = {x = pos.x + 7/16, y = pos.y + 0.6, 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), minsize = 2, maxsize = 4, collisiondetection = true, texture = texture, }) end end function animalia.can_reach(self, pos2) if pos2 then local pos = mobkit.get_stand_pos(self) local box = hitbox(self) local path_data = mob_core.find_path(pos, pos2, box[4] - 0.1, self.height, 100) if path_data and #path_data > 2 then return true, path_data end end return false end function animalia.find_collision(self, dir) local pos = mobkit.get_stand_pos(self) local pos2 = vector.add(pos, vector.multiply(dir, 16)) local ray = minetest.raycast(pos, pos2, false, false) for pointed_thing in ray do if pointed_thing.type == "node" then return pointed_thing.under end end return nil end function animalia.get_nearby_mate(self, name) for _, obj in ipairs(self.nearby_objects) do if mobkit.is_alive(obj) and not obj:is_player() and obj:get_luaentity().name == name and obj:get_luaentity().gender ~= self.gender and obj:get_luaentity().breeding then return obj end end end function animalia.feed_tame(self, clicker, feed_count, tame, breed) local item = clicker:get_wielded_item() local pos = self.object:get_pos() local mob_name = mob_core.get_name_proper(self.name) if mob_core.follow_holding(self, clicker) then if creative == false then item:take_item() clicker:set_wielded_item(item) end mobkit.heal(self, self.max_hp/feed_count) if self.hp >= self.max_hp then self.hp = self.max_hp end self.food = mobkit.remember(self, "food", self.food + 1) local minppos = vector.add(pos, hitbox(self)[4]) local maxppos = vector.subtract(pos, hitbox(self)[4]) local def = minetest.registered_items[item:get_name()] local texture = def.inventory_image if not texture or texture == "" then texture = def.wield_image if def.tiles then texture = def.tiles[1] end end texture = texture .. "^[resize:8x8" -- Crops image minetest.add_particlespawner({ amount = 12*self.height, time = 0.1, minpos = minppos, maxpos = maxppos, 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 = 2*self.height, maxsize = 3*self.height, collisiondetection = true, vertical = false, texture = texture, }) if self.food >= feed_count then self.food = mobkit.remember(self, "food", 0) if tame and not self.tamed and self.follow[1] == item:get_name() then mob_core.set_owner(self, clicker:get_player_name()) minetest.chat_send_player(clicker:get_player_name(), mob_name.." has been tamed!") mobkit.clear_queue_high(self) animalia.particle_spawner(pos, "mob_core_green_particle.png", "float", minppos, maxppos) end if breed then if self.child then return false end if self.breeding then return false end if self.breeding_cooldown <= 0 then self.breeding = true animalia.particle_spawner(pos, "heart.png", "float", minppos, maxppos) end end end end return false end local function is_within_reach(self, target) local dist = vec_dist(mobkit.get_stand_pos(self), mobkit.get_stand_pos(target)) - (hitbox(self)[4] + hitbox(target)[4]) if dist <= self.reach then return true end return false end local function is_on_ground(object) if object then local pos = object:get_pos() local sub = 1 if not object:is_player() then sub = math.abs(hitbox(object)[2]) + 1 end pos.y = pos.y - sub if minetest.registered_nodes[minetest.get_node(pos).name].walkable then return true end pos.y = pos.y - 1 if minetest.registered_nodes[minetest.get_node(pos).name].walkable then return true end end return false end local function get_height(name) local def = minetest.registered_entities[name] return abs(def.collisionbox[2]) + abs(def.collisionbox[5]) end ------------------ -- LQ Functions -- ------------------ function animalia.lq_follow_path(self, path_data, speed_factor, anim) speed_factor = speed_factor or 1 anim = anim or "walk" local dest = nil local timer = #path_data -- failsafe local width = hitbox(self)[4] local init = false local func = function(self) local pos = mobkit.get_stand_pos(self) local yaw = self.object:get_yaw() local s_fctr = speed_factor if path_data and #path_data > 1 then if #path_data >= math.ceil(width) then dest = path_data[1] else return true end else return true end if not self.isonground then table.remove(path_data, 1) timer = timer - 1 s_fctr = 0.25 end timer = timer - self.dtime if timer < 0 then return true end local y = self.object:get_velocity().y local tyaw = minetest.dir_to_yaw(vector.direction(pos, dest)) mobkit.turn2yaw(self, tyaw, self.turn_rate or 4) if vec_dist(pos, path_data[#path_data]) < math.ceil(width) then if not self.isonground and not self.isinliquid and mob_core.fall_check(self, pos, self.max_fall or self.jump_height) then self.object:set_velocity({x = 0, y = y, z = 0}) end return true end if vec_dist(pos, path_data[1]) < 2.5 and diff(yaw, tyaw) < 1.5 then table.remove(path_data, 1) timer = timer - 1 end if mob_core.fall_check(self, pos, self.max_fall or self.jump_height) then self.object:set_velocity({x = 0, y = y, z = 0}) return true end if self.isonground or self.isinliquid then local forward_dir = vector.normalize(minetest.yaw_to_dir(yaw)) forward_dir = vector.multiply(forward_dir, self.max_speed * s_fctr) forward_dir.y = y self.object:set_velocity(forward_dir) if not init then mobkit.animate(self, anim) init = true end end end mobkit.queue_low(self, func) end function animalia.lq_dumb_follow_path(self, path_data, speed_factor, anim) speed_factor = speed_factor or 1 anim = anim or "walk" local dest = nil local timer = 3 -- failsafe local width = hitbox(self)[4] local stop_thresh = 1 local init = false local func = function(self) local pos = mobkit.get_stand_pos(self) local yaw = self.object:get_yaw() if path_data and #path_data > 1 then dest = path_data[1] else return true end if not self.isonground then table.remove(path_data, 1) end timer = timer - self.dtime if timer < 0 then return true end local y = self.object:get_velocity().y local tyaw = minetest.dir_to_yaw(vector.direction(pos, dest)) if #path_data > 2 and ((dist_2d(pos, path_data[1]) < 1 or abs(tyaw - yaw) < 3) or dist_2d(pos, path_data[1]) >= dist_2d(pos, path_data[2])) then table.remove(path_data, 1) end if abs(yaw - tyaw) > 0.5 then mobkit.turn2yaw(self, tyaw, 8) end if dist_2d(pos, path_data[#path_data]) < 0.6 then if not self.isonground and not self.isinliquid and mob_core.fall_check(self, pos, self.max_fall or self.jump_height) then self.object:set_velocity({x = 0, y = y, z = 0}) end mobkit.animate(self, "stand") return true end if dist_2d(pos, path_data[#path_data]) < 0.6 then mobkit.animate(self, "stand") return true end if mob_core.fall_check(self, pos, self.max_fall or self.jump_height) then self.object:set_velocity({x = 0, y = y, z = 0}) return true end if self.isonground or self.isinliquid then local forward_dir = vector.normalize(minetest.yaw_to_dir(yaw)) forward_dir = vector.multiply(forward_dir, self.max_speed * speed_factor) forward_dir.y = y self.object:set_velocity(forward_dir) if not init then mobkit.animate(self, anim) init = true end end end mobkit.queue_low(self, func) end function animalia.lq_idle(self, duration, anim) anim = anim or 'stand' local random_yaw = nil local init = true local func = function(self) if init then mobkit.animate(self, anim) init = false end duration = duration - self.dtime if random(6) < 2 and not random_yaw then random_yaw = self.object:get_yaw() + random(-2, 2) elseif random_yaw and abs(self.object:get_yaw() - random_yaw) > 0.1 then mobkit.turn2yaw(self, random_yaw, 3) self._tyaw = random_yaw end if duration <= 0 then return true end end mobkit.queue_low(self, func) end --------------------------- -- Mob Control Functions -- --------------------------- function animalia.go_to_pos(self, tpos, speed_factor, anim) speed_factor = speed_factor or 1 local pos = self.object:get_pos() local dist = vec_dist(pos, tpos) if dist < 5 and minetest.line_of_sight(pos, tpos) then local _, pos2 = mob_core.get_next_waypoint(self, tpos) if pos2 then mob_core.lq_dumbwalk(self, pos2, speed_factor, anim) return end else local box = hitbox(self) local path_data = mob_core.find_path(mobkit.get_stand_pos(self), tpos, box[4] - 0.1, self.height, 100) if path_data then mob_core.lq_follow_path(self, path_data, speed_factor, anim) return end end mob_core.lq_dumbwalk(self, tpos, speed_factor, anim) end function animalia.go_to_pos_lite(self, tpos, speed_factor) speed_factor = speed_factor or 1 if mobkit.is_queue_empty_low(self) then local _, pos2 = mob_core.get_next_waypoint(self, tpos) if pos2 then mob_core.lq_dumbwalk(self, tpos, speed_factor) return true else local box = hitbox(self) local path_data = mob_core.find_path(mobkit.get_stand_pos(self), tpos, box[4] - 0.1, self.height, 100) if path_data and #path_data > 2 then mob_core.lq_follow_path(self, path_data, speed_factor, anim) return true end end end return false end --------------------------------- -- Entity Definition Functions -- --------------------------------- local function sensors() local timer = 2 local pulse = 1 return function(self) timer = timer - self.dtime if timer < 0 then pulse = pulse + 1 local range = self.view_range if pulse > 2 then pulse = 1 else range = self.view_range * 0.5 end local pos = self.object:get_pos() self.group = {} self.nearby_objects = minetest.get_objects_inside_radius(pos, range) for i, obj in ipairs(self.nearby_objects) do if obj ~= self.object and obj:get_luaentity() and obj:get_luaentity().name == self.name then table.insert(self.group, obj) elseif obj == self.object then table.remove(self.nearby_objects, i) break end end timer = 2 end end end function animalia.on_activate(self, staticdata, dtime_s) mob_core.on_activate(self, staticdata, dtime_s) self.sensefunc = sensors() self.order = mobkit.recall(self, "order") or "wander" self.gotten = mobkit.recall(self, "gotten") or false self.attention_span = mobkit.recall(self, "attention_span") or 0 self.breeding = mobkit.recall(self, "breeding") or false self.breeding_time = mobkit.recall(self, "breeding_time") or 0 self.breeding_cooldown = mobkit.recall(self, "breeding_cooldown") or 0 self.lasso_pos = mobkit.recall(self, "lasso_pos") or nil self.liquid_recovery_cooldown = 0 self.target_blacklist = {} if self.lasso_pos then self.caught_with_lasso = true if minetest.get_item_group(minetest.get_node(self.lasso_pos).name, "fence") > 0 then local object = minetest.add_entity(self.lasso_pos, "animalia:lasso_fence_ent") object:get_luaentity().parent = self.object end end for name in pairs(minetest.registered_entities) do if self.targets and self.prey_params then if animalia.is_prey(self.name, name) == 2 then table.insert(self.targets, name) end end end end local function lasso_effect(self, pos2) local pos = mobkit.get_stand_pos(self) pos.y = pos.y + (self.height * 0.5) local object = minetest.add_entity(pos2, "animalia:lasso_visual") local ent = object:get_luaentity() ent.parent = self.object ent.anchor_pos = pos2 return object end local function is_under_solid(pos) local pos2 = vector.new(pos.x, pos.y + 1, pos.z) local def = minetest.registered_nodes[minetest.get_node(pos2).name] return (def.walkable or ((mobkit.get_node_height(pos2) or 0) < 1.5)) end local function vec_center(vec) for _, v in pairs(vec) do v = floor(v + 0.5) end return vec end local function do_step(self, moveresult) local pos = mobkit.get_stand_pos(self) local width = hitbox(self)[4] - 0.1 if not self._step then for _, data in ipairs(moveresult.collisions) do if data.type == "node" and data.node_pos.y + 0.5 > pos.y and not is_under_solid(data.node_pos) and not vector.equals(vec_center(pos), vec_center(data.node_pos)) then local vel_yaw = self.object:get_yaw() local dir_yaw = minetest.dir_to_yaw(vector.direction(pos, data.node_pos)) if diff(vel_yaw, dir_yaw) < 1.6 then self._step = data.node_pos break end end end else local vel = self.object:get_velocity() self.object:set_velocity(vector.new(vel.x, 4, vel.z)) if self._step.y < pos.y - 0.5 then self.object:set_velocity(vector.new(vel.x, 0.5, vel.z)) self._step = nil end end end function animalia.on_step(self, dtime, moveresult) mob_core.on_step(self, dtime, moveresult) mob_core.vitals(self) if mobkit.timer(self, 1) then if self.breeding_cooldown > 0 then self.breeding_cooldown = self.breeding_cooldown - 1 end mobkit.remember(self, "breeding_cooldown", self.breeding_cooldown) end if mobkit.timer(self, 4) and #self.target_blacklist > 0 then table.remove(self.target_blacklist, 1) end if self.caught_with_lasso then if self.lasso_player and mobkit.is_alive(self) then local player = self.lasso_player local pos = mobkit.get_stand_pos(self) pos.y = pos.y + (self.height * 0.5) local ppos = player:get_pos() ppos.y = ppos.y + 1 if not self.lasso_visual or not self.lasso_visual:get_luaentity() then self.lasso_visual = lasso_effect(self, ppos) else self.lasso_visual:get_luaentity().anchor_pos = ppos end local dist = vector.distance(pos, ppos) local dist = vector.distance(pos, ppos) if dist_2d(pos, ppos) > 6 or abs(ppos.y - pos.y) > 8 then local p_target = vector.add(pos, vector.multiply(vector.direction(pos, ppos), dist * 0.8)) local g = -0.18 local v = vector.new(0, 0, 0) v.x = (1.0 + (0.005 * dist)) * (p_target.x - pos.x) / dist v.y = -((1.0 + (0.03 * dist)) * ((ppos.y - 4) - pos.y) / (dist * (g * dist))) v.z = (1.0 + (0.005 * dist)) * (p_target.z - pos.z) / dist self.object:add_velocity(v) end if player:get_wielded_item():get_name() ~= "animalia:lasso" or vector.distance(pos, ppos) > 20 then self.caught_with_lasso = nil self.lasso_player = nil if self.lasso_visual then self.lasso_visual:remove() self.lasso_visual = nil end end elseif self.lasso_pos and mobkit.is_alive(self) then mobkit.remember(self, "lasso_pos", self.lasso_pos) local pos = mobkit.get_stand_pos(self) pos.y = pos.y + (self.height * 0.5) local ppos = self.lasso_pos if not self.lasso_visual or not self.lasso_visual:get_luaentity() then self.lasso_visual = lasso_effect(self, ppos) else self.lasso_visual:get_luaentity().anchor_pos = ppos end local dist = vector.distance(pos, ppos) if dist_2d(pos, ppos) > 6 or abs(ppos.y - pos.y) > 8 then local p_target = vector.add(pos, vector.multiply(vector.direction(pos, ppos), dist * 0.8)) local g = -0.18 local v = vector.new(0, 0, 0) v.x = (1.0 + (0.005 * dist)) * (p_target.x - pos.x) / dist v.y = -((1.0 + (0.03 * dist)) * ((ppos.y - 4) - pos.y) / (dist * (g * dist))) v.z = (1.0 + (0.005 * dist)) * (p_target.z - pos.z) / dist self.object:add_velocity(v) end local objects = minetest.get_objects_inside_radius(ppos, 1) local is_lasso_attached = false for _, object in ipairs(objects) do if object and object:get_luaentity() and object:get_luaentity().name == "animalia:lasso_fence_ent" then is_lasso_attached = true end end if not is_lasso_attached then self.caught_with_lasso = nil self.lasso_pos = nil if self.lasso_visual then self.lasso_visual:remove() self.lasso_visual = nil end end else if self.lasso_pos then local objects = minetest.get_objects_inside_radius(self.lasso_pos, 0.4) for _, object in ipairs(objects) do if object and object:get_luaentity() and object:get_luaentity().name == "animalia:lasso_fence_ent" then minetest.add_item(object:get_pos(), "animalia:lasso") object:remove() end end end self.caught_with_lasso = nil self.lasso_pos = nil if self.lasso_visual then self.lasso_visual:remove() self.lasso_visual = nil end end end if mobkit.is_alive(self) then do_step(self, moveresult) end end ------------- -- Physics -- ------------- function animalia.lightweight_physics(self) local vel = self.object:get_velocity() if self.isonground and not self.isinliquid then self.object:set_velocity({x= vel.x> 0.2 and vel.x*mobkit.friction or 0, y=vel.y, z=vel.z > 0.2 and vel.z*mobkit.friction or 0}) end if self.springiness and self.springiness > 0 then local vnew = vector.new(vel) if not self.collided then for _,k in ipairs({'y','z','x'}) do if vel[k]==0 and abs(self.lastvelocity[k])> 0.1 then vnew[k]=-self.lastvelocity[k]*self.springiness end end end if not vector.equals(vel,vnew) then self.collided = true else if self.collided then vnew = vector.new(self.lastvelocity) end self.collided = false end self.object:set_velocity(vnew) end local surface = nil local surfnodename = nil local spos = mobkit.get_stand_pos(self) spos.y = spos.y+0.01 local snodepos = mobkit.get_node_pos(spos) local surfnode = mobkit.nodeatpos(spos) while surfnode and surfnode.drawtype == 'liquid' do surfnodename = surfnode.name surface = snodepos.y+0.5 if surface > spos.y+self.height then break end snodepos.y = snodepos.y+1 surfnode = mobkit.nodeatpos(snodepos) end self.isinliquid = surfnodename if surface then local submergence = min(surface-spos.y,self.height)/self.height local buoyacc = mobkit.gravity*(self.buoyancy-submergence) mobkit.set_acceleration(self.object, {x=-vel.x*self.water_drag,y=buoyacc-vel.y*abs(vel.y)*0.4,z=-vel.z*self.water_drag}) else self.object:set_acceleration({x=0,y=-2.8,z=0}) end end ------------------ -- HQ Functions -- ------------------ function animalia.hq_eat(self, prty) local func = function(self) local pos = mobkit.get_stand_pos(self) local under = vector.new(pos.x, pos.y - 1, pos.z) for _, 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 mobkit.remember(self, "gotten", self.gotten) return true else return true end end end mobkit.queue_high(self, func, prty) end -- Wandering -- function animalia.hq_go_to_land(self, prty) local init = false local tpos = nil local func = function(self) if self.liquid_recovery_cooldown > 0 then self.liquid_recovery_cooldown = self.liquid_recovery_cooldown - 1 return true end if not init then for i = 1, 359, 15 do local yaw = math.rad(i) local dir = minetest.yaw_to_dir(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 = mobkit.get_stand_pos(self) local yaw = self.object:get_yaw() local tyaw = minetest.dir_to_yaw(vec_dir(pos, tpos)) if abs(tyaw - yaw) > 0.1 then mobkit.turn2yaw(self, tyaw) else mobkit.go_forward_horizontal(self, self.max_speed * 0.66) mobkit.animate(self, "walk") end if dist_2d(pos, tpos) < 1 or (not self.isinliquid and self.isonground) then return true end else self.liquid_recovery_cooldown = 5 return true end end mobkit.queue_high(self, func, prty) end function animalia.hq_wander_ranged(self, prty) local idle_time = 3 local move_probability = 3 local func = function(self) if mobkit.is_queue_empty_low(self) then local pos = self.object:get_pos() local random_goal = vector.new( pos.x + random(-1, 1), pos.y, pos.z + random(-1, 1) ) local node = minetest.get_node(random_goal) if minetest.registered_nodes[node.name].drawtype == "liquid" or minetest.registered_nodes[node.name].walkable then random_goal = nil end 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 local _, pos2 = mobkit.get_next_waypoint(self, random_goal) if pos2 then random_goal = pos2 end mob_core.lq_dumbwalk(self, random_goal, 0.5) else animalia.lq_idle(self, idle_time) end end end mobkit.queue_high(self, func, prty) end function animalia.hq_wander_group(self, prty, group_range) local idle_time = 3 local move_probability = 3 local group_tick = 0 local func = function(self) if mobkit.is_queue_empty_low(self) then group_tick = group_tick - 1 local pos = self.object:get_pos() local group_positions = {} local random_goal = vector.new( pos.x + random(-1, 1), pos.y, pos.z + random(-1, 1) ) if group_tick <= 0 and self.group and #self.group > 0 then for _, obj in ipairs(self.group) do if obj and mobkit.is_alive(obj) and #group_positions < 4 then table.insert(group_positions, obj:get_pos()) end end if #group_positions > 2 then group_range = group_range + #group_positions local center = get_average_pos(group_positions) if center and ((vec_dist(random_goal, center) > group_range) or vec_dist(pos, center) > group_range) then random_goal = pos_to_neighbor(self, center) end end group_tick = 3 end local node = minetest.get_node(random_goal) if minetest.registered_nodes[node.name].drawtype == "liquid" or minetest.registered_nodes[node.name].walkable then random_goal = nil end 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 local _, pos2 = mobkit.get_next_waypoint(self, random_goal) if pos2 then random_goal = pos2 end mob_core.lq_dumbwalk(self, random_goal, 0.5) else animalia.lq_idle(self, idle_time) end end end mobkit.queue_high(self, func, prty) end -- Breeding -- function animalia.hq_breed(self, prty) local mate = animalia.get_nearby_mate(self, self.name) if not mate then return end local func = function(self) if not mobkit.is_alive(mate) then return true end local pos = mobkit.get_stand_pos(self) local tpos = mate:get_pos() local dist = vec_dist(pos, tpos) - math.abs(hitbox(self)[4]) local speed_factor = clamp(dist, 0.1, 0.65) if dist < 1.75 then self.breeding_time = self.breeding_time + 1 end if self.breeding_time >= 2 or mate:get_luaentity().breeding_time >= 2 then if self.gender == "female" then mob_core.spawn_child(pos, self.name) end self.breeding = false self.breeding_time = 0 self.breeding_cooldown = 300 mobkit.remember(self, "breeding", self.breeding) mobkit.remember(self, "breeding_time", self.breeding_time) mobkit.remember(self, "breeding_cooldown", self.breeding_cooldown) return true end if mobkit.is_queue_empty_low(self) then animalia.go_to_pos(self, tpos, speed_factor) end end mobkit.queue_high(self, func, prty) end function animalia.hq_fowl_breed(self, prty) local mate = animalia.get_nearby_mate(self, self.name) if not mate then return end local speed_factor = 0.5 local func = function(self) if mobkit.is_queue_empty_low(self) then local pos = mobkit.get_stand_pos(self) local tpos = mate:get_pos() local dist = vec_dist(pos, tpos) - math.abs(hitbox(self)[4]) if dist > 1.5 then speed_factor = 0.5 else speed_factor = 0.1 end mob_core.goto_next_waypoint(self, tpos, speed_factor) if dist < 1.75 then self.breeding_time = self.breeding_time + 1 end if self.breeding_time >= 2 or mate:get_luaentity().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", }) mob_core.spawn_child(pos, self.name) end self.breeding = false self.breeding_time = 0 self.breeding_cooldown = 300 mobkit.remember(self, "breeding", self.breeding) mobkit.remember(self, "breeding_time", self.breeding_time) mobkit.remember(self, "breeding_cooldown", self.breeding_cooldown) return true end end end mobkit.queue_high(self, func, prty) end -- Player Interaction -- function animalia.hq_sporadic_flee(self, prty) local timer = 12 local func = function(self) if mobkit.is_queue_empty_low(self) then local pos = self.object:get_pos() local random_goal = vector.new( pos.x + random(-6, 6), pos.y, pos.z + random(-6, 6) ) local node = minetest.get_node({x = random_goal.x, y = random_goal.y + 1, z = random_goal.z}) if minetest.registered_nodes[node.name].drawtype == "liquid" then random_goal = nil end if random_goal then local anim = "walk" if self.animation["run"] then anim = "run" end mob_core.lq_dumbwalk(self, random_goal, 1, anim) else animalia.lq_idle(self, 0.1) end end timer = timer - self.dtime if timer <= 0 then return true end end mobkit.queue_high(self, func, prty) end function animalia.hq_attack(self, prty, target) local func = function(self) if not mobkit.is_alive(target) then return true end mob_core.punch_timer(self) local pos = mobkit.get_stand_pos(self) local tpos = target:get_pos() if not is_on_ground(target) then table.insert(self.target_blacklist, target) return true end local can_punch = is_within_reach(self, target) if mobkit.is_queue_empty_low(self) then animalia.go_to_pos(self, tpos, 1, "run") end if self.punch_timer <= 0 and can_punch then target:punch(self.object, 1.0, { full_punch_interval = 0.1, damage_groups = {fleshy = self.damage} }, nil) mob_core.knockback(self, target) mob_core.punch_timer(self, self.punch_cooldown or 1) return true end end mobkit.queue_high(self, func, prty) end function animalia.hq_follow_player(self, prty, player, force) -- Follow Player if not player then return end if not force and not mob_core.follow_holding(self, player) then return end local func = function(self) if not mobkit.is_alive(player) then return true end local pos = mobkit.get_stand_pos(self) local tpos = player:get_pos() if mob_core.follow_holding(self, player) or force then self.status = mobkit.remember(self, "status", "following") local dist = vec_dist(pos, tpos) local yaw = self.object:get_yaw() local tyaw = minetest.dir_to_yaw(vector.direction(pos, tpos)) if dist > self.view_range then self.status = mobkit.remember(self, "status", "") return true end if mobkit.is_queue_empty_low(self) then if vec_dist(pos, tpos) > hitbox(self)[4] + 2 then animalia.go_to_pos(self, tpos, 0.6) else mobkit.lq_idle(self, 0.1, "stand") end end elseif mobkit.is_queue_empty_low(self) then self.status = mobkit.remember(self, "status", "") mobkit.lq_idle(self, 0.1, "stand") return true end end mobkit.queue_high(self, func, prty) end ------------------------------- -- Mob Specific HQ Functions -- ------------------------------- -- Cat -- function animalia.hq_find_and_break_glass(self, prty) local timer = 6 local moving = false local pos2 = nil mobkit.clear_queue_low(self) local func = function(self) local pos = mobkit.get_stand_pos(self) if not pos2 then local nodes = minetest.find_nodes_in_area( vector.subtract(pos, 8), vector.add(pos, 8), {"vessels:glass_bottle", "vessels:drinking_glass"} ) if #nodes > 0 then pos2 = nodes[1] end end if not pos2 then return true end timer = timer - self.dtime if mobkit.is_queue_empty_low(self) then if dist_2d(pos, pos2) > 0.5 then animalia.go_to_pos(self, pos2, 0.35) end end if dist_2d(pos, pos2) <= 0.5 then mobkit.lq_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 if timer < 0 then return true end end mobkit.queue_high(self, func, prty) end function animalia.hq_walk_in_front_of_player(self, prty, player) if not player then return end local can_reach = false local path_data = nil local timer = 8 local func = function(self) if not mobkit.is_alive(player) then return true end local pos = mobkit.get_stand_pos(self) 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 = mobkit.remember(self, "status", "following") local dist = vec_dist(pos, tpos) local yaw = self.object:get_yaw() local tyaw = minetest.dir_to_yaw(vector.direction(pos, tpos)) if dist > self.view_range then self.status = mobkit.remember(self, "status", "") return true end if mobkit.is_queue_empty_low(self) then if vec_dist(pos, tpos) > hitbox(self)[4] + 0.5 then if not can_reach then can_reach, path_data = animalia.can_reach(self, tpos) else animalia.lq_dumb_follow_path(self, path_data, 1, "run") end else can_reach = false path_data = nil mobkit.lq_idle(self, 0.1, "stand") end else can_reach = false path_data = nil end timer = timer - self.dtime if timer < 0 then return true end end mobkit.queue_high(self, func, prty) end -- Horse -- function animalia.hq_mount_logic(self, prty) local tvel = 0 local rearing = false local jumping = false local anim = "stand" local func = function(self) if not self.driver then return true end -- if horse is rearing, stop moving if rearing then if mobkit.timer(self, 1.5) then rearing = false end return end -- Controls local vel = self.object:get_velocity() local ctrl = self.driver:get_player_control() if ctrl.up then tvel = self.speed if ctrl.aux1 then tvel = self.speed * 2 end elseif tvel < 0.25 or tvel == 0 then tvel = 0 self.object:set_velocity({ x = 0, y = vel.y, z = 0 }) anim = "stand" end if self.isonground then if ctrl.jump then jumping = true vel.y = self.jump_power + 4.405 else jumping = false end end -- Physics and Animation if not ctrl.up and self.isonground then tvel = tvel * 0.75 elseif not self.isonground then if self.isinliquid then tvel = tvel * 0.4 vel.y = vel.y * 0.4 elseif jumping then tvel = tvel * 0.4 end end if tvel > 0 then if jumping then anim = "rear_constant" else if ctrl.aux1 then anim = "run" else anim = "walk" end end end if random(1024) < 2 then tvel = 0 anim = "rear" rearing = true end mobkit.animate(self, anim) local tyaw = self.driver:get_look_horizontal() or 0 self._tyaw = tyaw self.object:set_yaw(tyaw) local nvel = vector.multiply(minetest.yaw_to_dir(self.object:get_yaw()), tvel) self.object:set_velocity({ x = nvel.x, y = vel.y, z = nvel.z }) if ctrl.sneak then mob_core.detach(self.driver, {x = 1, y = 0, z = 1}) return true end end mobkit.queue_high(self, func, prty) end function animalia.hq_horse_breed(self, prty) local mate = animalia.get_nearby_mate(self, self.name) if not mate then return end local speed_factor = 0.5 local func = function(self) if mobkit.is_queue_empty_low(self) then local pos = mobkit.get_stand_pos(self) local tpos = mate:get_pos() local dist = vec_dist(pos, tpos) - math.abs(hitbox(self)[4]) if dist > 1.5 then speed_factor = 0.5 else speed_factor = 0.1 end mob_core.goto_next_waypoint(self, tpos, speed_factor) if dist < 1.75 then self.breeding_time = self.breeding_time + 1 end if self.breeding_time >= 2 or mate:get_luaentity().breeding_time >= 2 then if self.gender == "female" then local obj = mob_core.spawn_child(pos, self.name) local ent = obj:get_luaentity() local tex_no = self.texture_no if random(2) < 2 then tex_no = mate:get_luaentity().texture_no end mobkit.remember(ent, "texture_no", self.texture_no) mobkit.remember(ent, "speed", random(mate:get_luaentity().speed, self.speed)) mobkit.remember(ent, "jump_power", random(mate:get_luaentity().jump_power, self.jump_power)) mobkit.remember(ent, "max_hp", random(mate:get_luaentity().max_hp, self.max_hp)) ent.speed = mobkit.recall(ent, "speed") ent.jump_power = mobkit.recall(ent, "jump_power") ent.max_hp = mobkit.recall(ent, "max_hp") ent.object:set_properties({ texture = ent.textures[ent.texture_no] .. "^" .. mobkit.recall(ent, "pattern") }) end self.breeding = false self.breeding_time = 0 self.breeding_cooldown = 300 mobkit.remember(self, "breeding", self.breeding) mobkit.remember(self, "breeding_time", self.breeding_time) mobkit.remember(self, "breeding_cooldown", self.breeding_cooldown) return true end end end mobkit.queue_high(self, func, prty) end ----------------------- -- Dynamic Animation -- ----------------------- local function clamp_bone_rot(n) -- Fixes issues with bones jittering when yaw clamps if n < -180 then n = n + 360 elseif n > 180 then n = n - 360 end if n < -60 then n = -60 elseif n > 60 then n = 60 end return n end local function interp(a, b, w) -- Smoothens bone movement local pi = math.pi if math.abs(a - b) > math.deg(pi) then if a < b then return ((a + (b - a) * w) + (math.deg(pi) * 2)) elseif a > b then return ((a + (b - a) * w) - (math.deg(pi) * 2)) end end return a + (b - a) * w end local function move_head(self, tyaw, pitch) local data = self.head_data local _, rot = self.object:get_bone_position(data.bone or "Head.CTRL") local yaw = self.object:get_yaw() local look_yaw = clamp_bone_rot(math.deg(yaw - tyaw)) local look_pitch = 0 if pitch then look_pitch = clamp_bone_rot(math.deg(pitch)) end if tyaw ~= yaw then look_yaw = look_yaw * 0.66 end local yaw = interp(rot.z, look_yaw, 0.1) local ptch = interp(rot.x, look_pitch + data.pitch_correction, 0.1) self.object:set_bone_position(data.bone or "Head.CTRL", data.offset, {x = ptch, y = yaw, z = yaw}) end function animalia.head_tracking(self) if not self.head_data then return end local yaw = self.object:get_yaw() local pos = mobkit.get_stand_pos(self) local v = vector.add(pos, vector.multiply(yaw2dir(yaw), self.head_data.pivot_h)) pos.x = v.x pos.y = pos.y + self.head_data.pivot_v pos.z = v.z --[[minetest.add_particle({ pos = pos, velocity = {x=0, y=0, z=0}, acceleration = {x=0, y=0, z=0}, expirationtime = 0.1, size = 8, collisiondetection = false, vertical = false, texture = "mob_core_green_particle.png", playername = "singleplayer" })]] if not self.head_tracking then local objects = minetest.get_objects_inside_radius(pos, 6) for _, object in ipairs(objects) do if object:is_player() then local dir_2_plyr = vector.direction(pos, object:get_pos()) local yaw_2_plyr = dir2yaw(dir_2_plyr) if abs(yaw - yaw_2_plyr) < 1 or abs(yaw - yaw_2_plyr) > 5.3 then self.head_tracking = object end break end end if self._anim == "stand" then move_head(self, yaw) else move_head(self, self._tyaw) end else if not mobkit.exists(self.head_tracking) then self.head_tracking = nil return end local ppos = self.head_tracking:get_pos() ppos.y = ppos.y + 1.4 local dir = vector.direction(pos, ppos) local tyaw = minetest.dir_to_yaw(dir) if abs(yaw - tyaw) > 1 and abs(yaw - tyaw) < 5.3 then self.head_tracking = nil dir.y = 0 return end move_head(self, tyaw, dir.y) end end