open_ai/init.lua
jordan4ibanez f69c591a45 Fix crash
2016-12-21 23:09:05 -05:00

683 lines
21 KiB
Lua

--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
Fishing
fish mobs are drawn to lures
make fish mobs drown and flop around on land
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
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")
open_ai.register_mob = function(name,def)
minetest.register_entity(name, {
--Do simpler definition variables for ease of use
mob = true,
name = name,
collisionbox = {-def.width/2,-def.height/2,-def.width/2,def.width/2,def.height/2,def.width/2},
height = def.height,
width = def.width,
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,
--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,
--Physical variables
old_position = nil,
yaw = 0,
jump_height = def.jump_height,
float = def.float,
liquid = 0,
--Pathfinding variables
path = {},
target = "singleplayer",
following = false,
--what mobs do when created
on_activate = function(self, staticdata, dtime_s)
--debug for max mobs
open_ai.mob_count = open_ai.mob_count + 1
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
end,
--user defined function
user_defined_on_activate = def.on_activate,
--when the mob entity is deactivated
get_staticdata = function(self)
if self.activated == true then
open_ai.mob_count = open_ai.mob_count - 1
minetest.chat_send_all(open_ai.mob_count.." Mobs in world!")
end
self.activated = true
end,
--decide wether an entity should jump or change direction
jump = function(self)
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/2)-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 then
--find out if node is underneath
local under_node = minetest.get_node({x=pos.x,y=pos.y-(self.height/2)-0.1,z=pos.z}).name
if minetest.registered_nodes[under_node].walkable == false then
--print("JUMP FAILURE")
return
end
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
--turn it into usable position modifier
local x = (math.sin(yaw) * -1)*1.5
local z = (math.cos(yaw))*1.5
local node = minetest.get_node({x=pos.x+x,y=pos.y,z=pos.z+z}).name
if minetest.registered_nodes[node].walkable == true then
--print("jump")
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
--turn it into usable position modifier
local x = (math.sin(yaw) * -1)*1.5
local z = (math.cos(yaw))*1.5
local node = minetest.get_node({x=pos.x+x,y=pos.y,z=pos.z+z}).name
if minetest.registered_nodes[node].walkable == true then
--print("liquid jump")
self.object:setvelocity({x=vel.x,y=self.jump_height,z=vel.z})
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
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}
--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,
--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()
local vel = self.object:getvelocity()
local x = 0
local z = 0
for _,object in ipairs(minetest.env:get_objects_inside_radius(pos, self.width)) 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 = (self.width+0.5) - 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 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]
--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)
if self.float == true and self.liquid ~= 0 and self.liquid ~= nil then
gravity = self.liquid
end
--only do goal y velocity if swimming up
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
end,
swim = function(self)
self.liquid = minetest.registered_nodes[minetest.get_node(self.object:getpos()).name].liquid_viscosity
if self.liquid ~= 0 then
self.velocity = self.liquid
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
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
else
self.following = false
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()
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,
--how the mob sets it's mesh animation
set_animation = function(self)
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)
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
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)
--undo leash
if self.leashed == true then
self.leashed = false
return
end
--[[
local item = clicker:get_wielded_item()
if item:to_string() ~= "" then
if item:to_table().name == "open_ai:leash" then
self.target = clicker
self.leashed = true
end
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_to_follow(self)
self.behavior(self,dtime)
self.update(self,dtime)
self.set_animation(self)
self.movement(self)
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(name,def.ball_color)
end
--this is a test mob which can be used to learn how to make mobs using open ai - uses pilzadam's sheep mesh
open_ai.register_mob("open_ai:test",{
--mob physical variables
height = 0.7, --divide by 2 for even height
width = 0.7, --divide by 2 for even width
physical = true, --if the mob collides with the world, false is useful for ghosts
jump_height = 5, --how high a mob will jump
health = 20,
--mob movement variables
max_velocity = 3, --set the max velocity that a mob can move
acceleration = 3, --how quickly a mob gets up to max velocity
behavior_change_min = 3, -- the minimum time a mob will wait to change it's behavior
behavior_change_max = 5, -- the max time a mob will wait to change it's behavior
float = true, --if a mob tries to swim in liquids
--mob aesthetic variables
visual = "mesh", --can be changed to anything for flexibility
mesh = "mobs_sheep.x",
textures = {"mobs_sheep.png"},
animation = { --the animation keyframes and speed
speed_normal = 10,--animation speed
stand_start = 0,--standing animation start and end
stand_end = 80,
walk_start = 81,--walking animation start and end
walk_end = 100,
},
automatic_face_movement_dir = -90.0, --what direction the mob faces in
makes_footstep_sound = true, --if a mob makes footstep sounds
--mob behavior variables
follow_item = "default:dry_grass_1", --if you're holding this a peaceful mob will follow you
leash = true,
rides_cart = true,
--safari ball variables
ball_color = "FF0000",--color in hex, can be any color
--user defined functions
on_step = function(self,dtime)
--print("test")
end,
on_activate = function(self, staticdata, dtime_s)
--print("activating")
end,
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
--print("hit")
end,
on_die = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
--print("poof")
end,
on_rightclick = function(self, clicker)
--print("right clicked")
end,
})