2020-04-30 22:28:00 +02:00
|
|
|
--------------------------------------------------------
|
|
|
|
-- Minetest :: Mobs Lite Mod (mobs)
|
|
|
|
--
|
|
|
|
-- See README.txt for licensing and release notes.
|
|
|
|
-- Copyright (c) 2016-2020, Leslie E. Krause
|
|
|
|
--
|
|
|
|
-- ./games/minetest_game/mods/mobs/init.lua
|
|
|
|
--------------------------------------------------------
|
|
|
|
|
|
|
|
mobs = { }
|
|
|
|
|
2020-05-03 22:58:00 +02:00
|
|
|
local registry = {
|
|
|
|
players = { },
|
|
|
|
avatars = { },
|
2020-05-08 02:27:00 +02:00
|
|
|
objects = { },
|
2020-05-03 22:58:00 +02:00
|
|
|
spawnitems = { },
|
|
|
|
}
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-07 11:17:00 +02:00
|
|
|
local world_gravity = -10
|
|
|
|
local liquid_density = 0.5
|
|
|
|
local liquid_viscosity = 0.6
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
local next_noise_id = 1
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
--------------------
|
|
|
|
|
|
|
|
local random = math.random
|
|
|
|
local floor = math.floor
|
|
|
|
local min = math.min
|
|
|
|
local max = math.max
|
|
|
|
local sqrt = math.sqrt
|
|
|
|
local pow = math.pow
|
|
|
|
local abs = math.abs
|
|
|
|
local pi = math.pi
|
|
|
|
local atan2 = math.atan2
|
|
|
|
local sin = math.sin
|
|
|
|
local cos = math.cos
|
|
|
|
|
|
|
|
local rad_360 = 2 * pi
|
|
|
|
local rad_180 = pi
|
|
|
|
local rad_90 = pi / 2
|
|
|
|
local rad_60 = pi / 3
|
|
|
|
local rad_45 = pi / 4
|
|
|
|
local rad_30 = pi / 6
|
|
|
|
local rad_20 = pi / 9
|
2020-05-07 11:17:00 +02:00
|
|
|
local rad_10 = pi / 18
|
|
|
|
local rad_5 = pi / 36
|
2020-04-30 22:28:00 +02:00
|
|
|
local rad_0 = 0
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-05-07 11:17:00 +02:00
|
|
|
function Timekeeper( this )
|
2020-04-30 22:28:00 +02:00
|
|
|
local timer_defs = { }
|
2020-05-16 20:41:00 +02:00
|
|
|
local pending_timer_defs = { }
|
2020-04-30 22:28:00 +02:00
|
|
|
local clock = 0.0
|
|
|
|
local delay = 0.0
|
|
|
|
local self = { }
|
|
|
|
|
|
|
|
self.shift = function ( dtime )
|
|
|
|
delay = delay + dtime
|
|
|
|
end
|
|
|
|
|
|
|
|
self.unshift = function ( )
|
|
|
|
delay = 0.0
|
|
|
|
end
|
|
|
|
|
|
|
|
self.start = function ( period, name, func )
|
2020-05-16 20:41:00 +02:00
|
|
|
timer_defs[ name ] = nil
|
|
|
|
pending_timer_defs[ name ] = { cycles = 0, period = period, expiry = clock + delay + period, started = clock, func = func }
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
self.start_now = function ( period, name, func )
|
2020-05-16 20:41:00 +02:00
|
|
|
timer_defs[ name ] = nil
|
2020-05-14 03:46:00 +02:00
|
|
|
if not func( this, 0, period, 0.0, 0.0 ) then
|
2020-05-16 20:41:00 +02:00
|
|
|
pending_timer_defs[ name ] = { cycles = 0, period = period, expiry = clock + period, started = clock, func = func }
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
self.clear = function ( name )
|
2020-05-16 20:41:00 +02:00
|
|
|
pending_timer_defs[ name ] = nil
|
2020-04-30 22:28:00 +02:00
|
|
|
timer_defs[ name ] = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
self.on_step = function ( dtime )
|
|
|
|
clock = clock + dtime
|
|
|
|
|
2020-05-16 20:41:00 +02:00
|
|
|
for k, v in pairs( pending_timer_defs ) do
|
|
|
|
timer_defs[ k ] = v
|
|
|
|
pending_timer_defs[ k ] = nil
|
|
|
|
end
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
local timers = { }
|
|
|
|
for k, v in pairs( timer_defs ) do
|
|
|
|
if clock >= v.expiry and clock > v.started then
|
2020-05-08 02:27:00 +02:00
|
|
|
v.expiry = clock + v.period
|
|
|
|
v.cycles = v.cycles + 1
|
2020-04-30 22:28:00 +02:00
|
|
|
-- callback( this, cycles, period, elapsed, overrun )
|
|
|
|
if v.func and v.func( this, v.cycles, v.period, clock - v.started, clock - v.expiry ) then
|
|
|
|
self.clear( k )
|
|
|
|
end
|
|
|
|
timers[ k ] = v
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return timers
|
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-05-09 14:34:00 +02:00
|
|
|
mobs.effect = function ( pos, amount, texture, min_size, max_size, radius, gravity )
|
2020-04-30 22:28:00 +02:00
|
|
|
minetest.add_particlespawner({
|
|
|
|
amount = amount,
|
|
|
|
time = 0.5,
|
|
|
|
minpos = { x = pos.x, y = pos.y, z = pos.z },
|
|
|
|
maxpos = { x = pos.x, y = pos.y + 1.5, z = pos.z },
|
|
|
|
minvel = {x = -radius, y = -radius, z = -radius},
|
|
|
|
maxvel = {x = radius, y = radius, z = radius},
|
|
|
|
minacc = {x = 0, y = gravity, z = 0},
|
|
|
|
maxacc = {x = 0, y = gravity, z = 0},
|
|
|
|
minexptime = 0.1,
|
|
|
|
maxexptime = 1,
|
|
|
|
minsize = min_size,
|
|
|
|
maxsize = max_size,
|
|
|
|
texture = texture,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
local function smoke_effect( pos )
|
2020-05-05 02:41:00 +02:00
|
|
|
mobs.effect( pos, 8, "tnt_smoke.png", 1.4, 1.6, 2, 0 )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
local function blood_effect( pos )
|
2020-05-05 02:41:00 +02:00
|
|
|
mobs.effect( pos, 4, "mobs_blood.png", 1.2, 1.4, 2, -10 )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-05-08 00:30:00 +02:00
|
|
|
mobs.iterate_registry = function ( source_pos, radius, height, classes )
|
|
|
|
local length = radius * radius
|
|
|
|
local class_id = next( classes )
|
|
|
|
local key
|
|
|
|
|
|
|
|
local function is_inside_area( obj )
|
|
|
|
-- perform fast length-squared distance check
|
|
|
|
local target_pos = obj:get_pos( )
|
|
|
|
local a = source_pos.x - target_pos.x
|
|
|
|
local b = source_pos.z - target_pos.z
|
|
|
|
|
|
|
|
return a * a + b * b <= length and abs( source_pos.y - target_pos.y ) <= height
|
|
|
|
end
|
|
|
|
|
|
|
|
return function ( )
|
|
|
|
while class_id and classes[ class_id ] do
|
|
|
|
local obj
|
|
|
|
|
|
|
|
key, obj = next( registry[ class_id ], key )
|
|
|
|
if obj then
|
|
|
|
if obj:get_hp( ) > 0 and is_inside_area( obj ) then
|
|
|
|
return obj
|
|
|
|
end
|
|
|
|
else
|
|
|
|
class_id = next( classes, class_id )
|
|
|
|
key = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
local function node_locator( pos, size, time, color )
|
|
|
|
if is_debug then
|
|
|
|
minetest.add_particle( {
|
|
|
|
pos = pos,
|
2020-05-05 02:41:00 +02:00
|
|
|
velocity = { x=0, y=0, z=0 },
|
|
|
|
acceleration = { x=0, y=0, z=0 },
|
2020-04-30 22:28:00 +02:00
|
|
|
exptime = time + 4,
|
|
|
|
size = size,
|
|
|
|
collisiondetection = false,
|
|
|
|
vertical = true,
|
|
|
|
texture = "wool_" .. color .. ".png",
|
|
|
|
})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function printf( ... )
|
|
|
|
print( string.format( ... ) )
|
|
|
|
end
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-05-08 00:30:00 +02:00
|
|
|
local function to_vector3d( length, yaw, pitch )
|
|
|
|
local y = sin( pitch ) * length
|
|
|
|
local length2 = cos( pitch ) * length
|
|
|
|
local x = -sin( yaw ) * length2
|
|
|
|
local z = cos( yaw ) * length2
|
|
|
|
|
|
|
|
return { x = x, y = y, z = z }, length2
|
|
|
|
end
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
local function normalize_angle( r )
|
|
|
|
-- stackoverflow.com/questions/1878907/the-smallest-difference-between-2-angles
|
|
|
|
return atan2( sin( r ), cos( r ) )
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_vector_angle( p1, p2 )
|
|
|
|
return atan2( p2.z - p1.z, p2.x - p1.x )
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_vector_height( p1, p2 ) -- get altitude from p1 to p2
|
|
|
|
return abs( p2.y - p1.y )
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_vector_length( p1, p2 ) -- get distance from p1 to p2
|
|
|
|
return sqrt( pow( p2.x - p1.x, 2 ) + pow( p2.z - p1.z, 2 ) )
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_vector_incline( p1, p2 )
|
|
|
|
local h = get_vector_length( p1, p2 )
|
|
|
|
local v = p2.y - p1.y
|
|
|
|
return v / h
|
|
|
|
end
|
|
|
|
|
|
|
|
local function ramp( f, cur_v, max_v )
|
|
|
|
-- min function handles NaN, but let's err on the side of caution
|
|
|
|
return max_v == 0 and f or f * min( 1, cur_v / max_v )
|
|
|
|
end
|
|
|
|
|
|
|
|
local function sign( v )
|
|
|
|
return random( 2 ) == 1 and v or -v
|
|
|
|
end
|
|
|
|
|
|
|
|
local function lower_random( v_limit, v_ratio )
|
|
|
|
return sign( v_ratio * random( ) * v_limit )
|
|
|
|
end
|
|
|
|
|
|
|
|
local function upper_random( v_limit, v_ratio )
|
|
|
|
return sign( v_limit - v_ratio * random( ) * v_limit )
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_power_decrease( scale, power, value )
|
|
|
|
return value <= 1 - scale and 1.0 or
|
|
|
|
max( 1 - pow( ( scale + value - 1 ) / scale, 1 + power ), 0 )
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_power_increase( scale, power, value )
|
|
|
|
return value >= scale and 1.0 or
|
2020-05-08 02:27:00 +02:00
|
|
|
1 - pow( ( scale - value ) / scale, 1 + power )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
local function check_limits( v, min_v, max_v )
|
|
|
|
return v >= min_v and v <= max_v
|
|
|
|
end
|
|
|
|
|
|
|
|
local function random_range( min_v, max_v )
|
|
|
|
return random( min_v * 100, max_v * 100 ) / 100
|
|
|
|
end
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
|
|
|
minetest.register_on_leaveplayer( function( player, is_timeout )
|
|
|
|
local name = player:get_player_name( )
|
|
|
|
|
|
|
|
-- delete all target references (if applicable)
|
2020-05-08 00:30:00 +02:00
|
|
|
for id, obj in pairs( registry.avatars ) do
|
2020-05-14 03:46:00 +02:00
|
|
|
local this = obj:get_luaentity( )
|
|
|
|
if this.target and this.target.obj == player then
|
|
|
|
this:reset_alertness( "ignore" )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
end
|
2020-05-03 22:58:00 +02:00
|
|
|
registry.players[ name ] = nil
|
|
|
|
end )
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-03 22:58:00 +02:00
|
|
|
minetest.register_on_joinplayer( function( player )
|
|
|
|
local name = player:get_player_name( )
|
|
|
|
registry.players[ name ] = player
|
2020-04-30 22:28:00 +02:00
|
|
|
end )
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-05-03 22:58:00 +02:00
|
|
|
local builtin_item = minetest.registered_entities[ "__builtin:item" ]
|
|
|
|
|
|
|
|
builtin_item.old_on_activate = builtin_item.on_activate
|
|
|
|
builtin_item.old_set_item = builtin_item.set_item
|
|
|
|
|
|
|
|
builtin_item.set_item = function ( self, itemstring )
|
|
|
|
self:old_set_item( itemstring )
|
|
|
|
self.item_name = ItemStack( self.itemstring ):get_name( )
|
|
|
|
end
|
|
|
|
|
|
|
|
builtin_item.on_activate = function ( self, staticdata, dtime, id )
|
|
|
|
self:old_on_activate( staticdata, dtime )
|
|
|
|
registry.spawnitems[ id ] = self.object
|
|
|
|
end
|
|
|
|
|
|
|
|
builtin_item.on_deactivate = function ( self, id )
|
2020-05-08 00:30:00 +02:00
|
|
|
for id, obj in pairs( registry.avatars ) do
|
2020-05-14 03:46:00 +02:00
|
|
|
local this = obj:get_luaentity( )
|
|
|
|
if this.target and this.target.obj == self.object then
|
|
|
|
this:reset_alertness( "ignore" )
|
2020-05-03 22:58:00 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
registry.spawnitems[ id ] = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
local globaltimer = Timekeeper( { } )
|
|
|
|
|
|
|
|
minetest.register_globalstep( function ( dtime )
|
|
|
|
globaltimer.on_step( dtime )
|
|
|
|
end )
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-05-14 05:53:00 +02:00
|
|
|
minetest.register_entity( "mobs:gibbage", {
|
|
|
|
physical = true,
|
|
|
|
visual = "mesh",
|
|
|
|
visual_size = { x = 1.0, y = 1.0 },
|
|
|
|
collisionbox = { -0.2, -0.1, -0.2, 0.2, 0.1, 0.2 },
|
|
|
|
motion_sounds = { },
|
|
|
|
physics = {
|
|
|
|
density = 0.5,
|
|
|
|
elasticity = 0.2,
|
|
|
|
resistance = 0.0,
|
|
|
|
friction = 0.7,
|
|
|
|
},
|
|
|
|
|
|
|
|
on_activate = function ( self, staticdata, dtime )
|
|
|
|
BasicPhysics( self )
|
|
|
|
|
|
|
|
self.timekeeper = Timekeeper( self )
|
|
|
|
self.timekeeper.start( math.random( 4, 8 ), "gibbage", function ( )
|
|
|
|
self.object:remove( )
|
|
|
|
end )
|
|
|
|
|
|
|
|
if dtime > 0 then
|
|
|
|
self.object:remove( )
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
on_step = function ( self, dtime, pos )
|
|
|
|
self.timekeeper.on_step( dtime )
|
|
|
|
end,
|
|
|
|
|
|
|
|
launch = function ( self, intensity, texture, piece, sound )
|
|
|
|
local obj = self.object
|
|
|
|
|
|
|
|
obj:set_properties( {
|
|
|
|
mesh = "gib_" .. piece .. ".b3d",
|
|
|
|
textures = { texture },
|
|
|
|
} )
|
|
|
|
local vel_horz = min( 4, intensity * 0.5 )
|
|
|
|
local vel_vert = min( 4, intensity * 0.7 )
|
|
|
|
obj:set_velocity( vector.new( random_range( -vel_horz, vel_horz ), vel_vert, random_range( -vel_horz, vel_horz ) ) )
|
|
|
|
obj:set_animation( { x = 0, y = 120 }, random( 20, 40 ), 0, false )
|
|
|
|
self.motion_sounds.bouncing = sound
|
|
|
|
end,
|
|
|
|
} )
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
mobs.register_mob = function ( name, def )
|
|
|
|
minetest.register_entity( name, {
|
|
|
|
type = def.type,
|
|
|
|
hp_max = def.hp_max,
|
|
|
|
hp_low = def.hp_low,
|
|
|
|
physical = true,
|
|
|
|
mesh = def.mesh,
|
|
|
|
collisionbox = def.collisionbox,
|
|
|
|
visual = def.visual,
|
|
|
|
visual_size = def.visual_size,
|
|
|
|
height = def.height,
|
|
|
|
y_offset = def.y_offset,
|
|
|
|
gravity = def.gravity or world_gravity,
|
|
|
|
density = def.density or 0.5,
|
|
|
|
textures = def.textures,
|
|
|
|
makes_footstep_sound = def.makes_footstep_sound,
|
|
|
|
makes_bloodshed_effect = def.makes_bloodshed_effect,
|
2020-05-14 05:53:00 +02:00
|
|
|
gibbage_params = def.gibbage_params,
|
2020-05-13 16:24:00 +02:00
|
|
|
receptrons = def.receptrons,
|
2020-04-30 22:28:00 +02:00
|
|
|
death_message = def.death_message,
|
|
|
|
alertness_states = def.alertness_states,
|
2020-05-14 03:46:00 +02:00
|
|
|
awareness_stages = def.awareness_stages,
|
2020-04-30 22:28:00 +02:00
|
|
|
sensitivity = def.sensitivity or 0.0,
|
|
|
|
certainty = def.certainty or 1.0,
|
|
|
|
attack_range = def.attack_range,
|
|
|
|
escape_range = def.escape_range,
|
|
|
|
follow_range = def.follow_range,
|
2020-05-14 03:46:00 +02:00
|
|
|
search_range = def.search_range,
|
2020-05-03 22:58:00 +02:00
|
|
|
pickup_range = def.pickup_range,
|
2020-04-30 22:28:00 +02:00
|
|
|
sneak_velocity = def.sneak_velocity,
|
|
|
|
walk_velocity = def.walk_velocity,
|
|
|
|
run_velocity = def.run_velocity,
|
|
|
|
stray_velocity = def.stray_velocity,
|
|
|
|
recoil_velocity = def.recoil_velocity,
|
|
|
|
standoff = def.standoff or 5,
|
|
|
|
damage = def.damage,
|
|
|
|
light_damage = def.light_damage or 0,
|
|
|
|
water_damage = def.water_damage or 0,
|
|
|
|
lava_damage = def.lava_damage or 0,
|
|
|
|
enable_fall_damage = def.enable_fall_damage,
|
|
|
|
drops = def.drops,
|
|
|
|
armor = def.armor,
|
|
|
|
yaw_origin = def.drawtype == "front" and 0 or -rad_90,
|
|
|
|
drawtype = def.drawtype,
|
|
|
|
on_rightclick = def.on_rightclick,
|
|
|
|
attack_type = def.attack_type,
|
|
|
|
projectile = def.projectile,
|
|
|
|
shoot_period = def.shoot_period,
|
|
|
|
shoot_chance = def.shoot_chance,
|
|
|
|
weapon_params = def.weapon_params,
|
2020-05-14 03:46:00 +02:00
|
|
|
watch_wielditems = def.watch_wielditems or { },
|
|
|
|
watch_spawnitems = def.watch_spawnitems or { },
|
|
|
|
watch_players = def.watch_players or { },
|
2020-04-30 22:28:00 +02:00
|
|
|
sounds = def.sounds,
|
|
|
|
animation = def.animation,
|
|
|
|
fear_factor = def.fear_factor,
|
|
|
|
flee_factor = def.flee_factor,
|
|
|
|
can_jump = def.can_jump or false,
|
|
|
|
can_fly = def.can_fly or false,
|
|
|
|
can_walk = def.can_walk or false,
|
|
|
|
enable_climbing = true,
|
|
|
|
enable_swimming = true,
|
|
|
|
shoot_count = 0,
|
|
|
|
timeout = def.timeout,
|
2020-05-14 03:46:00 +02:00
|
|
|
neutral_state = "ignore",
|
2020-05-19 15:19:00 +02:00
|
|
|
offense_state = def.offense_state or def.type == "monster" and "attack" or "ignore",
|
|
|
|
defense_state = def.defense_state or def.type == "monster" and "attack" or "escape",
|
|
|
|
retreat_state = def.retreat_state or "escape",
|
2020-04-30 22:28:00 +02:00
|
|
|
is_tamed = false,
|
|
|
|
description = def.description,
|
2020-05-13 16:24:00 +02:00
|
|
|
after_activate = def.after_activate,
|
|
|
|
before_deactivate = def.before_deactivate,
|
2020-05-14 03:46:00 +02:00
|
|
|
before_state_change = def.before_state_change,
|
2020-05-13 16:24:00 +02:00
|
|
|
before_punch = def.before_punch,
|
2020-04-30 22:28:00 +02:00
|
|
|
|
|
|
|
-- prepare noise generator with seed, octaves, persistence, spread
|
|
|
|
hunger_noise = PerlinNoise( random( 1000 ), 1, 0, def.hunger_params.spread ),
|
|
|
|
hunger_offset = def.hunger_params.offset,
|
|
|
|
|
|
|
|
-- primitive movement functions --
|
|
|
|
|
|
|
|
set_speed = function( self, speed )
|
|
|
|
self.speed = speed
|
|
|
|
self.object:set_speed( speed )
|
|
|
|
end,
|
|
|
|
|
|
|
|
set_speed_lateral = function( self, speed_x, speed_y )
|
|
|
|
self.speed = speed_y
|
|
|
|
self.object:set_speed_lateral( speed_x, speed_y )
|
|
|
|
end,
|
|
|
|
|
|
|
|
set_velocity_vert = function ( self, vel_y )
|
|
|
|
self.object:set_velocity_vert( vel_y )
|
|
|
|
self.object:set_acceleration_vert( self.gravity )
|
|
|
|
end,
|
|
|
|
|
|
|
|
set_acceleration_vert = function ( self, acc_y )
|
|
|
|
self.object:set_acceleration_vert( acc_y )
|
|
|
|
end,
|
|
|
|
|
|
|
|
set_yaw = function ( self, yaw )
|
|
|
|
self.object:set_yaw( yaw )
|
|
|
|
end,
|
|
|
|
|
|
|
|
turn_to = function ( self, yaw, period )
|
|
|
|
local yaw_delta = normalize_angle( yaw - self.yaw )
|
|
|
|
self.object:turn_by( yaw_delta, period / 10 )
|
|
|
|
end,
|
|
|
|
|
|
|
|
turn_by = function ( self, yaw_delta, period )
|
|
|
|
self.object:turn_by( yaw_delta, period / 10 )
|
|
|
|
end,
|
|
|
|
|
|
|
|
move_by = function ( self, pos_delta, period )
|
|
|
|
self.object:move_by( pos_delta, period / 10 )
|
|
|
|
end,
|
|
|
|
|
|
|
|
get_random_yaw = function ( self, r_limit, r_ratio )
|
|
|
|
return self.yaw + upper_random( r_limit, r_ratio or 1 )
|
|
|
|
end,
|
|
|
|
|
|
|
|
get_direct_yaw = function ( self, pos )
|
|
|
|
-- convert to world coordinate system
|
|
|
|
return get_vector_angle( self.pos, pos ) - rad_90 - self.yaw_origin
|
|
|
|
end,
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
get_direct_yaw_delta = function ( self, pos )
|
|
|
|
local yaw = self:get_direct_yaw( pos )
|
|
|
|
return abs( normalize_angle( yaw - self.yaw ) )
|
2020-05-14 03:46:00 +02:00
|
|
|
end,
|
2020-05-13 16:24:00 +02:00
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
get_target_yaw = function ( self, r_limit, r_ratio )
|
|
|
|
return self:get_direct_yaw( self.target.pos ) + upper_random( r_limit, r_ratio or 1 )
|
2020-04-30 22:28:00 +02:00
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
get_target_yaw_delta = function ( self )
|
|
|
|
local yaw = self:get_direct_yaw( self.target.pos )
|
2020-04-30 22:28:00 +02:00
|
|
|
return abs( normalize_angle( yaw - self.yaw ) )
|
|
|
|
end,
|
|
|
|
|
2020-05-16 05:46:00 +02:00
|
|
|
get_target_distance = function ( self )
|
|
|
|
return vector.distance( self.pos, self.target.pos )
|
|
|
|
end,
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
-- collision processing --
|
|
|
|
|
|
|
|
get_pos_ahead = function ( self, outset, angle )
|
|
|
|
-- convert from world coordinate system
|
|
|
|
local rot = angle + self.yaw_origin
|
|
|
|
return vector.round( {
|
|
|
|
x = self.pos.x - sin( self.yaw + rot ) * ( self.collisionbox[ 4 ] + outset ),
|
|
|
|
y = self.pos.y,
|
|
|
|
z = self.pos.z + cos( self.yaw + rot ) * ( self.collisionbox[ 4 ] + outset )
|
|
|
|
} )
|
|
|
|
end,
|
|
|
|
|
|
|
|
can_walk_ahead = function ( self, outset, angle )
|
|
|
|
local fpos = self:get_pos_ahead( outset, angle )
|
|
|
|
local unknown_ndef = { groups = { }, walkable = false }
|
|
|
|
|
|
|
|
if self.height < 2 then
|
|
|
|
local node = minetest.get_node( fpos )
|
|
|
|
local ndef = core.registered_nodes[ node.name ] or unknown_ndef -- account for unknown nodes
|
|
|
|
local is_airlike = not ndef.walkable
|
|
|
|
|
|
|
|
--node_locator( vector.offset_y( fpos ), 4.5, 1.0, is_airlike and "green" or angle == 0 and "white" or "red" )
|
|
|
|
return is_airlike
|
|
|
|
else
|
|
|
|
local node_below = minetest.get_node( fpos )
|
|
|
|
local node_above = minetest.get_node_above( fpos )
|
|
|
|
local ndef_below = core.registered_nodes[ node_below.name ] or unknown_ndef -- account for unknown nodes
|
|
|
|
local ndef_above = core.registered_nodes[ node_above.name ] or unknown_ndef
|
|
|
|
local is_airlike = not ndef_below.walkable and not ndef_above.walkable
|
|
|
|
|
|
|
|
--node_locator( vector.offset_y( fpos ), 4.5, 1.0, is_airlike and "green" or angle == 0 and "white" or "red" )
|
|
|
|
return is_airlike
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
-- utility functions --
|
|
|
|
|
|
|
|
is_starving = function ( self )
|
|
|
|
local hunger = self.hunger_noise:get2d( { x = self.timeout, y = 0 } )
|
|
|
|
|
|
|
|
-- offset of -1 is never hungry, offset of 1 is always hungry
|
|
|
|
return hunger > -self.hunger_offset
|
|
|
|
end,
|
|
|
|
|
|
|
|
play_sound = function ( self, name )
|
2020-05-14 03:46:00 +02:00
|
|
|
minetest.sound_play( name, { object = self.object, loop = false } )
|
2020-05-13 16:24:00 +02:00
|
|
|
end,
|
|
|
|
|
|
|
|
play_sound_repeat = function ( self, name )
|
|
|
|
return minetest.sound_play( name, { object = self.object, loop = true } )
|
|
|
|
end,
|
|
|
|
|
|
|
|
make_noise = function ( self, radius, group, intensity )
|
|
|
|
axon.generate_radial_stimulus( self.pos, radius, 0.0, 0.0, 1, { [group] = intensity }, { avatars = true } )
|
|
|
|
end,
|
|
|
|
|
|
|
|
make_noise_repeat = function ( self, radius, interval, duration, group, intensity )
|
|
|
|
self.timekeeper.start_now( interval, "noise" .. next_noise_id, function ( this, cycles, period, elapsed )
|
|
|
|
if elapsed >= duration then return true end -- we're finished, so cancel timer
|
|
|
|
|
|
|
|
axon.generate_radial_stimulus( self.pos, radius, 0.0, 0.0, 1, { [group] = intensity }, { avatars = true } )
|
|
|
|
end )
|
|
|
|
next_noise_id = next_noise_id + 1
|
|
|
|
end,
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
-- sensory processing --
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
check_suspect = function ( self, target_obj, clarity, elapsed )
|
|
|
|
-- default to defense or neutral state if not in any watch list
|
2020-05-19 15:19:00 +02:00
|
|
|
local suspect = random( 10 ) <= self.fear_factor and self.offense_state or self.neutral_state
|
2020-05-14 03:46:00 +02:00
|
|
|
local entity = target_obj:get_luaentity( )
|
2020-05-03 22:58:00 +02:00
|
|
|
|
|
|
|
if not entity then
|
2020-05-14 03:46:00 +02:00
|
|
|
local player_name = target_obj:get_player_name( )
|
|
|
|
local item_name = target_obj:get_wielded_item( ):get_name( )
|
|
|
|
suspect = self.watch_players[ player_name ] or self.watch_wielditems[ item_name ] or suspect
|
2020-05-03 22:58:00 +02:00
|
|
|
elseif entity.name == "__builtin:item" then
|
2020-05-14 03:46:00 +02:00
|
|
|
suspect = self.watch_spawnitems[ entity.item_name ] or suspect
|
2020-05-03 22:58:00 +02:00
|
|
|
end
|
2020-04-30 22:28:00 +02:00
|
|
|
|
|
|
|
if type( suspect ) == "function" then
|
2020-05-14 03:46:00 +02:00
|
|
|
return suspect( self, target_obj, clarity, elapsed or 0.0 ) -- must always return a valid state
|
2020-04-30 22:28:00 +02:00
|
|
|
else
|
|
|
|
return suspect
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
get_visibility = function ( self, target_pos )
|
2020-04-30 22:28:00 +02:00
|
|
|
local length = get_vector_length( self.pos, target_pos )
|
2020-05-14 03:46:00 +02:00
|
|
|
local height = get_vector_height( self.pos, target_pos )
|
2020-04-30 22:28:00 +02:00
|
|
|
local this = self.alertness
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
if not this or length > 48 or height > 48 then -- immediately eliminate far objects
|
|
|
|
return 0.0, false, false
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
else
|
|
|
|
local view_range = this.view_radius + this.view_offset
|
|
|
|
local view_pos = this.view_offset == 0.0 and self.pos or vector.new(
|
|
|
|
self.pos.x - sin( self.yaw + self.yaw_origin ) * this.view_offset,
|
|
|
|
self.pos.y,
|
|
|
|
self.pos.z + cos( self.yaw + self.yaw_origin ) * this.view_offset
|
|
|
|
)
|
|
|
|
|
|
|
|
local radius = get_vector_length( view_pos, target_pos )
|
|
|
|
local clarity = get_power_decrease( 1.0, this.view_acuity, length / view_range )
|
|
|
|
|
|
|
|
-- certainty factor ranges from 0 (target never evident) to 1 (target always evident)
|
|
|
|
-- sensitivity threshold ranges from 0 (full perception) to 1 (no perception)
|
|
|
|
|
|
|
|
--node_locator( vector.offset_y( view_pos ), 4.5, 1.0, "yellow" )
|
2020-05-14 03:46:00 +02:00
|
|
|
--printf( "is_paranoid(%s):\n[%s] radius <= view_radius\n[%s] height <= view_height\n%0.2f = clarity (%0.2f = sensitivity, %$
|
|
|
|
|
|
|
|
local is_visible = radius <= this.view_radius and height <= this.view_height
|
|
|
|
local is_evident = clarity > self.sensitivity and clarity * self.certainty > random( )
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
return clarity, is_visible, is_evident
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
-- target functions --
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
create_target = function ( self, obj )
|
|
|
|
local pos = obj:get_pos( )
|
|
|
|
local clarity, is_visible, is_evident = self:get_visibility( pos )
|
|
|
|
local alertness = self.alertness_states[ self.state ]
|
|
|
|
|
|
|
|
-- when creating target it must be visible and evident!
|
|
|
|
if is_visible and is_evident then
|
|
|
|
if alertness and alertness.view_filter then
|
2020-05-19 15:19:00 +02:00
|
|
|
return alertness.view_filter( self, obj, clarity, 0.0 ), { obj = obj, pos = pos }
|
2020-05-14 03:46:00 +02:00
|
|
|
elseif clarity > 0.0 then
|
2020-05-19 15:19:00 +02:00
|
|
|
return self:check_suspect( obj, clarity, 0.0 ), { obj = obj, pos = pos }
|
2020-05-14 03:46:00 +02:00
|
|
|
end
|
|
|
|
else
|
|
|
|
return self.state, self.target
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
verify_target = function ( self, elapsed )
|
|
|
|
if self.target.obj then
|
|
|
|
local target_pos = self.target.obj:get_pos( ) -- check target's new position
|
|
|
|
local clarity, is_visible, is_evident = self:get_visibility( target_pos )
|
|
|
|
local alertness = self.alertness_states[ self.state ]
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
-- update last-known target position only if visible and evident
|
|
|
|
if is_visible and is_evident then
|
|
|
|
self.target.pos = target_pos
|
|
|
|
else
|
|
|
|
clarity = 0.0
|
|
|
|
end
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
if alertness and alertness.view_filter then
|
2020-05-19 15:19:00 +02:00
|
|
|
return alertness.view_filter( self, self.target.obj, clarity, elapsed )
|
2020-05-14 03:46:00 +02:00
|
|
|
elseif clarity > 0.0 then
|
2020-05-19 15:19:00 +02:00
|
|
|
return self.awareness.pass_state or self:check_suspect( self.target.obj, clarity, elapsed )
|
2020-05-14 03:46:00 +02:00
|
|
|
else
|
2020-05-19 15:19:00 +02:00
|
|
|
return self.awareness.fail_state or self.neutral_state
|
2020-05-14 03:46:00 +02:00
|
|
|
end
|
2020-05-05 02:41:00 +02:00
|
|
|
end
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
return self.state
|
2020-04-30 22:28:00 +02:00
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
locate_target = function ( self )
|
|
|
|
if self.sounds and self.sounds.random and random( 35 ) == 1 then
|
|
|
|
minetest.sound_play( self.sounds.random, { object = self.object } )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
-- when not upset, seek out food or prey at random intervals
|
|
|
|
for obj in mobs.iterate_registry( self.pos, 30, 30, { players = true, spawnitems = true } ) do
|
2020-05-05 02:41:00 +02:00
|
|
|
|
2020-05-19 15:19:00 +02:00
|
|
|
if random( 10 ) <= self.fear_factor and not obj:get_attach( ) then
|
2020-05-14 03:46:00 +02:00
|
|
|
local state, target = self:create_target( obj )
|
|
|
|
if state ~= self.state then
|
|
|
|
self:reset_alertness( state, target )
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2020-04-30 22:28:00 +02:00
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
-- alertness and awareness functions --
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-16 20:41:00 +02:00
|
|
|
start_awareness = function ( self )
|
2020-05-19 15:19:00 +02:00
|
|
|
self.awareness = self.awareness_stages[ self.state ] or { }
|
|
|
|
|
|
|
|
if self.awareness.decay and self.awareness.decay > 0 then
|
|
|
|
self.timekeeper.start( self.awareness.decay, "awareness", function ( )
|
|
|
|
self:reset_alertness( self.awareness.wait_state, self.target )
|
|
|
|
end )
|
2020-05-14 03:46:00 +02:00
|
|
|
else
|
|
|
|
self.timekeeper.clear( "awareness" )
|
|
|
|
end
|
2020-04-30 22:28:00 +02:00
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
reset_alertness = function ( self, state, target )
|
|
|
|
if state == self.state then return end
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
if self.before_state_change then
|
|
|
|
self:before_state_change( self.state, state )
|
2020-05-05 02:41:00 +02:00
|
|
|
end
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
self.state = state
|
|
|
|
self.target = target
|
|
|
|
self.alertness = self.alertness_states[ state ]
|
|
|
|
|
|
|
|
self:start_awareness( )
|
|
|
|
self.action_funcs[ state ]( self )
|
2020-04-30 22:28:00 +02:00
|
|
|
end,
|
|
|
|
|
|
|
|
-- action hooks --
|
|
|
|
|
|
|
|
start_ignore_action = function ( self )
|
2020-05-14 03:46:00 +02:00
|
|
|
self.target = nil -- forget any target
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
if not self.move_result.is_standing then
|
|
|
|
-- always stand when in mid-air
|
|
|
|
self:set_speed( 0 )
|
|
|
|
self:set_animation( self.can_fly and "swim" or "stand" )
|
|
|
|
elseif self:is_starving( ) then
|
|
|
|
self:set_speed( self.walk_velocity )
|
|
|
|
self:set_animation( "walk" )
|
|
|
|
else
|
|
|
|
self:set_speed( 0 )
|
|
|
|
self:set_animation( "stand" )
|
|
|
|
end
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
self.timekeeper.start( 1.0, "hunger", self.locate_target )
|
2020-04-30 22:28:00 +02:00
|
|
|
self.timekeeper.start( 1.0, "action", self.on_ignore_action )
|
|
|
|
end,
|
|
|
|
|
|
|
|
on_ignore_action = function ( self, cycles )
|
|
|
|
-- handle standing and walking motion
|
|
|
|
if self.speed == 0 then
|
|
|
|
if random( 3 ) == 1 then
|
|
|
|
-- turn randomly, because we're bored
|
|
|
|
self:turn_to( self:get_random_yaw( rad_180 ), random( 2 ) == 1 and 10 or 20 )
|
|
|
|
end
|
|
|
|
|
|
|
|
-- don't stand still when hungry
|
|
|
|
if self:is_starving( ) then
|
|
|
|
if self.can_fly and random( 2 ) == 1 then
|
|
|
|
self:set_speed( self.run_velocity )
|
|
|
|
self:set_velocity_vert( self.run_velocity )
|
|
|
|
self:set_animation( "swim" )
|
|
|
|
else
|
|
|
|
self:set_speed( self.walk_velocity )
|
|
|
|
self:set_animation( "walk" )
|
|
|
|
end
|
|
|
|
|
|
|
|
elseif self.can_walk and self.move_result.is_standing then
|
|
|
|
self:set_animation( "stand" )
|
|
|
|
|
|
|
|
elseif self.can_fly then
|
|
|
|
local is_above = minetest.line_of_sight( self.pos,
|
|
|
|
{ x = self.pos.x, y = self.pos.y - self.standoff, z = self.pos.z }, 1 )
|
|
|
|
|
|
|
|
if self.can_walk then
|
|
|
|
-- slowly descend until reaching ground
|
|
|
|
self.object:set_velocity_vert( -self.walk_velocity )
|
|
|
|
elseif is_above then
|
|
|
|
self.object:set_velocity_vert( random_range( -self.sneak_velocity, 0 ) )
|
|
|
|
else
|
|
|
|
self.object:set_velocity_vert( random_range( 0, self.sneak_velocity ) )
|
|
|
|
end
|
|
|
|
self:set_animation( "swim" )
|
|
|
|
end
|
|
|
|
|
|
|
|
else
|
|
|
|
if not self:can_walk_ahead( 1.0, 0 ) then
|
|
|
|
if self:can_walk_ahead( 0.8, rad_90 ) then
|
|
|
|
self:turn_by( rad_60, 5 )
|
|
|
|
elseif self:can_walk_ahead( 0.8, -rad_90 ) then
|
|
|
|
self:turn_by( -rad_60, 5 )
|
|
|
|
else
|
|
|
|
self:turn_by( rad_180, 20 )
|
|
|
|
end
|
|
|
|
|
|
|
|
elseif random( 3 ) == 1 then
|
|
|
|
-- otherwise occasionally change direction
|
|
|
|
self:turn_to( self:get_random_yaw( rad_60 ), 20 )
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
if self.move_result.is_standing then
|
|
|
|
if self.can_jump and random( 10 ) == 1 then
|
|
|
|
-- random jump
|
|
|
|
self.object:set_velocity_vert( 5 )
|
|
|
|
end
|
|
|
|
|
|
|
|
if not self:is_starving( ) then
|
|
|
|
self:set_speed( 0 )
|
|
|
|
self:set_animation( "stand" )
|
|
|
|
else
|
|
|
|
self:set_animation( "walk" )
|
|
|
|
end
|
|
|
|
|
|
|
|
elseif self.can_fly then
|
|
|
|
-- random vertical flight pattern, keeping clear of ground obstacles
|
|
|
|
local is_above = minetest.line_of_sight( self.pos,
|
|
|
|
{ x = self.pos.x, y = self.pos.y - self.standoff, z = self.pos.z }, 1 )
|
|
|
|
|
|
|
|
if not self:is_starving( ) then
|
|
|
|
self:set_speed( 0 )
|
|
|
|
self.object:set_velocity_vert( is_above and -self.walk_velocity )
|
|
|
|
elseif is_above then
|
|
|
|
self.object:set_velocity_vert( random_range( -self.sneak_velocity, self.sneak_velocity ) )
|
|
|
|
else
|
|
|
|
self.object:set_velocity_vert( self.walk_velocity )
|
|
|
|
end
|
|
|
|
|
|
|
|
self:set_animation( "swim" )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
start_follow_action = function ( self )
|
2020-05-14 03:46:00 +02:00
|
|
|
assert( self.target.pos ) -- sanity check
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
self:turn_to( self:get_target_yaw( rad_20 ), 10 )
|
|
|
|
self:set_speed( self.recoil_velocity )
|
|
|
|
self:set_animation( "walk" )
|
|
|
|
|
|
|
|
self.timekeeper.clear( "hunger" )
|
|
|
|
self.timekeeper.start( 0.5, "action", self.on_follow_action )
|
|
|
|
end,
|
|
|
|
|
|
|
|
on_follow_action = function ( self, cycles, period, elapsed )
|
2020-05-14 03:46:00 +02:00
|
|
|
if cycles % 2 == 0 then -- validate target every 1.0 seconds
|
|
|
|
local goal_state = self:verify_target( )
|
|
|
|
if goal_state ~= self.state then
|
|
|
|
self:reset_alertness( goal_state, self.target )
|
2020-05-13 16:24:00 +02:00
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-16 05:46:00 +02:00
|
|
|
local target_pos = self.target.pos
|
|
|
|
local dist = self:get_target_distance( )
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
if cycles % 2 == 0 then
|
2020-05-19 15:19:00 +02:00
|
|
|
if self:get_target_yaw_delta( ) > rad_45 or random( 3 ) == 1 then
|
|
|
|
self:turn_to( self:get_target_yaw( rad_20 ), 15 )
|
2020-05-13 16:24:00 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if dist <= self.follow_range then
|
|
|
|
if self.speed > 0 then
|
|
|
|
self:set_speed( 0 )
|
|
|
|
self:set_animation( "stand" )
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if self.speed == 0 then
|
|
|
|
self:set_speed( self.walk_velocity )
|
|
|
|
self:set_animation( "walk" )
|
|
|
|
|
|
|
|
else
|
|
|
|
if self.can_fly then
|
|
|
|
-- descend or ascend to slightly above player altitude, but prevent incessant bobbing
|
|
|
|
local v = 0
|
|
|
|
if target_pos.y + 2.5 > self.pos.y then
|
|
|
|
v = self.walk_velocity
|
|
|
|
elseif target_pos.y + 0.5 < self.pos.y then
|
|
|
|
v = -self.walk_velocity
|
|
|
|
end
|
|
|
|
self:set_velocity_vert( v )
|
|
|
|
|
|
|
|
elseif self.can_jump and self.move_result.is_standing then
|
|
|
|
if self.yield_level < 3 and self.move_result.collides_xz then
|
|
|
|
self.yield_level = self.yield_level + 1
|
|
|
|
self.object:set_velocity_vert( 5 )
|
2020-05-16 05:46:00 +02:00
|
|
|
elseif random( 5 ) == 1 then
|
2020-05-13 16:24:00 +02:00
|
|
|
self.object:set_velocity_vert( 5 )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
start_search_action = function ( self )
|
|
|
|
assert( self.target.pos ) -- sanity check
|
|
|
|
|
2020-05-16 05:46:00 +02:00
|
|
|
local dist = self:get_target_distance( )
|
|
|
|
|
|
|
|
self:turn_to( self:get_target_yaw( rad_0 ), 20 )
|
|
|
|
if self:get_target_yaw_delta( ) > rad_10 or dist <= self.search_range then
|
|
|
|
self:set_speed( 0 )
|
|
|
|
self:set_animation( "stand" )
|
|
|
|
else
|
|
|
|
self:set_speed( self.sneak_velocity )
|
|
|
|
self:set_animation( "walk" )
|
|
|
|
end
|
2020-05-14 03:46:00 +02:00
|
|
|
|
|
|
|
self.timekeeper.clear( "hunger" )
|
|
|
|
self.timekeeper.start( 0.5, "action", self.on_search_action )
|
|
|
|
end,
|
|
|
|
|
|
|
|
on_search_action = function ( self, cycles, period, elapsed )
|
|
|
|
if cycles % 2 == 0 then -- validate target every 1.0 seconds
|
|
|
|
local goal_state = self:verify_target( )
|
|
|
|
if goal_state ~= self.state then
|
|
|
|
self:reset_alertness( goal_state, self.target )
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local target_pos = self.target.pos
|
2020-05-16 05:46:00 +02:00
|
|
|
local dist = self:get_target_distance( )
|
2020-05-14 03:46:00 +02:00
|
|
|
|
|
|
|
-- go to last known position of target and look around
|
|
|
|
if dist <= self.search_range then
|
|
|
|
if self.speed > 0 then
|
|
|
|
self:set_speed( 0 )
|
|
|
|
self:set_animation( "stand" )
|
|
|
|
end
|
|
|
|
|
|
|
|
if random( 4 ) == 1 then
|
2020-05-16 05:46:00 +02:00
|
|
|
self:turn_to( self:get_random_yaw( rad_180 ), 10 )
|
2020-05-14 03:46:00 +02:00
|
|
|
end
|
|
|
|
else
|
2020-05-16 05:46:00 +02:00
|
|
|
-- wait at least 2 seconds after turning to start walking
|
|
|
|
if self.speed == 0 and cycles >= 4 and random( 3 ) == 1 then
|
|
|
|
self:set_speed( self.sneak_velocity )
|
2020-05-14 03:46:00 +02:00
|
|
|
self:set_animation( "walk" )
|
2020-05-16 05:46:00 +02:00
|
|
|
end
|
2020-05-14 03:46:00 +02:00
|
|
|
|
2020-05-16 05:46:00 +02:00
|
|
|
if self.can_fly then
|
|
|
|
-- descend or ascend to slightly above player altitude, but prevent incessant bobbing
|
|
|
|
local v = 0
|
|
|
|
if target_pos.y + 2.5 > self.pos.y then
|
|
|
|
v = self.walk_velocity
|
|
|
|
elseif target_pos.y + 0.5 < self.pos.y then
|
|
|
|
v = -self.walk_velocity
|
|
|
|
end
|
|
|
|
self:set_velocity_vert( v )
|
2020-05-14 03:46:00 +02:00
|
|
|
|
2020-05-16 05:46:00 +02:00
|
|
|
elseif self.can_jump and self.move_result.is_standing then
|
|
|
|
if self.yield_level < 3 and self.move_result.collides_xz then
|
|
|
|
self.yield_level = self.yield_level + 1
|
|
|
|
self.object:set_velocity_vert( 5 )
|
2020-05-14 03:46:00 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
start_escape_action = function ( self )
|
2020-05-14 03:46:00 +02:00
|
|
|
assert( self.target.pos ) -- sanity check
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-16 05:46:00 +02:00
|
|
|
local dist = self:get_target_distance( )
|
2020-05-13 16:24:00 +02:00
|
|
|
if self:get_target_yaw_delta( ) < rad_60 and dist <= self.escape_range then
|
2020-04-30 22:28:00 +02:00
|
|
|
-- recoil if facing intruder
|
|
|
|
self:set_speed( -self.recoil_velocity )
|
|
|
|
else
|
|
|
|
-- otherwise run in current direction
|
|
|
|
self:set_speed( self.run_velocity )
|
|
|
|
end
|
|
|
|
if self.can_fly then
|
|
|
|
self:set_velocity_vert( self.walk_velocity )
|
|
|
|
self:set_animation( "swim" )
|
|
|
|
else
|
|
|
|
self:set_animation( "walk" )
|
|
|
|
end
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
if self.target.obj and self.target.obj:is_player( ) then
|
|
|
|
-- this is a bad player, so keep watch
|
|
|
|
self.watch_players[ self.target.obj:get_player_name( ) ] = "escape"
|
|
|
|
end
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
if self.sounds and self.sounds.escape and random( 2 ) == 1 then
|
|
|
|
minetest.sound_play( self.sounds.escape, { object = self.object } )
|
|
|
|
end
|
|
|
|
|
|
|
|
self.timekeeper.clear( "hunger" )
|
|
|
|
self.timekeeper.start( 0.5, "action", self.on_escape_action )
|
|
|
|
end,
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
on_escape_action = function ( self, cycles, period, elapsed )
|
2020-04-30 22:28:00 +02:00
|
|
|
if cycles % 2 == 0 then
|
2020-05-14 03:46:00 +02:00
|
|
|
local goal_state = self:verify_target( )
|
|
|
|
if goal_state ~= self.state or random( 10 ) > self.flee_factor then
|
|
|
|
self:reset_alertness( goal_state, self.target )
|
2020-04-30 22:28:00 +02:00
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
local target_pos = self.target.pos
|
2020-05-16 05:46:00 +02:00
|
|
|
local dist = self:get_target_distance( )
|
2020-05-14 03:46:00 +02:00
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
if dist <= self.escape_range then
|
|
|
|
-- if close, keep backtracking
|
|
|
|
if cycles % 2 == 0 then
|
|
|
|
-- turn every 1.0 seconds (2 cycles)
|
2020-05-13 16:24:00 +02:00
|
|
|
self:turn_to( self:get_target_yaw( rad_20 ), 10 )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
if self.speed > 0 then
|
|
|
|
self:set_speed( -self.recoil_velocity )
|
|
|
|
self:set_animation( self.can_fly and "swim" or "walk" )
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- otherwise turn and run away
|
2020-05-13 16:24:00 +02:00
|
|
|
if cycles % 4 == 0 and self:get_target_yaw_delta( ) < rad_60 then
|
2020-04-30 22:28:00 +02:00
|
|
|
-- turn immediately if facing target
|
2020-05-13 16:24:00 +02:00
|
|
|
self:turn_to( self:get_target_yaw( rad_60 ) + rad_180, 20 )
|
2020-04-30 22:28:00 +02:00
|
|
|
elseif cycles % 2 == 0 then
|
|
|
|
-- otherwise turn every 1.0 seconds (2 cycles)
|
2020-05-13 16:24:00 +02:00
|
|
|
self:turn_to( self:get_target_yaw( rad_30 ) + rad_180, 10 )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
if self.speed <= 0 then
|
|
|
|
self:set_speed( self.run_velocity )
|
|
|
|
self:set_animation( self.can_fly and "swim" or "run" )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.can_fly then
|
|
|
|
local is_above = minetest.line_of_sight( self.pos,
|
|
|
|
{ x = self.pos.x, y = self.pos.y - self.standoff, z = self.pos.z }, 1 )
|
|
|
|
|
|
|
|
if is_above then
|
|
|
|
self.object:set_velocity_vert( random_range( -self.stray_velocity, self.stray_velocity ) )
|
|
|
|
else
|
|
|
|
self.object:set_velocity_vert( random_range( self.walk_velocity, self.run_velocity ) )
|
|
|
|
end
|
|
|
|
|
|
|
|
elseif self.can_jump and self.move_result.is_standing then
|
|
|
|
if self.yield_level < 3 and self.move_result.collides_xz then
|
|
|
|
self.yield_level = self.yield_level + 1
|
|
|
|
self:set_velocity_vert( 5 )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
start_attack_action = function ( self )
|
2020-05-14 03:46:00 +02:00
|
|
|
assert( self.target.pos and self.target.obj ) -- sanity check
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
self:set_speed( self.run_velocity )
|
|
|
|
self:set_animation( "run" )
|
2020-05-13 16:24:00 +02:00
|
|
|
self:turn_to( self:get_target_yaw( rad_90 ), 10 )
|
2020-04-30 22:28:00 +02:00
|
|
|
|
|
|
|
if self.sounds and self.sounds.attack and random( 2 ) == 1 then
|
|
|
|
minetest.sound_play( self.sounds.attack, { object = self.object } )
|
|
|
|
end
|
|
|
|
|
|
|
|
self.timekeeper.clear( "hunger" )
|
|
|
|
self.timekeeper.start( 0.2, "action", self.on_attack_action )
|
|
|
|
end,
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
on_attack_action = function ( self, cycles, period, elapsed )
|
2020-05-14 03:46:00 +02:00
|
|
|
if self.target.obj:get_hp( ) == 0 or self.target.obj:get_attach( ) then
|
|
|
|
self:reset_alertness( "ignore" )
|
2020-04-30 22:28:00 +02:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
if cycles % 5 == 0 then -- validate target once per second
|
2020-05-14 03:46:00 +02:00
|
|
|
local goal_state = self:verify_target( )
|
|
|
|
if goal_state ~= self.state then
|
|
|
|
self:reset_alertness( goal_state, self.target )
|
2020-04-30 22:28:00 +02:00
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
local target_pos = self.target.pos
|
2020-05-16 05:46:00 +02:00
|
|
|
local dist = self:get_target_distance( )
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
self:turn_to( self:get_target_yaw( rad_0 ), 5 )
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
if dist <= self.attack_range then
|
2020-05-13 16:24:00 +02:00
|
|
|
if self.attack_type == "shoot" and self.fire_weapon then
|
2020-04-30 22:28:00 +02:00
|
|
|
if cycles % ( self.shoot_period * 5 ) == 0 and random( self.shoot_chance ) == 1 then
|
|
|
|
if self.sounds and self.sounds.attack and random( 2 ) == 1 then
|
|
|
|
minetest.sound_play( self.sounds.attack, { object = self.object } )
|
|
|
|
end
|
|
|
|
self:fire_weapon( target_pos )
|
|
|
|
self:set_animation( "punch" )
|
|
|
|
else
|
|
|
|
self:set_animation( self.can_fly and "swim" or "walk" )
|
|
|
|
end
|
|
|
|
|
|
|
|
elseif self.attack_type == "melee" then
|
|
|
|
if cycles % 5 == 0 then
|
|
|
|
if self.sounds and self.sounds.attack and random( 3 ) == 1 then
|
|
|
|
minetest.sound_play( self.sounds.attack, { object = self.object } )
|
|
|
|
end
|
2020-05-13 16:24:00 +02:00
|
|
|
-- if minetest.line_of_sight( pos, target_pos, 0.5 ) then -- do not hit player through walls!
|
2020-05-14 03:46:00 +02:00
|
|
|
self.target.obj:punch( self.object, 1.0, {
|
2020-04-30 22:28:00 +02:00
|
|
|
full_punch_interval = 1.0,
|
|
|
|
damage_groups = { fleshy=self.damage }
|
|
|
|
}, vector.direction( self.pos, target_pos ) )
|
2020-05-13 16:24:00 +02:00
|
|
|
-- end
|
2020-04-30 22:28:00 +02:00
|
|
|
self:set_animation( "punch" )
|
|
|
|
else
|
|
|
|
self:set_animation( self.can_fly and "swim" or "walk" )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.can_fly and cycles % 2 == 0 then
|
|
|
|
local v = self.new_vel.y
|
|
|
|
if target_pos.y + 0.5 > self.pos.y then
|
|
|
|
v = self.stray_velocity
|
|
|
|
elseif target_pos.y + 2.5 < self.pos.y then
|
|
|
|
v = -self.stray_velocity
|
|
|
|
elseif dist < 1.0 then
|
|
|
|
v = self.stray_velocity
|
|
|
|
else
|
|
|
|
v = random_range( -self.stray_velocity, self.stray_velocity )
|
|
|
|
end
|
|
|
|
self:set_velocity_vert( v )
|
|
|
|
end
|
|
|
|
|
|
|
|
if random( 5 ) == 1 then -- dodge player by sidestepping at random intervals
|
|
|
|
self:set_speed_lateral( random( -self.stray_velocity, self.stray_velocity ), 0 )
|
|
|
|
end
|
|
|
|
|
|
|
|
else
|
|
|
|
if self.can_fly and cycles % 2 == 0 then
|
|
|
|
local v = self.new_vel.y
|
|
|
|
-- descend or ascend to slightly above player altitude, but prevent incessant bobbing
|
|
|
|
if target_pos.y + 0.5 > self.pos.y then
|
|
|
|
v = target_pos.y - self.pos.y > self.standoff and self.run_velocity or self.stray_velocity
|
|
|
|
elseif target_pos.y + 2.5 < self.pos.y then
|
|
|
|
v = target_pos.y - self.pos.y < -self.standoff and -self.run_velocity or -self.stray_velocity
|
|
|
|
elseif dist > self.attack_range * 2 then
|
|
|
|
v = self.stray_velocity
|
|
|
|
elseif random( 10 ) == 1 then
|
|
|
|
v = random_range( -self.stray_velocity, self.stray_velocity )
|
|
|
|
end
|
|
|
|
self:set_velocity_vert( v )
|
|
|
|
|
|
|
|
elseif self.can_jump and self.move_result.is_standing and
|
|
|
|
self.yield_level < 3 and self.move_result.collides_xz then
|
|
|
|
self.yield_level = self.yield_level + 1
|
|
|
|
self:set_velocity_vert( 5 )
|
|
|
|
end
|
2020-05-13 16:24:00 +02:00
|
|
|
|
|
|
|
if self:get_target_yaw_delta( ) > rad_30 then
|
2020-04-30 22:28:00 +02:00
|
|
|
-- defend from turn-strafing
|
|
|
|
if random( 15 ) == 1 then
|
|
|
|
self:set_speed( 0 )
|
|
|
|
end
|
|
|
|
self:set_animation( "walk" )
|
|
|
|
elseif self.speed <= 0 then
|
|
|
|
self:set_speed( self.run_velocity )
|
|
|
|
self:set_animation( "run" )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
-- animation handler --
|
|
|
|
|
|
|
|
set_animation = function ( self, type )
|
|
|
|
if not self.animation or self.cur_animation == type then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
if type == "stand" then
|
|
|
|
if self.animation.stand_start and self.animation.stand_end and self.animation.speed_normal then
|
|
|
|
self.object:set_animation(
|
|
|
|
{ x = self.animation.stand_start, y = self.animation.stand_end },
|
|
|
|
self.animation.speed_normal, 0
|
|
|
|
)
|
|
|
|
end
|
|
|
|
elseif type == "walk" then
|
|
|
|
if self.animation.walk_start and self.animation.walk_end and self.animation.speed_normal then
|
|
|
|
self.object:set_animation(
|
|
|
|
{ x = self.animation.walk_start, y = self.animation.walk_end },
|
|
|
|
self.animation.speed_normal, 0
|
|
|
|
)
|
|
|
|
end
|
|
|
|
elseif type == "run" then
|
|
|
|
if self.animation.run_start and self.animation.run_end and self.animation.speed_run then
|
|
|
|
self.object:set_animation(
|
|
|
|
{ x = self.animation.run_start, y = self.animation.run_end },
|
|
|
|
self.animation.speed_run, 0
|
|
|
|
)
|
|
|
|
end
|
|
|
|
elseif type == "swim" then
|
|
|
|
if self.animation.swim_start and self.animation.swim_end and self.animation.speed_swim then
|
|
|
|
self.object:set_animation(
|
|
|
|
{ x = self.animation.swim_start, y = self.animation.swim_end },
|
|
|
|
self.animation.speed_swim, 0
|
|
|
|
)
|
|
|
|
end
|
|
|
|
elseif type == "punch" then
|
|
|
|
if self.animation.punch_start and self.animation.punch_end and self.animation.speed_normal then
|
|
|
|
self.object:set_animation(
|
|
|
|
{ x = self.animation.punch_start, y = self.animation.punch_end },
|
|
|
|
self.animation.speed_normal, 0
|
|
|
|
)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
return -- not a valid animation type, so abort
|
|
|
|
end
|
|
|
|
|
|
|
|
self.cur_animation = type
|
|
|
|
end,
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
-- damage handler --
|
2020-04-30 22:28:00 +02:00
|
|
|
|
|
|
|
handle_damage = function ( self )
|
|
|
|
-- handle environmental damage (light, water, and lava)
|
|
|
|
local hp = self.object:get_hp( )
|
|
|
|
local node = minetest.get_node_above( self.pos, 0.5 )
|
|
|
|
local time_of_day = minetest.get_timeofday( )
|
2020-05-01 20:08:00 +02:00
|
|
|
|
|
|
|
if node.name == "ignore" then return end -- mapblock is not loaded, so abort!
|
2020-04-30 22:28:00 +02:00
|
|
|
|
|
|
|
if self.light_damage and self.light_damage > 0 and self.pos.y > 0 and
|
2020-05-01 20:08:00 +02:00
|
|
|
minetest.get_node_light( self.pos ) > 4 and check_limits( time_of_day, 0.2, 0.8 ) then
|
2020-04-30 22:28:00 +02:00
|
|
|
hp = hp - self.light_damage
|
|
|
|
self.object:set_hp( hp )
|
|
|
|
if hp <= 0 then
|
|
|
|
smoke_effect( self.pos )
|
|
|
|
self.object:remove( )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.water_damage and self.water_damage > 0 and minetest.get_item_group( node.name, "water" ) ~= 0 then
|
|
|
|
hp = hp - self.water_damage
|
|
|
|
self.object:set_hp( hp )
|
|
|
|
if hp <= 0 then
|
|
|
|
smoke_effect( self.pos )
|
|
|
|
self.object:remove( )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.lava_damage and self.lava_damage > 0 and minetest.get_item_group( node.name, "lava" ) ~= 0 then
|
|
|
|
hp = hp - self.lava_damage
|
|
|
|
self.object:set_hp( hp )
|
|
|
|
if hp <= 0 then
|
|
|
|
smoke_effect( self.pos )
|
|
|
|
self.object:remove( )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
-- generic callbacks --
|
|
|
|
|
2020-05-01 20:08:00 +02:00
|
|
|
on_step = function( self, dtime, pos, rot, new_vel, old_vel, move_result )
|
2020-04-30 22:28:00 +02:00
|
|
|
self.pos = pos
|
2020-05-03 22:58:00 +02:00
|
|
|
self.yaw = rot.y
|
2020-05-01 20:08:00 +02:00
|
|
|
self.new_vel = new_vel
|
2020-04-30 22:28:00 +02:00
|
|
|
self.move_result = move_result
|
|
|
|
|
|
|
|
local hp = self.object:get_hp( )
|
|
|
|
|
|
|
|
if hp == 0 then
|
|
|
|
self.object:remove( )
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
self.timeout = self.timeout - dtime
|
|
|
|
if self.timeout <= 0 and not self.tamed then
|
|
|
|
smoke_effect( pos )
|
|
|
|
self.object:remove( )
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.enable_fall_damage and move_result.collides_y and old_vel.y < -5 then
|
|
|
|
local damage = floor( -old_vel.y - 5 ) + 0.5
|
|
|
|
self.object:set_hp( hp - damage )
|
|
|
|
if hp - damage <= 0.0 then
|
|
|
|
smoke_effect( pos )
|
|
|
|
self.object:remove( )
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if move_result.is_swimming then
|
|
|
|
local drag = -new_vel.y * liquid_viscosity * 1.5
|
|
|
|
local buoyancy = self.density - liquid_density
|
|
|
|
self.object:set_acceleration_vert( world_gravity * buoyancy + drag )
|
2020-05-08 00:30:00 +02:00
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
elseif self.is_swimming then
|
|
|
|
self.object:set_acceleration_vert( ramp( world_gravity, new_vel.y, 2.0 ) ) -- hack to reduce oscilations
|
|
|
|
end
|
|
|
|
self.is_swimming = move_result.is_swimming
|
|
|
|
|
|
|
|
if self.yield_level > 0 and not move_result.collides_xz then
|
|
|
|
self.yield_level = 0
|
|
|
|
end
|
|
|
|
|
|
|
|
self.timekeeper.on_step( dtime ) -- run timer callbacks
|
|
|
|
end,
|
|
|
|
|
|
|
|
on_activate = function ( self, staticdata, dtime_s, id )
|
2020-05-08 00:30:00 +02:00
|
|
|
registry.avatars[ id ] = self.object
|
2020-04-30 22:28:00 +02:00
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
if self.receptrons then
|
|
|
|
AxonObject( self, { fleshy = self.armor } ) -- only inherit from superclass if receptrons exist
|
|
|
|
end
|
|
|
|
if self.weapon_params then
|
|
|
|
TurretShooter( self ) -- only inherit from supereclass if weapon params exist
|
|
|
|
end
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
self:set_acceleration_vert( self.gravity )
|
|
|
|
self:set_velocity_vert( 0 )
|
|
|
|
self:set_yaw( random( ) * rad_360 )
|
|
|
|
|
|
|
|
self.yield_level = 0
|
|
|
|
self.move_result = { collides_xz = false, collides_y = true, is_standing = true }
|
|
|
|
self.pos = self.object:get_pos( )
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
self.action_funcs = {
|
|
|
|
ignore = self.start_ignore_action,
|
|
|
|
remark = self.start_remark_action,
|
|
|
|
search = self.start_search_action,
|
|
|
|
follow = self.start_follow_action,
|
|
|
|
escape = self.start_escape_action,
|
|
|
|
attack = self.start_attack_action,
|
2020-05-13 16:24:00 +02:00
|
|
|
}
|
|
|
|
|
2020-05-17 19:32:00 +02:00
|
|
|
self.watch_players = table.copy( self.watch_players )
|
|
|
|
self.watch_spawnitems = table.copy( self.watch_spawnitems )
|
|
|
|
self.watch_wielditems = table.copy( self.watch_wielditems )
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
if staticdata then
|
|
|
|
local tmp = minetest.deserialize( staticdata )
|
|
|
|
if tmp and tmp.lifetimer then
|
|
|
|
self.timeout = tmp.lifetimer - dtime_s
|
|
|
|
end
|
|
|
|
if tmp and tmp.tamed then
|
|
|
|
self.tamed = tmp.tamed
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
self.timeout = self.timeout - dtime_s
|
|
|
|
if self.timeout <= 0 and not self.is_tamed then
|
|
|
|
self.object:remove( )
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
self.timekeeper = Timekeeper( self )
|
|
|
|
self.timekeeper.start( 1.5, "damage", self.handle_damage )
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
if self.after_activate then
|
|
|
|
self.after_activate( self, id )
|
2020-05-05 02:41:00 +02:00
|
|
|
end
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
self:reset_alertness( self.neutral_state )
|
2020-04-30 22:28:00 +02:00
|
|
|
end,
|
|
|
|
|
|
|
|
on_deactivate = function ( self, id )
|
2020-05-13 16:24:00 +02:00
|
|
|
if self.before_deactivate then
|
|
|
|
self.before_deactivate( self, id )
|
2020-05-05 02:41:00 +02:00
|
|
|
end
|
|
|
|
|
2020-05-03 22:58:00 +02:00
|
|
|
registry.avatars[ id ] = nil
|
2020-04-30 22:28:00 +02:00
|
|
|
end,
|
|
|
|
|
|
|
|
get_staticdata = function ( self )
|
|
|
|
return minetest.serialize( {
|
|
|
|
lifetimer = self.timeout,
|
|
|
|
is_tamed = self.is_tamed,
|
|
|
|
} )
|
|
|
|
end,
|
|
|
|
|
|
|
|
on_punch = function ( self, hitter, time_from_last_punch, tool_capabilities, direction, damage )
|
|
|
|
local hp = self.object:get_hp( )
|
|
|
|
local pos = self.pos
|
|
|
|
local tool = hitter:get_wielded_item( )
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
if self.before_punch then
|
|
|
|
if not self.before_punch( self, hitter, tool, hp, damage ) then return end
|
2020-05-05 02:41:00 +02:00
|
|
|
end
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
if damage == 0 then return end
|
|
|
|
|
2020-05-14 05:53:00 +02:00
|
|
|
if hp - damage <= 0 and self.gibbage_params then
|
|
|
|
local params = self.gibbage_params
|
|
|
|
local intensity = 0
|
|
|
|
for k, v in pairs( tool_capabilities.damage_groups ) do
|
|
|
|
if params.damage_groups[ k ] and v >= params.damage_groups[ k ] then
|
|
|
|
intensity = max( intensity, v ) -- get maximum intensity among all damage groups
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if intensity > 0 then
|
|
|
|
for i = 1, #params.pieces do
|
|
|
|
local obj = minetest.add_entity( vector.offset_y( self.pos, 1.5 ), "mobs:gibbage" )
|
|
|
|
obj:get_luaentity( ):launch(
|
|
|
|
intensity, params.textures[ random( #params.textures ) ], params.pieces[ i ], params.sound )
|
|
|
|
end
|
|
|
|
else
|
|
|
|
blood_effect( pos )
|
|
|
|
end
|
|
|
|
elseif self.makes_bloodshed_effect and random( 2 ) == 2 then
|
2020-04-30 22:28:00 +02:00
|
|
|
blood_effect( pos )
|
|
|
|
end
|
2020-05-13 16:24:00 +02:00
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
if self.sounds and self.sounds.damage_hand and self.sounds.damage_tool then
|
2020-05-13 16:24:00 +02:00
|
|
|
self:play_sound( minetest.registered_tools[ tool:get_name( ) ] and
|
|
|
|
self.sounds.damage_tool or self.sounds.damage_hand )
|
2020-04-30 22:28:00 +02:00
|
|
|
else
|
2020-05-13 16:24:00 +02:00
|
|
|
self:play_sound( "mobs_damage" )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
if hitter:is_player( ) then
|
2020-05-05 02:41:00 +02:00
|
|
|
if tool then
|
2020-04-30 22:28:00 +02:00
|
|
|
tool:add_wear( 100 )
|
|
|
|
hitter:set_wielded_item( tool )
|
|
|
|
end
|
|
|
|
|
|
|
|
if hp - damage <= self.hp_low then
|
2020-05-19 15:19:00 +02:00
|
|
|
self:reset_alertness( self.retreat_state, { obj = hitter, pos = hitter:get_pos( ) } )
|
|
|
|
else
|
|
|
|
self:reset_alertness( self.defense_state, { obj = hitter, pos = hitter:get_pos( ) } )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
2020-05-13 16:24:00 +02:00
|
|
|
end
|
2020-04-30 22:28:00 +02:00
|
|
|
end,
|
|
|
|
|
|
|
|
on_death = function( self, killer )
|
|
|
|
for _, drop in ipairs( self.drops ) do
|
|
|
|
if random( drop.chance ) == 1 then
|
|
|
|
minetest.spawn_item( self.pos, drop.name .. " " .. random( drop.min, drop.max ), 1, 5 )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if not self.makes_bloodshed_effect then
|
|
|
|
smoke_effect( self.pos )
|
|
|
|
end
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
if self.sounds and self.sounds.death then
|
|
|
|
self:play_sound( self.sounds.death )
|
|
|
|
end
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
if killer:is_player( ) then
|
|
|
|
if self.death_message then
|
|
|
|
minetest.chat_send_all( string.format( self.death_message, killer:get_player_name( ), self.description ) )
|
|
|
|
end
|
|
|
|
minetest.log( "action", name .. " killed by " .. killer:get_player_name( ) )
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
} )
|
|
|
|
end
|
|
|
|
|
|
|
|
mobs.register_spawn = function ( name, def )
|
|
|
|
local max_object_count = def.max_object_count
|
|
|
|
local min_height = def.min_height
|
|
|
|
local max_height = def.max_height
|
|
|
|
local min_light = def.min_light
|
|
|
|
local max_light = def.max_light
|
|
|
|
local can_spawn = def.can_spawn
|
|
|
|
|
|
|
|
minetest.register_abm( {
|
|
|
|
nodenames = def.nodenames,
|
|
|
|
neighbors = { "air" },
|
|
|
|
interval = 10,
|
|
|
|
chance = def.chance,
|
|
|
|
action = function( pos, node, active_object_count, active_object_count_wider )
|
|
|
|
if active_object_count_wider > max_object_count then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local pos2 = vector.offset_y( pos )
|
|
|
|
if minetest.get_node( pos2 ).name ~= "air" or minetest.get_node_above( pos2 ).name ~= "air" then
|
|
|
|
return
|
|
|
|
elseif not check_limits( minetest.get_node_light( pos2 ), min_light, max_light ) then
|
|
|
|
return
|
|
|
|
elseif not check_limits( pos2.y, min_height, max_height ) then
|
|
|
|
return
|
|
|
|
elseif can_spawn and not can_spawn( pos2 ) then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
minetest.log( "action", "Adding mob " .. name .. " on ".. node.name .." at " .. minetest.pos_to_string( pos ) )
|
|
|
|
minetest.add_entity( pos2, name )
|
|
|
|
end
|
|
|
|
} )
|
|
|
|
end
|
|
|
|
|
|
|
|
mobs.register_spawn_near = function ( name, def )
|
|
|
|
local nodenames = def.nodenames
|
|
|
|
local max_light = def.max_light
|
|
|
|
local min_light = def.min_light
|
|
|
|
local chance = def.chance
|
|
|
|
local vert_shift = def.vert_shift
|
|
|
|
local is_area_safe = def.is_area_safe
|
|
|
|
local safe_area = VoxelArea:new( { MinEdge = def.safe_edge1, MaxEdge = def.safe_edge2 } )
|
|
|
|
local can_spawn = def.can_spawn
|
|
|
|
|
|
|
|
globaltimer.shift( 0.5 )
|
|
|
|
globaltimer.start( 10, name, function ( )
|
2020-05-03 22:58:00 +02:00
|
|
|
for player_name, player in pairs( registry.players ) do
|
|
|
|
local player_pos = player:get_pos( )
|
2020-04-30 22:28:00 +02:00
|
|
|
|
|
|
|
if random( chance ) == 1 and is_area_safe == safe_area:containsp( player_pos ) then
|
|
|
|
local positions = minetest.find_nodes_in_area_under_air(
|
|
|
|
vector.offset( player_pos, -10, vert_shift - 5, -10 ),
|
|
|
|
vector.offset( player_pos, 10, vert_shift + 5, 10 ),
|
|
|
|
nodenames )
|
|
|
|
|
|
|
|
if #positions > 0 then
|
|
|
|
local y_offset = minetest.registered_entities[ name ].y_offset
|
|
|
|
local pos = positions[ random( #positions ) ]
|
|
|
|
local pos2 = vector.offset_y( pos, 2 )
|
|
|
|
|
|
|
|
if minetest.get_node( pos2 ).name == "air" and minetest.get_node_above( pos2 ).name == "air" and
|
|
|
|
check_limits( minetest.get_node_light( pos2 ), min_light, max_light ) and
|
|
|
|
( not can_spawn or can_spawn( pos2 ) ) then
|
|
|
|
|
|
|
|
minetest.log( "action", "Adding mob " .. name .. " near player " ..
|
|
|
|
player_name .. " at " .. minetest.pos_to_string( pos2 ) )
|
|
|
|
minetest.add_entity( vector.offset_y( pos2, y_offset ), name )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end )
|
|
|
|
end
|
|
|
|
|
|
|
|
mobs.register_spawner_node = function ( name, def )
|
|
|
|
local chance = def.chance
|
|
|
|
local min_light = def.min_light
|
|
|
|
local max_light = def.max_light
|
|
|
|
local mob_name = def.mob_name
|
|
|
|
|
|
|
|
def.on_timer = function ( pos, elapsed )
|
|
|
|
local y_offset = minetest.registered_entities[ mob_name ].y_offset
|
|
|
|
local pos2 = vector.offset_y( pos )
|
|
|
|
|
|
|
|
if random( chance ) == 1 and check_limits( minetest.get_node_light( pos2 ), min_light, max_light ) and
|
|
|
|
minetest.get_node( pos2 ).name == "air" and minetest.get_node_above( pos2 ).name == "air" then
|
|
|
|
|
|
|
|
minetest.add_entity( vector.offset_y( pos2, y_offset ), mob_name )
|
|
|
|
minetest.log( "action", "Adding mob " .. mob_name .. " on " .. name .. " at " .. minetest.pos_to_string( pos ) )
|
|
|
|
end
|
|
|
|
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
def.after_place_node = function ( pos, player, itemstack )
|
|
|
|
minetest.get_node_timer( pos ):start( 10 )
|
|
|
|
end
|
|
|
|
|
|
|
|
minetest.register_node( name, def )
|
|
|
|
end
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
--------------------
|
|
|
|
|
2020-05-14 03:46:00 +02:00
|
|
|
mobs.play_sound = function ( pos, name )
|
|
|
|
minetest.sound_play( name, { loop = false }, true )
|
2020-05-13 16:24:00 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
mobs.make_noise = function ( pos, radius, group, intensity )
|
2020-05-14 03:46:00 +02:00
|
|
|
axon.generate_radial_stimulus( pos, radius, 0.0, 0.0, { [group] = intensity }, { avatars = true } )
|
2020-05-13 16:24:00 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
mobs.make_noise_repeat = function ( pos, radius, interval, duration, group, intensity )
|
|
|
|
globaltimer.start_now( interval, "noise" .. next_noise_id, function ( this, cycles, period, elapsed )
|
|
|
|
if elapsed >= duration then return true end -- we're finished, so cancel timer
|
2020-05-14 03:46:00 +02:00
|
|
|
axon.generate_radial_stimulus( pos, radius, 0.0, 0.0, { [group] = intensity }, { avatars = true } )
|
2020-05-13 16:24:00 +02:00
|
|
|
end )
|
|
|
|
next_noise_id = next_noise_id + 1
|
|
|
|
end
|
|
|
|
|
2020-05-08 02:27:00 +02:00
|
|
|
mobs.insert_object = function ( id, obj )
|
|
|
|
registry.objects[ id ] = obj
|
|
|
|
end
|
|
|
|
|
|
|
|
mobs.remove_object = function ( id )
|
|
|
|
registry.objects[ id ] = nil
|
|
|
|
end
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
--------------------
|
|
|
|
|
2020-05-04 01:39:00 +02:00
|
|
|
mobs.presets = {
|
|
|
|
grab_handout = function ( def )
|
|
|
|
local grab_chance = def.grab_chance
|
|
|
|
local wait_chance = def.wait_chance
|
|
|
|
local can_eat = def.can_eat
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
return function ( self, target_obj, elapsed )
|
2020-05-04 01:39:00 +02:00
|
|
|
|
|
|
|
if random( grab_chance ) == 1 then
|
2020-05-13 16:24:00 +02:00
|
|
|
local target_pos = vector.offset_y( target_obj:get_pos( ) )
|
2020-05-04 01:39:00 +02:00
|
|
|
local dist = vector.distance( self.pos, target_pos )
|
|
|
|
|
2020-05-13 16:24:00 +02:00
|
|
|
if self:get_direct_yaw_delta( target_pos ) < rad_30 and dist <= self.pickup_range then
|
2020-05-14 03:46:00 +02:00
|
|
|
if target_obj:is_player( ) then
|
|
|
|
target_obj:get_wielded_item( ):take_item( )
|
|
|
|
target_obj:set_wielded_item( "" )
|
|
|
|
elseif target_obj:get_entity_name( ) == "__builtin:item" then
|
|
|
|
target_obj:remove( )
|
2020-05-04 01:39:00 +02:00
|
|
|
end
|
|
|
|
if can_eat then
|
|
|
|
-- minetest.sound_play( "hbhunger_eat_generic", {
|
|
|
|
-- object = self.object,
|
|
|
|
-- max_hear_distance = 10,
|
|
|
|
-- gain = 1.0
|
|
|
|
-- } )
|
|
|
|
minetest.add_particle( {
|
|
|
|
pos = vector.offset_y( self.pos, 0.5 ),
|
|
|
|
velocity = { x = 0, y = 0.8, z = 0 },
|
|
|
|
acceleration = { x = 0, y = 0, z = 0 },
|
|
|
|
exptime = 4.0,
|
|
|
|
size = 3,
|
|
|
|
collisiondetection = false,
|
|
|
|
vertical = true,
|
|
|
|
texture = "heart.png",
|
|
|
|
} )
|
|
|
|
end
|
2020-05-19 15:19:00 +02:00
|
|
|
return self.neutral_state
|
2020-05-04 01:39:00 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-19 15:19:00 +02:00
|
|
|
return random( wait_chance ) == 1 and "follow" or self.offense_state
|
2020-05-04 01:39:00 +02:00
|
|
|
end
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-05-09 14:34:00 +02:00
|
|
|
local emit_defs = { "mobs:griefer_ghost" }
|
|
|
|
|
|
|
|
minetest.register_chatcommand( "mobs", {
|
|
|
|
description = "Spawn random mobs in the area (for testing purposes or just plain fun).",
|
|
|
|
privs = { server = true },
|
|
|
|
func = function( player_name, param )
|
|
|
|
local pos = minetest.get_player_by_name( player_name ):getpos( )
|
|
|
|
local total = param ~= "" and tonumber( param ) or 10
|
|
|
|
|
|
|
|
for count = 1, total do
|
|
|
|
local index = math.random( #emit_defs )
|
|
|
|
local y = pos.y + minetest.registered_entities[ emit_defs[ index ] ].y_offset + 2
|
|
|
|
local x = pos.x + math.random( -5, 5 )
|
|
|
|
local z = pos.z + math.random( -5, 5 )
|
|
|
|
|
|
|
|
minetest.add_entity( { x = x, y = y, z = z }, emit_defs[ index ] )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
} )
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
|
2020-04-30 22:28:00 +02:00
|
|
|
dofile( minetest.get_modpath( "mobs" ) .. "/extras.lua" )
|
|
|
|
dofile( minetest.get_modpath( "mobs" ) .. "/monsters.lua" )
|
|
|
|
dofile( minetest.get_modpath( "mobs" ) .. "/animals.lua" )
|
|
|
|
|
|
|
|
-- compatibility for Minetest S3 engine
|
|
|
|
|
|
|
|
if not vector.origin then
|
2020-05-04 01:39:00 +02:00
|
|
|
dofile( minetest.get_modpath( "mobs" ) .. "/compatibility.lua" )
|
2020-04-30 22:28:00 +02:00
|
|
|
end
|
2020-05-19 15:19:00 +02:00
|
|
|
|