open_ai/init.lua
2016-12-30 20:24:14 -05:00

1318 lines
42 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
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 = 2000 -- 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")
dofile(minetest.get_modpath("open_ai").."/items.lua")
dofile(minetest.get_modpath("open_ai").."/rayguns.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],
--create variable that can be added to pos to find center
center = (def.collisionbox[5]+def.collisionbox[2])/2,
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},
eye_offset = def.eye_offset,
visual_offset = def.visual_offset,
player_pose = def.player_pose,
--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,
--taming variables
tameable = def.tameable,
tame_item = def.tame_item,
owner = nil,
owner_name = nil,
tamed = false,
tame_click_min = def.tame_click_min,
tame_click_max = def.tame_click_max,
--chair variables - what the player sits on
mob_chair = def.mob_chair,
has_chair = false,
chair_textures = def.chair_textures,
--Physical variables
old_position = nil,
yaw = 0,
jump_timer = 0,
jump_height = def.jump_height,
float = def.float,
liquid = 0,
hurt_velocity= def.hurt_velocity,
liquid_mob = def.liquid_mob,
attached = nil,
attached_name= nil,
jump_only = def.jump_only,
jumped = false,
scale_size = 1,
--Pathfinding variables
path = {},
target = nil,
target_name = nil,
following = false,
--what mobs do when created
on_activate = function(self, staticdata, dtime_s)
--print("activating at "..dump(self.object:getpos()))
if string.sub(staticdata, 1, string.len("return")) == "return" then
local data = minetest.deserialize(staticdata)
for key,value in pairs(data) do
self[key] = value
end
end
--keep hp
if self.old_hp then
self.object:set_hp(self.old_hp)
end
--keep leashes connected when respawning
if self.target_name then
self.target = minetest.get_player_by_name(self.target_name)
end
--keep object riding
if self.attached_name then
self.attached = minetest.get_player_by_name(self.attached_name)
self.attached:set_attach(self.object, "", {x=0, y=self.visual_offset, z=0}, {x=0, y=self.automatic_face_movement_dir+90, z=0})
if self.attached:is_player() == true then
self.attached:set_properties({
visual_size = {x=1/self.visual_size.x, y=1/self.visual_size.y},
})
--set players eye offset for mob
self.attached:set_eye_offset({x=0,y=self.eye_offset,z=0},{x=0,y=0,z=0})
end
end
--set the amount of times a player has to feed the mob to tame it
if not self.tame_amount and self.tameable == true then
self.tame_amount = math.random(self.tame_click_min,self.tame_click_max)
end
--re apply the mob chair texture
if self.has_chair and self.has_chair == true then
self.object:set_properties({textures = self.chair_textures})
end
--re apply collisionbox and visualsize
if self.scale_size ~= 1 and self.collisionbox and self.visual_size then
self.object:set_properties({collisionbox = self.collisionbox,visual_size = self.visual_size})
end
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)
--print("staticdata at "..dump(self.object:getpos()))
--self.global_mob_counter(self)
local serialize_table = {}
for key,value in pairs(self) do
--don't get object item
if key ~= "object" then
--don't do userdata
if type(value) == "userdata" then
value = nil
end
serialize_table[key] = value
end
end
--manually save collisionbox
serialize_table["collisionbox"] = self.collisionbox
local value_string = minetest.serialize(serialize_table)
return(value_string)
end,
--used to tell if mob entity has despawned
global_mob_counter = function(self)
--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,
--allow players to make mob jump when riding mobs
ridden_jump = function(self,dtime)
if self.attached ~= nil then
local pos = self.object:getpos()
local vel = self.object:getvelocity()
if self.attached:is_player() then
if self.attached:get_player_control().jump == true then
--jump only if standing on node
if self.liquid == 0 then
--return to save cpu
if vel.y ~= 0 or (self.old_velocity_y and self.old_velocity_y > 0) or (self.old_velocity_y == nil) then
--print("velocity failure")
return
end
--use velocity calculation to find whether to jump
local x = (math.sin(self.yaw) * -1) * self.velocity
local z = (math.cos(self.yaw)) * self.velocity
self.object:setvelocity({x=x,y=self.jump_height,z=z})
self.jumped = true
--always allowed to jump in water
elseif self.liquid ~= 0 then
--use velocity calculation to find whether to jump
local x = (math.sin(self.yaw) * -1) * self.velocity
local z = (math.cos(self.yaw)) * self.velocity
self.object:setvelocity({x=x,y=self.jump_height,z=z})
self.jumped = true
end
end
end
end
end,
--decide wether an entity should jump or change direction
jump = function(self,dtime)
self.jump_timer = self.jump_timer + dtime
if self.jump_timer >= 0.25 then
self.jump_timer = 0
--only jump on it's own if player is not riding
if self.attached == nil then
local vel = self.object:getvelocity()
--don't execute if liquid mob
if self.liquid_mob == true then
--use this calc to find if it should change direction
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
--put this here because it's only used by pathfinding jumping
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
--return to save cpu
if vel.y ~= 0 or (self.old_velocity_y and self.old_velocity_y > 0) or (self.old_velocity_y == nil) then
--print("velocity failure")
return
end
--return if nan
if self.yaw ~= self.yaw then
return
end
--use velocity calculation to find whether to jump
local x = (math.sin(self.yaw) * -1) * self.velocity
local z = (math.cos(self.yaw)) * self.velocity
self.object:setvelocity({x=x,y=self.jump_height,z=z})
self.jumped = true
--stupidly jump
elseif self.following == false and self.liquid == 0 and self.leashed == false then
--return to save cpu
if vel.y ~= 0 or (self.old_velocity_y and self.old_velocity_y > 0) or (self.old_velocity_y == nil) then
--print("velocity failure")
return
end
--use velocity calculation to find whether to jump
local x = (math.sin(self.yaw) * -1) * self.velocity
local z = (math.cos(self.yaw)) * self.velocity
if (x~= 0 and vel.x == 0) or (z~= 0 and vel.z == 0) then
self.object:setvelocity({x=x,y=self.jump_height,z=z})
self.jumped = true
end
elseif self.liquid ~= 0 then
--use velocity calculation to find whether to jump
local x = (math.sin(self.yaw) * -1) * self.velocity
local z = (math.cos(self.yaw)) * self.velocity
if (x~= 0 and vel.x == 0) or (z~= 0 and vel.z == 0) then
self.object:setvelocity({x=x,y=self.jump_height,z=z})
self.jumped = true
end
end
end
end
end
end,
user_defined_on_jump = def.on_jump,
--jump only mob movements
jumping_movement = function(self,dtime)
self.jump_timer = self.jump_timer + dtime
--print(self.jump_timer)
--only check to jump every half second
if self.jump_timer >= 0.5 then
local pos = self.object:getpos()
local vel = self.object:getvelocity()
self.jump_timer = 0
--return to save cpu
if vel.y ~= 0 or (self.old_velocity_y and self.old_velocity_y > 0) or (self.old_velocity_y == nil) then
--print("velocity failure")
return
end
--use velocity calculation to find whether to jump
local x = (math.sin(self.yaw) * -1) * self.velocity
local z = (math.cos(self.yaw)) * self.velocity
self.object:setvelocity({x=x,y=self.jump_height,z=z})
self.jumped = true
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.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
--normal direction changing, or if jump_only, only change direction on ground
if self.jump_only ~= true or (self.jump_only == true and vel.y == 0) 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
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)
--exception for if mob spawns with player that is not the leash owner
if not self.target or (self.target and self.target:is_player() and self.target:getpos() == nil) then
--print("fail player")
self.target = nil
self.target_name = nil
self.leashed = false
return
end
--exception for if mob spawns without other mob in world
if not self.target or (self.target and self.target:getpos() == nil) then
--print("fail mob")
self.target = nil
self.target_name = nil
self.leashed = false
return
end
local pos = self.object:getpos()
local pos2 = self.target:getpos()
--auto configure the center of objects
pos.y = pos.y + self.center
if self.target:is_player() then
pos2.y = pos2.y + 1
else
pos2.y = pos2.y + self.target:get_luaentity().center
end
local vec = {x=pos.x-pos2.x,y=pos.y-pos2.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)
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.object:setvelocity({x=vel.x,y=self.jump_height,z=vel.z})
self.jumped = true
self.velocity = 0
--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)
--only allow owners to ride
if self.tamed == true and self.attached ~= nil and self.attached:get_player_name() == self.owner_name then
if self.attached:is_player() then
self.yaw = self.attached:get_look_horizontal()
if self.attached:get_player_control().up == true then
if self.has_chair and self.has_chair == true then
self.velocity = self.max_velocity * 1.5 --double the speed if wearing a chair
else
self.velocity = self.max_velocity
end
else
self.velocity = 0
end
end
end
end,
-- how a mob moves around the world
movement = function(self,dtime)
--if jump_only mob then only jump
if self.jump_only == true then
self.jumping_movement(self,dtime)
--else normal jumping
else
self.jump(self,dtime)
end
self.ridden_jump(self,dtime)--allow players to jump while they ride mobs
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
local gravity = -10
self.swim(self) --this gets the viscosity of the liquid it's in
--mobs that float float
if self.float == true and self.liquid ~= 0 and self.liquid ~= nil then
gravity = self.liquid
--make mobs that can't swim sink to the bottom
elseif (self.float == nil or self.float == false) 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
--execute player defined function
if self.jumped == true and self.user_defined_on_jump then
self.user_defined_on_jump(self,dtime)
self.jumped = false
end
--stop constant motion if stopped
--[[
if (math.abs(vel.x) < 0.1 and math.abs(vel.z) < 0.1) and (vel.x ~= 0 and vel.z ~= 0) and self.velocity == 0 then
self.object:setvelocity({x=0,y=vel.y,z=0})
--only apply gravity if stopped
elseif self.velocity == 0 and (math.abs(vel.x) < 0.1 and math.abs(vel.z) < 0.1) then
self.object:setacceleration({x=0,y=-10,z=0})
--stop motion if trying to stop
elseif self.velocity == 0 and (math.abs(vel.x) > 0.1 or math.abs(vel.z) > 0.1) then
self.object:setacceleration({x=(0 - vel.x + c_x)*self.acceleration,y=-10,z=(0 - vel.z + c_z)*self.acceleration})
--do normal things
elseif self.velocity ~= 0 then
]]--
--land mob
if self.liquid_mob == false or self.liquid_mob == nil then
--jump only mobs
if self.jump_only == true then
--fall and stop because jump_only mobs only jump around to move
if gravity == -10 and vel.y == 0 then
self.object:setacceleration({x=(0 - vel.x + c_x)*self.acceleration,y=-10,z=(0 - vel.z + c_z)*self.acceleration})
--move around normally if jumping
elseif gravity == -10 and vel.y ~= 0 then
self.object:setacceleration({x=(x - vel.x + c_x)*self.acceleration,y=-10,z=(z - vel.z + c_z)*self.acceleration})
--allow jump only mobs to swim
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
--normal walking mobs
else
--fall
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})
--swim
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
--liquid mob
elseif self.liquid_mob == true then
--out of water
if gravity == -10 and self.liquid == 0 then
self.object:setacceleration({x=(0 - vel.x + c_x)*self.acceleration,y=-10,z=(0 - vel.z + c_z)*self.acceleration})
--swimming
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
--this is used for jumping
self.old_velocity_y = vel.y
end,
--slow down mobs in water and allow water mobs to swim around
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
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 self.old_hp and 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()
--only use jump animation for jump only mobs
if self.jump_only == true then
--set animation if jumping
--future note, this is the function that should be used when setting the jump animation for normal mobs
if vel.y == 0 and (self.old_velocity_y and self.old_velocity_y < 0) then
self.object:set_animation({x=self.animation.jump_start,y=self.animation.jump_end}, self.animation.speed_normal, 0, false)
minetest.after(self.animation.speed_normal/100, function(self)
self.object:set_animation({x=self.animation.stand_start,y=self.animation.stand_end}, self.animation.speed_normal, 0, true)
end,self)
end
--do normal walking animations
else
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)
--set the riding player's animation to sitting
if self.attached and self.attached:is_player() and self.player_pose then
self.attached:set_animation(self.player_pose, 30,0)
end
end
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
--return player back to normal scale
if self.attached then
if self.attached:is_player() == true then
self.attached:set_properties({
visual_size = {x=1, y=1},
})
--revert back to normal
self.attached:set_eye_offset({x=0,y=0,z=0},{x=0,y=0,z=0})
end
end
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 a player tries to ride the mob
try_to_ride = function(self,clicker)
local item = clicker:get_wielded_item()
--don't ride if putting on mob chair
if self.has_chair and self.has_chair == false or (item:to_string() ~= "" and item:to_table().name == self.mob_chair) then
return
end
--initialize riding the horse
if self.rideable == true and self.tamed == true and clicker:get_player_name() == self.owner_name then
if self.attached == nil and self.leashed == false then
self.attached = clicker
self.attached_name = clicker:get_player_name()
self.attached:set_attach(self.object, "", {x=0, y=self.visual_offset, z=0}, {x=0, y=self.automatic_face_movement_dir+90, z=0})
--sit animation
if self.attached:is_player() == true then
self.attached:set_properties({
visual_size = {x=1/self.visual_size.x, y=1/self.visual_size.y},
})
--set players eye offset for mob
self.attached:set_eye_offset({x=0,y=self.eye_offset,z=0},{x=0,y=0,z=0})
end
elseif self.attached ~= nil then
--normal animation
if self.attached:is_player() == true then
self.attached:set_properties({
visual_size = {x=1, y=1},
})
--revert back to normal
self.attached:set_eye_offset({x=0,y=0,z=0},{x=0,y=0,z=0})
end
self.attached:set_detach()
self.attached_name = nil
self.attached = nil
end
end
end,
taming = function(self,clicker)
--disalow mobs that can't be tamed or mobs that are already tamed
if not self.tameable or (self.tameable == false or self.tamed == true) then
return
end
local item = clicker:get_wielded_item()
if item:to_string() ~= "" and item:to_table().name == self.tame_item then
item:take_item(1)
clicker:set_wielded_item(item)
self.tame_amount = self.tame_amount - 1
end
if self.tame_amount <= 0 then
self.tamed = true
self.owner = clicker
self.owner_name = clicker:get_player_name()
local pos = self.object:getpos()
minetest.add_particlespawner({
amount = 50,
time = 0.001,
minpos = pos,
maxpos = pos,
minvel = {x=-6, y=3, z=-6},
maxvel = {x=6, y=8, z=6},
minacc = {x=0, y=-10, z=0},
maxacc = {x=0, y=-10, z=0},
minexptime = 1,
maxexptime = 2,
minsize = 1,
maxsize = 2,
collisiondetection = false,
vertical = false,
texture = "heart.png",
})
end
end,
--how a player puts a "chair" on a mob
place_chair = function(self,clicker)
if self.tameable == false or self.tamed == false or self.rideable == false or self.mob_chair == nil or self.has_chair == true then
return
end
local item = clicker:get_wielded_item()
if item:to_string() ~= "" and item:to_table().name == self.mob_chair then
item:take_item(1)
clicker:set_wielded_item(item)
self.has_chair = true
self.object:set_properties({textures = self.chair_textures})
end
end,
--what happens when you right click a mob
on_rightclick = function(self, clicker)
self.try_to_ride(self,clicker)
self.place_chair(self,clicker) -- this after try to ride so player puts on chair before riding
self.taming(self,clicker)
--undo leash
if self.leashed == true then
self.leashed = false
self.target = nil
self.target_name = 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,
--How a mob changes it's size
change_size = function(self,dtime)
--initialize this variable here for testing
if self.grow_timer == nil then
return
end
self.grow_timer = self.grow_timer - dtime
--limit ray size
if self.grow_timer <= 0 or ((self.scale_size > 5 and self.size_change > 0) or (self.scale_size < -5 and self.size_change < 0)) then
print("size too big or too small")
self.grow_timer = nil
self.size_change = nil
return
end
--change based on variable
local size_multiplier = 1.1
if self.size_change < 0 then
print("shrink")
self.scale_size = self.scale_size / 1.1
--iterate through collisionbox
for i = 1,table.getn(self.collisionbox) do
self.collisionbox[i] = self.collisionbox[i] / size_multiplier
end
self.visual_size = {x=self.visual_size.x / size_multiplier, y = self.visual_size.y / size_multiplier}
elseif self.size_change > 0 then
print("grow")
self.scale_size = self.scale_size * 1.1
--iterate through collisionbox
for i = 1,table.getn(self.collisionbox) do
self.collisionbox[i] = self.collisionbox[i] * size_multiplier
end
self.visual_size = {x=self.visual_size.x * size_multiplier, y = self.visual_size.y * size_multiplier}
end
--self.collisionbox[2] = self.collisionbox[2] - dtime
self.height = self.collisionbox[2]
self.width = math.abs(self.collisionbox[1])
--vars for collision detection and floating
self.overhang = self.collisionbox[5]
--create variable that can be added to pos to find center
self.center = (self.collisionbox[5]+self.collisionbox[2])/2
--attempt to set the collionbox to internal yadayada
self.object:set_properties({collisionbox = self.collisionbox,visual_size=self.visual_size})
end,
--what mobs do on each server step
on_step = function(self,dtime)
self.change_size(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,dtime)
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")