--PROJECT GOALS --[[ List of individual goals 0 is mainly function ideas/notes on how to execute things 0.) Make functions less linear and have sub functions after the proof of concept is completed 0.) check if mob is hanging off the side of a node, somehow 0.) only check nodes using voxelmanip when in new floored position 0.) minetest.line_of_sight(pos1, pos2, stepsize) to check if a mob sees player 0.) minetest.find_path(pos1,pos2,searchdistance,max_jump,max_drop,algorithm) 0.) do pathfinding by setting yaw towards the next point in the table 0.) only run this function if the goal entity/player is in a new node to prevent extreme lag 0.) Lasso that pulls mobs towards you without pathfinding 0.) sneaking mobs, if a mob is sneaking, vs running at you, make no walking sounds 0.) Make mobs define wether they float or sink in water 0.) running particles 0.) make mobs get hurt in nodes that deal player damage 0.) make mobs slow down or bounce on nodes that do that to players 0.) particles when falling in water 0.) Make mobs collision detection detect boats, minecarts, and other physical things, somehow, possibly just don't collide with 0.) item entities 0.) when mob gets below 0.1 velocity do set velocity to make it stand still ONCE so mobs don't float and set acceleration to 0 0.) if mob stops moving and def.opens_doors = true and in door then open door to leave Fishing fish mobs are drawn to lures make fish mobs drown and flop around on land CURRENT: definable collision radius from center 1.) lightweight ai that walks around, stands, does something like eat grass 2.) make mobs drown if drown = true in definition 3.) Make mobs avoid other mobs and players when walking around 5.) attacking players 6.) drop items 7.) utility mobs 8.) underwater and flying mobs 9.) pet mobs 10.) mobs climb ladders, and are affected by nodes like players are 0.) if the pathfinding goal is unreachable then don't pathfind 0.) else if pathfinding, try to find ladder in area if ladder = true in definition, then climb the ladder to the goal 11.) have mobs with player mesh able to equip armor and wield an item with armor mod 12.) Document each function with line number in release 13.) Mobs that build structures 14.) Traders 14.) Traders can use stock gui or a 3d representation of a gui that shows the item physically in the world Or in other words, mobs that add fun and aesthetic to the game, yet do not take away performance This is my first "professional" level mod which I hope to complete This is a huge undertaking, I will sometimes take breaks, days in between to avoid rage quitting, but will try to make updates as frequently as possible Rolling release to make updates in minetest daily (git) to bring out the best in the engine I use tab because it's the easiest for me to follow in my editor, I apologize if this is not the same for you I am also not the best speller, feel free to call me out on spelling mistakes CC0 to embrace the GNU principles followed by Minetest, you are allowed to relicense this to your hearts desires This mod is designed for the community's fun Idea sources https://en.wikipedia.org/wiki/Mob_(video_gaming) Help from tenplus1 https://github.com/tenplus1/mobs_redo/blob/master/api.lua Help from pilzadam https://github.com/PilzAdam/mobs/blob/master/api.lua --notes on how to create mobs the height will divide by 2 to find the height, make your mob mesh centered in the editor to fit centered in the collisionbox This is done to use the actual center of mobs in functions, same with width ]]-- --global to enable other mods/packs to utilize the ai open_ai = {} open_ai.mob_count = 0 open_ai.max_mobs = 20 -- limit the max number of mobs existing in the world dofile(minetest.get_modpath("open_ai").."/leash.lua") dofile(minetest.get_modpath("open_ai").."/safari_ball.lua") dofile(minetest.get_modpath("open_ai").."/spawning.lua") dofile(minetest.get_modpath("open_ai").."/fishing.lua") dofile(minetest.get_modpath("open_ai").."/commands.lua") open_ai.register_mob = function(name,def) minetest.register_entity("open_ai:"..name, { --Do simpler definition variables for ease of use mob = true, name = "open_ai:"..name, collisionbox = def.collisionbox,--{-def.width/2,-def.height/2,-def.width/2,def.width/2,def.height/2,def.width/2}, height = def.collisionbox[2], --sample from bottom of collisionbox - absolute for the sake of math width = math.abs(def.collisionbox[1]), --sample first item of collisionbox --vars for collision detection and floating overhang = def.collisionbox[5], collision_radius = def.collision_radius+0.5, -- collision sphere radius physical = def.physical, collide_with_objects = false, -- for magnetic collision max_velocity = def.max_velocity, acceleration = def.acceleration, hp_max = def.health, automatic_face_movement_dir = def.automatic_face_movement_dir, --for smoothness --Aesthetic variables visual = def.visual, mesh = def.mesh, textures = def.textures, makes_footstep_sound = def.makes_footstep_sound, animation = def.animation, visual_size = {x=def.visual_size.x, y=def.visual_size.y}, --Behavioral variables behavior_timer = 0, --when this reaches behavior change goal, it changes states and resets behavior_timer_goal = 0, --randomly selects between min and max time to change direction behavior_change_min = def.behavior_change_min, behavior_change_max = def.behavior_change_max, update_timer = 0, follow_item = def.follow_item, leash = def.leash, leashed = false, in_cart = false, rides_cart = def.rides_cart, rideable = def.rideable, --Physical variables old_position = nil, yaw = 0, jump_height = def.jump_height, float = def.float, liquid = 0, hurt_velocity= def.hurt_velocity, liquid_mob = def.liquid_mob, on_land = false, attached = nil, --Pathfinding variables path = {}, target = nil, following = false, --what mobs do when created on_activate = function(self, staticdata, dtime_s) --minetest.chat_send_all(open_ai.mob_count.." Mobs in world!") --debug for movement self.velocity = math.random(1,self.max_velocity)+math.random() self.behavior_timer_goal = math.random(self.behavior_change_min,self.behavior_change_max) local pos = self.object:getpos() --pos.y = pos.y - (self.height/2) -- the bottom of the entity --self.old_position = vector.floor(pos) self.yaw = (math.random(0, 360)/360) * (math.pi*2) if self.user_defined_on_activate then self.user_defined_on_activate(self, staticdata, dtime_s) end self.old_hp = self.object:get_hp() --create variable that can be added to pos to find center self.center = (self.overhang+self.height)/2 --create swim direction on activating if self.liquid_mob == true then self.swim_pitch = math.random(-self.max_velocity,self.max_velocity)+(math.random()*math.random(-1,1)) end end, --user defined function user_defined_on_activate = def.on_activate, --when the mob entity is deactivated get_staticdata = function(self) self.global_mob_counter(self) end, --used to tell if mob entity has despawned global_mob_counter = function(self) --print(dump(minetest.get_node_or_nil(pos))) --debug for limiting max mobs --do this to save a lot of resources vs a global table --automatically remove mob if dead if self.object:get_hp() <= 0 then open_ai.mob_count = open_ai.mob_count - 1 minetest.chat_send_all(open_ai.mob_count.." Mobs in world!") else--use assumption logic for mob counter minetest.after(0,function(self) local pos = self.object:getpos() local exists --for despawned mobs if pos == nil then exists = nil else exists = table.getn(minetest.get_objects_inside_radius(pos, 0.01)) end --print("static data global mob count") if exists == nil then open_ai.mob_count = open_ai.mob_count - 1 minetest.chat_send_all(open_ai.mob_count.." Mobs in world!") elseif exists > 0 then --limit the max amount of mobs in the world if self.activated == nil then if open_ai.mob_count+1 > open_ai.max_mobs then self.object:remove() minetest.chat_send_all(open_ai.max_mobs.." mob limit reached!") else open_ai.mob_count = open_ai.mob_count + 1 minetest.chat_send_all(open_ai.mob_count.." Mobs in world!") end --trigger to not readd mobs to global mob counter when already existing self.activated = true end end end,self) end end, --decide wether an entity should jump or change direction jump = function(self) --don't execute if liquid mob if self.liquid_mob == true then local vel = self.object:getvelocity() --use velocity calculation to find whether to jump local x = (math.sin(self.yaw) * -1) local z = (math.cos(self.yaw)) --reset the timer to change direction if (x~= 0 and vel.x == 0) or (z~= 0 and vel.z == 0) then self.behavior_timer = self.behavior_timer_goal end else local pos = self.object:getpos() --only jump when path step is higher up if self.following == true and self.leashed == false then --only try to jump if pathfinding exists if self.path and table.getn(self.path) > 1 then --don't jump if current position is equal to or higher than goal if vector.round(pos).y >= self.path[2].y then return end --don't jump if pathfinding doesn't exist else return end --find out if node is underneath local under_node = minetest.get_node({x=pos.x,y=pos.y+self.height-0.1,z=pos.z}).name local vel = self.object:getvelocity() if minetest.registered_nodes[under_node].walkable == true then --print("jump") self.object:setvelocity({x=vel.x,y=self.jump_height,z=vel.z}) end --stupidly jump elseif self.following == false and self.liquid == 0 and self.leashed == false then local vel = self.object:getvelocity() --return to save cpu if vel.y ~= 0 then return end --find out if node is underneath local under_node = minetest.get_node({x=pos.x,y=pos.y+self.height-0.1,z=pos.z}).name if minetest.registered_nodes[under_node].walkable == false then --print("JUMP FAILURE") return end local yaw = self.yaw --don't check if not moving instead change direction if yaw == yaw then --check for nan --use velocity calculation to find whether to jump local x = (math.sin(yaw) * -1) local z = (math.cos(yaw)) if (x~= 0 and vel.x == 0) or (z~= 0 and vel.z == 0) then self.object:setvelocity({x=vel.x,y=self.jump_height,z=vel.z}) end end elseif self.liquid ~= 0 then local vel = self.object:getvelocity() --commented out section is to use vel to get yaw dir, hence redeffing it as local yaw verus self.yaw local yaw = self.yaw--(math.atan(vel.z / vel.x) + math.pi / 2) --don't check if not moving instead change direction if yaw == yaw then --check for nan --use velocity calculation to find whether to jump local x = (math.sin(yaw) * -1) local z = (math.cos(yaw)) if (x~= 0 and vel.x == 0) or (z~= 0 and vel.z == 0) then self.object:setvelocity({x=vel.x,y=self.jump_height,z=vel.z}) end end end end end, --this runs everything that happens when a mob update timer resets update = function(self,dtime) self.update_timer = self.update_timer + dtime if self.update_timer >= 0.1 then self.update_timer = 0 self.jump(self) self.path_find(self) end end, --how a mob thinks behavior = function(self,dtime) self.behavior_timer = self.behavior_timer + dtime local vel = self.object:getvelocity() --debug to find node the mob exists in local testpos = self.object:getpos() testpos.y = testpos.y-- - (self.height/2) -- the bottom of the entity local vec_pos = vector.floor(testpos) -- the node that the mob exists in --debug test to change behavior if self.following == false and self.behavior_timer >= self.behavior_timer_goal and self.leashed == false then --print("Changed direction") --self.goal = {x=math.random(-self.max_velocity,self.max_velocity),y=math.random(-self.max_velocity,self.max_velocity),z=math.random(-self.max_velocity,self.max_velocity)} self.yaw = (math.random(0, 360)/360) * (math.pi*2) --double pi to allow complete rotation self.velocity = math.random(1,self.max_velocity)+math.random() self.behavior_timer_goal = math.random(self.behavior_change_min,self.behavior_change_max) self.behavior_timer = 0 --make fish swim up and down randomly if self.liquid_mob == true then self.swim_pitch = math.random(-self.max_velocity,self.max_velocity)+(math.random()*math.random(-1,1)) end --print("randomly moving around") elseif self.following == true then --print("following in behavior function") elseif self.leashed == true then self.leashed_function(self,dtime) end end, --when a mob is on a leash leashed_function = function(self,dtime) local pos = self.object:getpos() local pos2 = self.target:getpos() local c = 0 if self.target:is_player() then c = 1 end local vec = {x=pos.x-pos2.x,y=pos.y-pos2.y-c, z=pos.z-pos2.z} --how strong a leash is pulling up a mob self.leash_pull = vec.y --print(vec.x,vec.z) self.yaw = math.atan(vec.z/vec.x)+ math.pi / 2 if pos2.x > pos.x then self.yaw = self.yaw+math.pi end --do max velocity if distance is over 2 else stop moving local distance = vector.distance(pos,pos2) --run leash visual self.leash_visual(self,distance,pos,vec) if distance < 2 then distance = 0 end self.velocity = distance end, --if fish is on land, flop flop_on_land = function(self) --if caught then don't execute if self.object:get_attach() then return end local vel = self.object:getvelocity() local pos = self.object:getpos() --return to save cpu if vel.y ~= 0 then return end --find out if node is underneath local under_node = minetest.get_node({x=pos.x,y=pos.y+self.height-0.1,z=pos.z}).name if minetest.registered_nodes[under_node].walkable == false then --print("JUMP FAILURE") return end self.on_land = true --stop fish from moving around self.object:setvelocity({x=vel.x,y=self.jump_height,z=vel.z}) self.velocity = 0 self.behavior_timer = -5 --play flop sound minetest.sound_play("open_ai_flop", { pos = pos, max_hear_distance = 10, gain = 1.0, }) end, --a visual of the leash leash_visual = function(self,distance,pos,vec) --multiply times two if too far distance = math.floor(distance*2) --make this an int for this function --divide the vec into a step to run through in the loop local vec_steps = {x=vec.x/distance,y=vec.y/distance,z=vec.z/distance} --add particles to visualize leash for i = 1,math.floor(distance) do minetest.add_particle({ pos = {x=pos.x-(vec_steps.x*i), y=pos.y-(vec_steps.y*i), z=pos.z-(vec_steps.z*i)}, velocity = {x=0, y=0, z=0}, acceleration = {x=0, y=0, z=0}, expirationtime = 0.01, size = 1, collisiondetection = false, vertical = false, texture = "open_ai_leash_particle.png", }) end end, --how the mob collides with other mobs and players collision = function(self) local pos = self.object:getpos() pos.y = pos.y + self.height -- check bottom of mob local vel = self.object:getvelocity() local x = 0 local z = 0 for _,object in ipairs(minetest.env:get_objects_inside_radius(pos, 1)) do --only collide with other mobs and players --add exception if a nil entity exists around it if object:is_player() or (object:get_luaentity() and object:get_luaentity().mob == true and object ~= self.object) then local pos2 = object:getpos() local vec = {x=pos.x-pos2.x, z=pos.z-pos2.z} --push away harder the closer the collision is, could be used for mob cannons --+0.5 to add player's collisionbox, could be modified to get other mobs widths local force = (1) - vector.distance({x=pos.x,y=0,z=pos.z}, {x=pos2.x,y=0,z=pos2.z})--don't use y to get verticle distance --modify existing value to magnetize away from mulitiple entities/players x = x + (vec.x * force) * 20 z = z + (vec.z * force) * 20 --ride in a minecart elseif not object:is_player() and self.rides_cart == true and (object:get_luaentity() and object ~= self.object and object:get_luaentity().old_dir and object:get_luaentity().driver == nil) then self.ride_in_cart(self,object) end end return({x,z}) end, --logic for riding in cart ride_in_cart = function(self,object) --reset value if cart is removed local ride = self.object:get_attach() if ride == nil and self.in_cart == true then self.in_cart = false end if self.in_cart == false then self.object:set_attach(object, "", {x=0, y=0, z=0}, {x=0, y=0, z=0}) object:get_luaentity().driver = "open_ai:mob" self.in_cart = true end end, --how mobs move around when a player is riding it ridden = function(self) if self.attached ~= nil then if self.attached:is_player() then self.yaw = self.attached:get_look_horizontal() if self.attached:get_player_control().up == true then self.velocity = self.max_velocity else self.velocity = 0 end end end end, -- how a mob moves around the world movement = function(self) local collide_values = self.collision(self) local c_x = collide_values[1] local c_z = collide_values[2] self.ridden(self) --print(c_x,c_z) --move mob to goal velocity using acceleration for smoothness local vel = self.object:getvelocity() local x = math.sin(self.yaw) * -self.velocity local z = math.cos(self.yaw) * self.velocity --debug to float mobs for now local gravity = -10 self.swim(self) --this gets the viscosity of the liquid it's in if self.float == true and self.liquid ~= 0 and self.liquid ~= nil then gravity = self.liquid end --drag the mob up nodes with leash, or lift them up if self.leashed == true then if (x~= 0 and vel.x == 0) or (z~= 0 and vel.z == 0) then gravity = self.velocity elseif self.leash_pull < -3 then gravity = (self.leash_pull-3)*-1 end end --make mobs swim in water, fall back into it, if jumped out if self.liquid_mob == true and self.liquid ~= 0 then gravity = self.swim_pitch elseif self.liquid_mob == true and self.liquid == 0 then self.flop_on_land(self) end --only do goal y velocity if swimming up --print(self.velocity) --land mob if self.liquid_mob == false or self.liquid_mob == nil then if gravity == -10 then self.object:setacceleration({x=(x - vel.x + c_x)*self.acceleration,y=-10,z=(z - vel.z + c_z)*self.acceleration}) else self.object:setacceleration({x=(x - vel.x + c_x)*self.acceleration,y=(gravity-vel.y)*self.acceleration,z=(z - vel.z + c_z)*self.acceleration}) end elseif self.liquid_mob == true then--liquid mob if gravity == -10 and self.on_land == false then self.object:setacceleration({x=(x - vel.x + c_x)*self.acceleration,y=-10,z=(z - vel.z + c_z)*self.acceleration}) elseif gravity == -10 and self.on_land == true then self.object:setacceleration({x=(0 - vel.x + c_x)*self.acceleration,y=-10,z=(0 - vel.z + c_z)*self.acceleration}) else self.object:setacceleration({x=(x - vel.x + c_x)*self.acceleration,y=(gravity-vel.y)*self.acceleration,z=(z - vel.z + c_z)*self.acceleration}) end end end, follow_lure = function(self) end, swim = function(self) local pos = self.object:getpos() pos.y = pos.y + self.center self.liquid = minetest.registered_nodes[minetest.get_node(pos).name].liquid_viscosity --make land mobs slow down in water if self.liquid ~= 0 then if self.liquid_mob == nil or self.liquid_mob == false then self.velocity = self.liquid end end --reset the on_land variable if self.liquid ~= 0 and self.on_land == true then self.on_land = false end end, --check if a mob should follow a player when holding an item check_to_follow = function(self) --don't follow if leashed if self.leashed == true then return end self.following = false --liquid mobs follow lure if self.liquid_mob == true then local pos = self.object:getpos() for _,object in ipairs(minetest.env:get_objects_inside_radius(pos, 10)) do if not object:is_player() and object:get_luaentity() and object:get_luaentity().is_lure == true and object:get_luaentity().in_water == true and object:get_luaentity().attached == nil then local pos2 = object:getpos() local vec = {x=pos.x-pos2.x,y=pos2.y-pos.y, z=pos.z-pos2.z} --how strong a leash is pulling up a mob self.leash_pull = vec.y --print(vec.x,vec.z) local yaw = math.atan(vec.z/vec.x)+ math.pi / 2 if yaw == yaw then if pos2.x > pos.x then self.yaw = yaw+math.pi end self.yaw = yaw end --float up or down to lure self.swim_pitch = vec.y end end else local pos = self.object:getpos() for _,object in ipairs(minetest.env:get_objects_inside_radius(pos, 10)) do if object:is_player() then local item = object:get_wielded_item() if item:to_string() ~= "" and item:to_table().name == self.follow_item then self.following = true self.target = object else self.following = false end end end end end, --path finding towards goal - can be used to find food or water, or attack players or other mobs path_find = function(self) if self.following == true then self.velocity = self.max_velocity local pos1 = self.object:getpos() pos1.y = pos1.y + self.height local pos2 = self.target:getpos() -- this is the goal debug local path = nil --if can't get goal then don't pathfind if not pos2 then path = self.path else path = minetest.find_path(pos1,pos2,5,1,3,"Dijkstra") end --if in path step, delete it to not get stuck in place local vec_pos = vector.round(pos1) --print(vec_pos.x,vec_pos.z, self.path[2].x,self.path[2].z) if table.getn(self.path) > 1 then if vec_pos.x == self.path[2].x and vec_pos.z == self.path[2].z then print("delete first step") --self.path[1] = nil table.remove(self.path, 1) end end --Debug to visualize mob paths if table.getn(self.path) > 0 then for _,pos in pairs(self.path) do minetest.add_particle({ pos = pos, velocity = {x=0, y=0, z=0}, acceleration = {x=0, y=0, z=0}, expirationtime = 0.1, size = 4, collisiondetection = false, vertical = false, texture = "heart.png", }) end end --debug pathfinding local pos3 = nil --create a path internally if path then self.path = path end --follow internal path if self.path and table.getn(self.path) > 1 then --the second step in the path pos3 = self.path[2] --display the path goal minetest.add_particle({ pos = pos3, velocity = {x=0, y=0, z=0}, acceleration = {x=0, y=0, z=0}, expirationtime = 0.1, size = 4, collisiondetection = false, vertical = false, texture = "default_stone.png", }) else --print("less than 2 steps, stop") self.velocity = 0 end if pos3 then local vec = {x=pos1.x-pos3.x, z=pos1.z-pos3.z} --print(vec.x,vec.z) self.yaw = math.atan(vec.z/vec.x)+ math.pi / 2 if pos3.x > pos1.x then self.yaw = self.yaw+math.pi end else --print("failure in pathfinding") end end end, --mob velocity damage x,y, and z velocity_damage = function(self,dtime) local vel = self.object:getvelocity() if self.old_vel then if (vel.x == 0 and self.old_vel.x ~= 0) or (vel.y == 0 and self.old_vel.y ~= 0) or (vel.z == 0 and self.old_vel.z ~= 0) then local diff = vector.subtract(vel, self.old_vel) diff.x = math.ceil(math.abs(diff.x)) diff.y = math.ceil(math.abs(diff.y)) diff.z = math.ceil(math.abs(diff.z)) local punches = 0 --2 hearts of damage every 2 points over hurt_velocity if diff.x > self.hurt_velocity then punches = punches + diff.x end if diff.y > self.hurt_velocity then punches = punches + diff.y end if diff.z > self.hurt_velocity then punches = punches + diff.z end --hurt entity and set texture modifier if punches > 0 then self.object:punch(self.object, 1.0, { full_punch_interval=1.0, damage_groups = {fleshy=punches} }, nil) end end end --this is created here because it is unnecasary to define it in initial properties self.old_vel = vel end, --check if mob is hurt and show damage check_for_hurt = function(self,dtime) local hp = self.object:get_hp() if hp < self.old_hp then --run texture function self.hurt_texture(self,(self.old_hp-hp)/4) --allow user to do something when hurt if self.user_on_hurt then self.user_on_hurt(self,self.old_hp-hp) end end self.old_hp = hp end, user_on_hurt = def.on_hurt, --makes a mob turn red when hurt hurt_texture = function(self,punches) self.fall_damaged_timer = 0 self.fall_damaged_limit = punches end, --makes a mob turn back to normal after being hurt hurt_texture_normalize = function(self,dtime) --reset the mob texture and timer if self.fall_damaged_timer ~= nil then self.object:settexturemod("^[colorize:#ff0000:100") self.fall_damaged_timer = self.fall_damaged_timer + dtime if self.fall_damaged_timer >= self.fall_damaged_limit then self.object:settexturemod("") self.fall_damaged_timer = nil self.fall_damaged_limit = nil end end end, --how the mob sets it's mesh animation set_animation = function(self,dtime) local vel = self.object:getvelocity() local speed = (math.abs(vel.x)+math.abs(vel.z))*self.animation.speed_normal --check this self.object:set_animation({x=self.animation.walk_start,y=self.animation.walk_end}, speed, 0, true) --run this in here because it is part of animation and textures self.hurt_texture_normalize(self,dtime) end, --what happens when you hit a mob on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir) if self.user_defined_on_punch then self.user_defined_on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir) end if self.object:get_hp() <= 0 then self.global_mob_counter(self) --remove from global mob count if self.user_defined_on_die then self.user_defined_on_die(self, puncher, time_from_last_punch, tool_capabilities, dir) end end end, --user defined user_defined_on_punch = def.on_punch, user_defined_on_die = def.on_die, --what happens when you right click a mob on_rightclick = function(self, clicker) --initialize riding the horse if self.rideable == true then if self.attached == nil and self.leashed == false then self.attached = clicker clicker:set_attach(self.object, "", {x=0, y=0, z=0}, {x=0, y=0, z=0}) elseif self.attached ~= nil then self.attached:set_detach() self.attached = nil end end --undo leash if self.leashed == true then self.leashed = false self.target = nil return end if self.user_defined_on_rightclick then self.user_defined_on_rightclick(self, clicker) end end, --user defined user_defined_on_rightclick = def.on_rightclick, --what mobs do on each server step on_step = function(self,dtime) self.check_for_hurt(self,dtime) self.check_to_follow(self) self.behavior(self,dtime) self.update(self,dtime) self.set_animation(self,dtime) self.movement(self) self.velocity_damage(self,dtime) if self.user_defined_on_step then self.user_defined_on_step(self,dtime) end end, --a function that users can define user_defined_on_step = def.on_step, }) open_ai.register_safari_ball("open_ai:"..name,def.ball_color,math.abs(def.collisionbox[2])) end --run api call dofile(minetest.get_modpath("open_ai").."/mobs.lua")