- Closes #10
- Closes #16
- Closes #20
This commit is contained in:
Leslie Krause 2020-05-13 21:46:00 -04:00
parent a849767796
commit 0ae41552a8
4 changed files with 359 additions and 175 deletions

View File

@ -27,11 +27,11 @@ Here are some of the other highlights of the Mobs Lite engine:
* Animals will attempt to flee to safety when a player that previously punched them * Animals will attempt to flee to safety when a player that previously punched them
returns to the field of view. returns to the field of view.
* Creatures avoid running into most obstacles by analyzing the surroundings and steadily * Animals can be programmed to follow players that are wielding food and to eat directly
adjusting their course. from the player's hand.
* Mobs can be randomly spawned in the vicinity of players, thus relieving the overhead of * Seamless integration with Axon allows Mobs to react to various environmental stimulii
costly ABM-based spawners. like smells, sounds, etc.
* Timekeeper helper class ensures efficient dispatching of mob-related callbacks at the * Timekeeper helper class ensures efficient dispatching of mob-related callbacks at the
appropriate server step. appropriate server step.
@ -39,6 +39,12 @@ Here are some of the other highlights of the Mobs Lite engine:
* Builtin lookup table allows for efficiently iterating over multiple classes of objects * Builtin lookup table allows for efficiently iterating over multiple classes of objects
within a specific radius. within a specific radius.
* Mobs can be randomly spawned in the vicinity of players, thus relieving the overhead of
costly ABM-based spawners.
* Mobs will avoid running into most obstacles by analyzing the surroundings and steadily
adjusting their course.
* And of course, much much more! * And of course, much much more!
Since Mobs Lite is still in early beta, there is the likelihood of lingering bugs. The API Since Mobs Lite is still in early beta, there is the likelihood of lingering bugs. The API
@ -70,6 +76,9 @@ TNT Mod (required)
Default Mod (required) Default Mod (required)
https://github.com/minetest-game-mods/default https://github.com/minetest-game-mods/default
Axon Mod (optional)
https://bitbucket.org/sorcerykid/axon
Installation Installation
---------------------- ----------------------

View File

@ -34,21 +34,32 @@ mobs.register_mob( "mobs:kitten", {
hunger_params = { offset = -0.5, spread = 2.5 }, hunger_params = { offset = -0.5, spread = 2.5 },
alertness_states = { alertness_states = {
ignore = { view_offset = 2, view_radius = 4, view_height = 4, view_acuity = 3 }, ignore = { view_offset = 2, view_radius = 4, view_height = 4, view_acuity = 3 },
search = { view_offset = 2, view_radius = 10, view_height = 4, view_acuity = 3, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "follow"
end },
follow = { view_offset = 2, view_radius = 10, view_height = 4, view_acuity = 3 }, follow = { view_offset = 2, view_radius = 10, view_height = 4, view_acuity = 3 },
escape = { view_offset = 2, view_radius = 10, view_height = 4, view_acuity = 3 }, escape = { view_offset = 2, view_radius = 10, view_height = 4, view_acuity = 3 },
}, },
awareness_stages = {
search = { decay = 12.0, abort_state = "ignore" },
follow = { decay = 0.0, abort_state = "search" },
escape = { decay = 12.0, abort_state = "ignore" },
},
certainty = 1.0, certainty = 1.0,
sensitivity = 0.6, sensitivity = 0.6,
fear_factor = 0, fear_factor = 6,
flee_factor = 10, flee_factor = 10,
sneak_velocity = 0.8,
walk_velocity = 0.8, walk_velocity = 0.8,
stray_velocity = 1.0, stray_velocity = 1.0,
recoil_velocity = 1.0, recoil_velocity = 1.0,
run_velocity = 1.2, run_velocity = 1.2,
escape_range = 3.0, search_range = 3.0,
follow_range = 2.0, follow_range = 2.0,
pickup_range = 2.0, pickup_range = 2.0,
escape_range = 3.0,
can_jump = false, can_jump = false,
can_walk = true, can_walk = true,
enable_fall_damage = true, enable_fall_damage = true,
@ -129,21 +140,32 @@ mobs.register_mob( "mobs:rat", {
hunger_params = { offset = 0.0, spread = 2.5 }, hunger_params = { offset = 0.0, spread = 2.5 },
alertness_states = { alertness_states = {
ignore = { view_offset = 0, view_radius = 3, view_height = 3, view_acuity = 4 }, ignore = { view_offset = 0, view_radius = 3, view_height = 3, view_acuity = 4 },
search = { view_offset = 0, view_radius = 6, view_height = 3, view_acuity = 6, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "follow"
end },
follow = { view_offset = 0, view_radius = 6, view_height = 3, view_acuity = 6 }, follow = { view_offset = 0, view_radius = 6, view_height = 3, view_acuity = 6 },
escape = { view_offset = 0, view_radius = 6, view_height = 3, view_acuity = 6 }, escape = { view_offset = 0, view_radius = 6, view_height = 3, view_acuity = 6 },
}, },
awareness_stages = {
search = { decay = 12.0, abort_state = "ignore" },
follow = { decay = 0.0, abort_state = "search" },
escape = { decay = 12.0, abort_state = "ignore" },
},
certainty = 1.0, certainty = 1.0,
sensitivity = 0.0, sensitivity = 0.0,
fear_factor = 8, fear_factor = 8,
flee_factor = 10, flee_factor = 10,
sneak_velocity = 0.5,
walk_velocity = 0.5, walk_velocity = 0.5,
stray_velocity = 0.5, stray_velocity = 0.5,
recoil_velocity = 0.5, recoil_velocity = 0.5,
run_velocity = 1.2, run_velocity = 1.2,
escape_range = 0.0, search_range = 2.0,
follow_range = 2.0, follow_range = 2.0,
pickup_range = 2.0, pickup_range = 2.0,
escape_range = 0.0,
can_jump = false, can_jump = false,
can_walk = true, can_walk = true,
enable_fall_damage = true, enable_fall_damage = true,
@ -212,21 +234,32 @@ mobs.register_mob( "mobs:hare", {
hunger_params = { offset = 0.0, spread = 2.0 }, hunger_params = { offset = 0.0, spread = 2.0 },
alertness_states = { alertness_states = {
ignore = { view_offset = 0, view_radius = 2, view_height = 6, view_acuity = 0 }, ignore = { view_offset = 0, view_radius = 2, view_height = 6, view_acuity = 0 },
search = { view_offset = 0, view_radius = 12, view_height = 6, view_acuity = 3, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "follow"
end },
follow = { view_offset = 0, view_radius = 12, view_height = 6, view_acuity = 3 }, follow = { view_offset = 0, view_radius = 12, view_height = 6, view_acuity = 3 },
escape = { view_offset = 0, view_radius = 12, view_height = 6, view_acuity = 3 }, escape = { view_offset = 0, view_radius = 12, view_height = 6, view_acuity = 3 },
}, },
awareness_stages = {
search = { decay = 12.0, abort_state = "ignore" },
follow = { decay = 0.0, abort_state = "search" },
escape = { decay = 12.0, abort_state = "ignore" },
},
certainty = 1.0, certainty = 1.0,
sensitivity = 0.5, sensitivity = 0.5,
fear_factor = 6, fear_factor = 6,
flee_factor = 10, flee_factor = 10,
sneak_velocity = 2.0,
walk_velocity = 3.0, walk_velocity = 3.0,
stray_velocity = 1.0, stray_velocity = 1.0,
recoil_velocity = 2.0, recoil_velocity = 2.0,
run_velocity = 3.5, run_velocity = 3.5,
escape_range = 3.0, search_range = 3.0,
follow_range = 3.0, follow_range = 3.0,
pickup_range = 2.0, pickup_range = 2.0,
escape_range = 3.0,
can_jump = true, can_jump = true,
can_walk = true, can_walk = true,
enable_fall_damage = true, enable_fall_damage = true,
@ -306,18 +339,29 @@ mobs.register_mob( "mobs:chicken", {
hunger_params = { offset = 0.0, spread = 2.0 }, hunger_params = { offset = 0.0, spread = 2.0 },
alertness_states = { alertness_states = {
ignore = { view_offset = 2, view_radius = 4, view_height = 4, view_acuity = 0 }, ignore = { view_offset = 2, view_radius = 4, view_height = 4, view_acuity = 0 },
search = { view_offset = 2, view_radius = 6, view_height = 4, view_acuity = 2, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "follow"
end },
follow = { view_offset = 2, view_radius = 6, view_height = 4, view_acuity = 2 }, follow = { view_offset = 2, view_radius = 6, view_height = 4, view_acuity = 2 },
escape = { view_offset = 2, view_radius = 6, view_height = 4, view_acuity = 2 }, escape = { view_offset = 2, view_radius = 6, view_height = 4, view_acuity = 2 },
}, },
awareness_stages = {
search = { decay = 12.0, abort_state = "ignore" },
follow = { decay = 0.0, abort_state = "search" },
escape = { decay = 12.0, abort_state = "ignore" },
},
certainty = 1.0, certainty = 1.0,
sensitivity = 0.3, sensitivity = 0.3,
fear_factor = 2, fear_factor = 2,
flee_factor = 10, flee_factor = 10,
sneak_velocity = 1.5,
walk_velocity = 1.5, walk_velocity = 1.5,
stray_velocity = 1.0, stray_velocity = 1.0,
recoil_velocity = 2.0, recoil_velocity = 2.0,
run_velocity = 2.0, run_velocity = 2.0,
search_range = 3.0,
follow_range = 3.0, follow_range = 3.0,
pickup_range = 2.0, pickup_range = 2.0,
escape_range = 3.0, escape_range = 3.0,

399
init.lua
View File

@ -20,6 +20,8 @@ local world_gravity = -10
local liquid_density = 0.5 local liquid_density = 0.5
local liquid_viscosity = 0.6 local liquid_viscosity = 0.6
local next_noise_id = 1
-------------------- --------------------
local random = math.random local random = math.random
@ -66,7 +68,7 @@ function Timekeeper( this )
end end
self.start_now = function ( period, name, func ) self.start_now = function ( period, name, func )
if func( this, 0, period, 0.0, 0.0 ) then if not func( this, 0, period, 0.0, 0.0 ) then
timer_defs[ name ] = { cycles = 0, period = period, expiry = clock + period, started = clock, func = func } timer_defs[ name ] = { cycles = 0, period = period, expiry = clock + period, started = clock, func = func }
end end
end end
@ -256,8 +258,9 @@ minetest.register_on_leaveplayer( function( player, is_timeout )
-- delete all target references (if applicable) -- delete all target references (if applicable)
for id, obj in pairs( registry.avatars ) do for id, obj in pairs( registry.avatars ) do
if obj:get_luaentity( ).target == player then local this = obj:get_luaentity( )
this:reset_target( ) if this.target and this.target.obj == player then
this:reset_alertness( "ignore" )
end end
end end
registry.players[ name ] = nil registry.players[ name ] = nil
@ -287,8 +290,9 @@ end
builtin_item.on_deactivate = function ( self, id ) builtin_item.on_deactivate = function ( self, id )
for id, obj in pairs( registry.avatars ) do for id, obj in pairs( registry.avatars ) do
if obj:get_luaentity( ).target == self.object then local this = obj:get_luaentity( )
this:reset_target( ) if this.target and this.target.obj == self.object then
this:reset_alertness( "ignore" )
end end
end end
registry.spawnitems[ id ] = nil registry.spawnitems[ id ] = nil
@ -324,11 +328,13 @@ mobs.register_mob = function ( name, def )
receptrons = def.receptrons, receptrons = def.receptrons,
death_message = def.death_message, death_message = def.death_message,
alertness_states = def.alertness_states, alertness_states = def.alertness_states,
awareness_stages = def.awareness_stages,
sensitivity = def.sensitivity or 0.0, sensitivity = def.sensitivity or 0.0,
certainty = def.certainty or 1.0, certainty = def.certainty or 1.0,
attack_range = def.attack_range, attack_range = def.attack_range,
escape_range = def.escape_range, escape_range = def.escape_range,
follow_range = def.follow_range, follow_range = def.follow_range,
search_range = def.search_range,
pickup_range = def.pickup_range, pickup_range = def.pickup_range,
sneak_velocity = def.sneak_velocity, sneak_velocity = def.sneak_velocity,
walk_velocity = def.walk_velocity, walk_velocity = def.walk_velocity,
@ -351,9 +357,9 @@ mobs.register_mob = function ( name, def )
shoot_period = def.shoot_period, shoot_period = def.shoot_period,
shoot_chance = def.shoot_chance, shoot_chance = def.shoot_chance,
weapon_params = def.weapon_params, weapon_params = def.weapon_params,
watch_wielditems = def.watch_wielditems, watch_wielditems = def.watch_wielditems or { },
watch_spawnitems = def.watch_spawnitems, watch_spawnitems = def.watch_spawnitems or { },
watch_players = def.watch_players, watch_players = def.watch_players or { },
sounds = def.sounds, sounds = def.sounds,
animation = def.animation, animation = def.animation,
fear_factor = def.fear_factor, fear_factor = def.fear_factor,
@ -365,11 +371,13 @@ mobs.register_mob = function ( name, def )
enable_swimming = true, enable_swimming = true,
shoot_count = 0, shoot_count = 0,
timeout = def.timeout, timeout = def.timeout,
neutral_state = "ignore",
defense_state = def.type == "monster" and "attack" or "ignore",
is_tamed = false, is_tamed = false,
description = def.description, description = def.description,
after_activate = def.after_activate, after_activate = def.after_activate,
before_deactivate = def.before_deactivate, before_deactivate = def.before_deactivate,
after_state_change = def.after_state_change, before_state_change = def.before_state_change,
before_punch = def.before_punch, before_punch = def.before_punch,
-- prepare noise generator with seed, octaves, persistence, spread -- prepare noise generator with seed, octaves, persistence, spread
@ -426,14 +434,14 @@ mobs.register_mob = function ( name, def )
get_direct_yaw_delta = function ( self, pos ) get_direct_yaw_delta = function ( self, pos )
local yaw = self:get_direct_yaw( pos ) local yaw = self:get_direct_yaw( pos )
return abs( normalize_angle( yaw - self.yaw ) ) return abs( normalize_angle( yaw - self.yaw ) )
end
get_target_yaw = function ( self, pos, r_limit, r_ratio )
return self:get_direct_yaw( pos ) + upper_random( r_limit, r_ratio or 1 )
end, end,
get_target_yaw_delta = function ( self, pos ) get_target_yaw = function ( self, r_limit, r_ratio )
local yaw = self:get_direct_yaw( pos ) return self:get_direct_yaw( self.target.pos ) + upper_random( r_limit, r_ratio or 1 )
end,
get_target_yaw_delta = function ( self )
local yaw = self:get_direct_yaw( self.target.pos )
return abs( normalize_angle( yaw - self.yaw ) ) return abs( normalize_angle( yaw - self.yaw ) )
end, end,
@ -482,7 +490,7 @@ mobs.register_mob = function ( name, def )
end, end,
play_sound = function ( self, name ) play_sound = function ( self, name )
minetest.sound_play( name, { object = self.object } ) minetest.sound_play( name, { object = self.object, loop = false } )
end, end,
play_sound_repeat = function ( self, name ) play_sound_repeat = function ( self, name )
@ -504,31 +512,34 @@ mobs.register_mob = function ( name, def )
-- sensory processing -- -- sensory processing --
check_suspect = function ( self, target, elapsed ) check_suspect = function ( self, target_obj, clarity, elapsed )
local entity = target:get_luaentity( ) -- default to defense or neutral state if not in any watch list
local suspect local suspect = random( 10 ) <= self.fear_factor and self.defense_state or self.neutral_state
local entity = target_obj:get_luaentity( )
if not entity then if not entity then
local player_name = target:get_player_name( ) local player_name = target_obj:get_player_name( )
local item_name = target:get_wielded_item( ):get_name( ) local item_name = target_obj:get_wielded_item( ):get_name( )
suspect = self.watch_players[ player_name ] or self.watch_wielditems[ item_name ] suspect = self.watch_players[ player_name ] or self.watch_wielditems[ item_name ] or suspect
elseif entity.name == "__builtin:item" then elseif entity.name == "__builtin:item" then
suspect = self.watch_spawnitems[ entity.item_name ] suspect = self.watch_spawnitems[ entity.item_name ] or suspect
end end
if type( suspect ) == "function" then if type( suspect ) == "function" then
return suspect( self, target, elapsed or 0.0 ) return suspect( self, target_obj, clarity, elapsed or 0.0 ) -- must always return a valid state
else else
return suspect return suspect
end end
end, end,
is_paranoid = function ( self, target_pos ) get_visibility = function ( self, target_pos )
local length = get_vector_length( self.pos, target_pos ) local length = get_vector_length( self.pos, target_pos )
local height = get_vector_height( self.pos, target_pos )
local this = self.alertness local this = self.alertness
if not this or length > 48 then if not this or length > 48 or height > 48 then -- immediately eliminate far objects
return false return 0.0, false, false
else else
local view_range = this.view_radius + this.view_offset local view_range = this.view_radius + this.view_offset
local view_pos = this.view_offset == 0.0 and self.pos or vector.new( local view_pos = this.view_offset == 0.0 and self.pos or vector.new(
@ -538,93 +549,123 @@ mobs.register_mob = function ( name, def )
) )
local radius = get_vector_length( view_pos, target_pos ) local radius = get_vector_length( view_pos, target_pos )
local height = get_vector_height( view_pos, target_pos )
local clarity = get_power_decrease( 1.0, this.view_acuity, length / view_range ) 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) -- certainty factor ranges from 0 (target never evident) to 1 (target always evident)
-- sensitivity threshold ranges from 0 (full perception) to 1 (no perception) -- sensitivity threshold ranges from 0 (full perception) to 1 (no perception)
--node_locator( vector.offset_y( view_pos ), 4.5, 1.0, "yellow" ) --node_locator( vector.offset_y( view_pos ), 4.5, 1.0, "yellow" )
--printf( "is_paranoid(%s):\n[%s] radius <= view_radius\n[%s] height <= view_height\n%0.2f = clarity (%0.2f = sensitivity, %0.2f = certainty)", self.name, radius <= this.view_radius and "x" or " ", height <= this.view_height and "x" or " ", clarity, self.sensitivity, self.certainty ) --printf( "is_paranoid(%s):\n[%s] radius <= view_radius\n[%s] height <= view_height\n%0.2f = clarity (%0.2f = sensitivity, %$
return radius <= this.view_radius and height <= this.view_height and local is_visible = radius <= this.view_radius and height <= this.view_height
clarity > self.sensitivity and clarity * self.certainty > random( ) local is_evident = clarity > self.sensitivity and clarity * self.certainty > random( )
return clarity, is_visible, is_evident
end end
end, end,
-- state-change handlers -- -- target functions --
reset_target = function ( self ) create_target = function ( self, obj )
if self.target then local pos = obj:get_pos( )
self:set_ignore_state( ) 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
return alertness.view_filter( self, obj, clarity ), { obj = obj, pos = pos }
elseif clarity > 0.0 then
return self:check_suspect( obj, clarity ), { obj = obj, pos = pos }
end
else
return self.state, self.target
end end
end, end,
set_ignore_state = function ( self ) verify_target = function ( self, elapsed )
if self.state == "ignore" then return end 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 ]
self.state = "ignore" -- update last-known target position only if visible and evident
self.target = nil if is_visible and is_evident then
self.alertness = self.alertness_states.ignore self.target.pos = target_pos
self.is_tamed = false else
clarity = 0.0
end
if self.custom.after_state_change then if alertness and alertness.view_filter then
self.custom.after_state_change( self ) return alertness.view_filter( self, self.target.obj, clarity )
elseif clarity > 0.0 then
return self:check_suspect( self.target.obj, clarity, elapsed )
else
return self.abort_state or self.neutral_state
end
end end
self:start_ignore_action( ) return self.state
end, end,
set_escape_state = function ( self, target ) locate_target = function ( self )
if self.state == "escape" then return end if self.sounds and self.sounds.random and random( 35 ) == 1 then
self.state = "escape" minetest.sound_play( self.sounds.random, { object = self.object } )
end
-- 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
if random( 10 ) <= self.fear_factor and obj:is_player( ) and not obj:get_attach( ) then
local state, target = self:create_target( obj )
if state ~= self.state then
self:reset_alertness( state, target )
return
end
end
end
end,
-- alertness and awareness functions --
start_awareness = function ( self, target )
local awareness = self.awareness_stages[ self.state ]
if awareness then
self.abort_state = awareness.abort_state
if awareness.decay > 0 then
self.timekeeper.start( awareness.decay, "awareness", function ( )
self:reset_alertness( self.abort_state, self.target )
end )
else
self.timekeeper.clear( "awareness" )
end
else
self.abort_state = nil
self.timekeeper.clear( "awareness" )
end
end,
reset_alertness = function ( self, state, target )
if state == self.state then return end
if self.before_state_change then
self:before_state_change( self.state, state )
end
self.state = state
self.target = target self.target = target
self.alertness = self.alertness_states.escape self.alertness = self.alertness_states[ state ]
self.is_tamed = false
if random( 2 ) == 1 then self:start_awareness( )
-- this is a bad player, so keep watch self.action_funcs[ state ]( self )
self.watch_players[ target:get_player_name( ) ] = "escape"
end
if self.custom.after_state_change then
self.custom.after_state_change( self )
end
self:start_escape_action( )
end,
set_follow_state = function ( self, target )
if self.state == "follow" then return end
self.state = "follow"
self.target = target
self.alertness = self.alertness_states.follow
self.is_tamed = true
if self.custom.after_state_change then
self.custom.after_state_change( self )
end
self:start_follow_action( )
end,
set_attack_state = function ( self, target )
if self.state == "attack" then return end
self.state = "attack"
self.target = target
self.alertness = self.alertness_states.attack
self.is_tamed = false
if self.custom.after_state_change then
self.custom.after_state_change( self )
end
self:start_attack_action( )
end, end,
-- action hooks -- -- action hooks --
start_ignore_action = function ( self ) start_ignore_action = function ( self )
self.target = nil -- forget any target
if not self.move_result.is_standing then if not self.move_result.is_standing then
-- always stand when in mid-air -- always stand when in mid-air
self:set_speed( 0 ) self:set_speed( 0 )
@ -637,7 +678,7 @@ mobs.register_mob = function ( name, def )
self:set_animation( "stand" ) self:set_animation( "stand" )
end end
self.timekeeper.start( 1.0, "hunger", self.handle_hunger ) self.timekeeper.start( 1.0, "hunger", self.locate_target )
self.timekeeper.start( 1.0, "action", self.on_ignore_action ) self.timekeeper.start( 1.0, "action", self.on_ignore_action )
end, end,
@ -727,6 +768,8 @@ mobs.register_mob = function ( name, def )
end, end,
start_follow_action = function ( self ) start_follow_action = function ( self )
assert( self.target.pos ) -- sanity check
self:turn_to( self:get_target_yaw( rad_20 ), 10 ) self:turn_to( self:get_target_yaw( rad_20 ), 10 )
self:set_speed( self.recoil_velocity ) self:set_speed( self.recoil_velocity )
self:set_animation( "walk" ) self:set_animation( "walk" )
@ -736,13 +779,10 @@ mobs.register_mob = function ( name, def )
end, end,
on_follow_action = function ( self, cycles, period, elapsed ) on_follow_action = function ( self, cycles, period, elapsed )
local target_pos = self:get_target_pos( 0.5 ) if cycles % 2 == 0 then -- validate target every 1.0 seconds
local dist = vector.distance( self.pos, target_pos ) local goal_state = self:verify_target( )
if goal_state ~= self.state then
if cycles % 2 == 0 then self:reset_alertness( goal_state, self.target )
local next_state = validate_target( )
if next_state ~= self.state then
self:reset_state( next_state, self.target )
return return
end end
end end
@ -753,6 +793,9 @@ mobs.register_mob = function ( name, def )
end end
end end
local target_pos = self.target.pos
local dist = vector.distance( self.pos, target_pos )
if dist <= self.follow_range then if dist <= self.follow_range then
if self.speed > 0 then if self.speed > 0 then
self:set_speed( 0 ) self:set_speed( 0 )
@ -786,10 +829,71 @@ mobs.register_mob = function ( name, def )
end end
end, end,
start_escape_action = function ( self ) start_search_action = function ( self )
local target_pos = vector.offset_y( self.target:get_pos( ) ) assert( self.target.pos ) -- sanity check
local dist = vector.distance( self.pos, self.target.pos )
self:turn_to( self:get_target_yaw( rad_5 ), 10 )
self:set_speed( self.walk_velocity )
self:set_animation( "walk" )
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
local dist = vector.distance( self.pos, target_pos )
-- 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
self:turn_to( self:get_random_yaw( rad_180 ), 20 )
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 )
elseif random( 2 ) == 1 then
self.object:set_velocity_vert( 5 )
end
end
end
end
end,
start_escape_action = function ( self )
assert( self.target.pos ) -- sanity check
local dist = vector.distance( self.pos, self.target.pos )
if self:get_target_yaw_delta( ) < rad_60 and dist <= self.escape_range then if self:get_target_yaw_delta( ) < rad_60 and dist <= self.escape_range then
-- recoil if facing intruder -- recoil if facing intruder
self:set_speed( -self.recoil_velocity ) self:set_speed( -self.recoil_velocity )
@ -804,6 +908,11 @@ mobs.register_mob = function ( name, def )
self:set_animation( "walk" ) self:set_animation( "walk" )
end end
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
if self.sounds and self.sounds.escape and random( 2 ) == 1 then if self.sounds and self.sounds.escape and random( 2 ) == 1 then
minetest.sound_play( self.sounds.escape, { object = self.object } ) minetest.sound_play( self.sounds.escape, { object = self.object } )
end end
@ -813,16 +922,17 @@ mobs.register_mob = function ( name, def )
end, end,
on_escape_action = function ( self, cycles, period, elapsed ) on_escape_action = function ( self, cycles, period, elapsed )
local target_pos = self:get_target_pos( 0.5 )
local dist = vector.distance( self.pos, target_pos )
if cycles % 2 == 0 then if cycles % 2 == 0 then
if not self:is_paranoid( elapsed ) or random( 10 ) > self.flee_factor then local goal_state = self:verify_target( )
self:lower_awareness( ) if goal_state ~= self.state or random( 10 ) > self.flee_factor then
self:reset_alertness( goal_state, self.target )
return return
end end
end end
local target_pos = self.target.pos
local dist = vector.distance( self.pos, target_pos )
if dist <= self.escape_range then if dist <= self.escape_range then
-- if close, keep backtracking -- if close, keep backtracking
if cycles % 2 == 0 then if cycles % 2 == 0 then
@ -867,6 +977,8 @@ mobs.register_mob = function ( name, def )
end, end,
start_attack_action = function ( self ) start_attack_action = function ( self )
assert( self.target.pos and self.target.obj ) -- sanity check
self:set_speed( self.run_velocity ) self:set_speed( self.run_velocity )
self:set_animation( "run" ) self:set_animation( "run" )
self:turn_to( self:get_target_yaw( rad_90 ), 10 ) self:turn_to( self:get_target_yaw( rad_90 ), 10 )
@ -880,23 +992,24 @@ mobs.register_mob = function ( name, def )
end, end,
on_attack_action = function ( self, cycles, period, elapsed ) on_attack_action = function ( self, cycles, period, elapsed )
if self.target:get_hp( ) == 0 or self.target:get_attach( ) then if self.target.obj:get_hp( ) == 0 or self.target.obj:get_attach( ) then
self:set_ignore_state( ) self:reset_alertness( "ignore" )
return return
end end
if cycles % 5 == 0 then -- validate target once per second if cycles % 5 == 0 then -- validate target once per second
if not self:is_paranoid( elapsed ) then local goal_state = self:verify_target( )
self:lower_awareness( ) if goal_state ~= self.state then
self:reset_alertness( goal_state, self.target )
return return
end end
end end
self:turn_to( self:get_target_yaw( rad_0 ), 5 ) local target_pos = self.target.pos
local target_pos = vector.offset_y( self.target.pos, 0.5 )
local dist = vector.distance( self.pos, target_pos ) local dist = vector.distance( self.pos, target_pos )
self:turn_to( self:get_target_yaw( rad_0 ), 5 )
if dist <= self.attack_range then if dist <= self.attack_range then
if self.attack_type == "shoot" and self.fire_weapon then if self.attack_type == "shoot" and self.fire_weapon then
if cycles % ( self.shoot_period * 5 ) == 0 and random( self.shoot_chance ) == 1 then if cycles % ( self.shoot_period * 5 ) == 0 and random( self.shoot_chance ) == 1 then
@ -915,7 +1028,7 @@ mobs.register_mob = function ( name, def )
minetest.sound_play( self.sounds.attack, { object = self.object } ) minetest.sound_play( self.sounds.attack, { object = self.object } )
end end
-- if minetest.line_of_sight( pos, target_pos, 0.5 ) then -- do not hit player through walls! -- if minetest.line_of_sight( pos, target_pos, 0.5 ) then -- do not hit player through walls!
self.target:punch( self.object, 1.0, { self.target.obj:punch( self.object, 1.0, {
full_punch_interval = 1.0, full_punch_interval = 1.0,
damage_groups = { fleshy=self.damage } damage_groups = { fleshy=self.damage }
}, vector.direction( self.pos, target_pos ) ) }, vector.direction( self.pos, target_pos ) )
@ -1027,7 +1140,7 @@ mobs.register_mob = function ( name, def )
self.cur_animation = type self.cur_animation = type
end, end,
-- damage and hunger routines -- -- damage handler --
handle_damage = function ( self ) handle_damage = function ( self )
-- handle environmental damage (light, water, and lava) -- handle environmental damage (light, water, and lava)
@ -1066,24 +1179,6 @@ mobs.register_mob = function ( name, def )
end end
end, end,
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 } )
end
-- 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
if random( 10 ) <= self.fear_factor and obj:is_player( ) and not obj:get_attach( ) then
local state, target = self:create_target( obj )
if state ~= self.state then
self:reset_state( state, target )
return
end
end
end
end,
-- generic callbacks -- -- generic callbacks --
on_step = function( self, dtime, pos, rot, new_vel, old_vel, move_result ) on_step = function( self, dtime, pos, rot, new_vel, old_vel, move_result )
@ -1153,19 +1248,13 @@ mobs.register_mob = function ( name, def )
self.move_result = { collides_xz = false, collides_y = true, is_standing = true } self.move_result = { collides_xz = false, collides_y = true, is_standing = true }
self.pos = self.object:get_pos( ) self.pos = self.object:get_pos( )
if self.type == "monster" then self.action_funcs = {
self.on_create_target = function ( obj, clarity ) ignore = self.start_ignore_action,
return clarity < 0.5 and "search" or "attack" remark = self.start_remark_action,
end search = self.start_search_action,
else follow = self.start_follow_action,
self:set_escape_state( obj ) escape = self.start_escape_action,
return attack = self.start_attack_action,
end
self.reset_funcs = {
ignore = self.set_ignore_state,
remark = self.set_remark_state,
search = self.set_search_state,
} }
if staticdata then if staticdata then
@ -1191,7 +1280,7 @@ mobs.register_mob = function ( name, def )
self.after_activate( self, id ) self.after_activate( self, id )
end end
self:set_ignore_state( ) self:reset_alertness( self.neutral_state )
end, end,
on_deactivate = function ( self, id ) on_deactivate = function ( self, id )
@ -1238,9 +1327,9 @@ mobs.register_mob = function ( name, def )
end end
if hp - damage <= self.hp_low then if hp - damage <= self.hp_low then
self:set_escape_state( hitter ) self:reset_alertness( "escape", { obj = hitter, pos = hitter:get_pos( ) } )
elseif self.type == "monster" then elseif self.type == "monster" then
self:set_attack_state( hitter ) self:reset_alertness( "attack", { obj = hitter, pos = hitter:get_pos( ) } )
end end
end end
end, end,
@ -1373,18 +1462,18 @@ end
-------------------- --------------------
mobs.play_sound = function ( obj, name ) mobs.play_sound = function ( pos, name )
minetest.sound_play( name, { loop = false }, true ) minetest.sound_play( name, { loop = false }, true )
end end
mobs.make_noise = function ( pos, radius, group, intensity ) mobs.make_noise = function ( pos, radius, group, intensity )
axon.generate_radial_stimulus( pos, radius, 0.0, 0.0, 1, { [group] = intensity }, { avatars = true } ) axon.generate_radial_stimulus( pos, radius, 0.0, 0.0, { [group] = intensity }, { avatars = true } )
end end
mobs.make_noise_repeat = function ( pos, radius, interval, duration, group, intensity ) mobs.make_noise_repeat = function ( pos, radius, interval, duration, group, intensity )
globaltimer.start_now( interval, "noise" .. next_noise_id, function ( this, cycles, period, elapsed ) 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 if elapsed >= duration then return true end -- we're finished, so cancel timer
axon.generate_radial_stimulus( pos, radius, 0.0, 0.0, 1, { [group] = intensity }, { avatars = true } ) axon.generate_radial_stimulus( pos, radius, 0.0, 0.0, { [group] = intensity }, { avatars = true } )
end ) end )
next_noise_id = next_noise_id + 1 next_noise_id = next_noise_id + 1
end end
@ -1412,11 +1501,11 @@ mobs.presets = {
local dist = vector.distance( self.pos, target_pos ) local dist = vector.distance( self.pos, target_pos )
if self:get_direct_yaw_delta( target_pos ) < rad_30 and dist <= self.pickup_range then if self:get_direct_yaw_delta( target_pos ) < rad_30 and dist <= self.pickup_range then
if target:is_player( ) then if target_obj:is_player( ) then
target:get_wielded_item( ):take_item( ) target_obj:get_wielded_item( ):take_item( )
target:set_wielded_item( "" ) target_obj:set_wielded_item( "" )
elseif target:get_entity_name( ) == "__builtin:item" then elseif target_obj:get_entity_name( ) == "__builtin:item" then
target:remove( ) target_obj:remove( )
end end
if can_eat then if can_eat then
-- minetest.sound_play( "hbhunger_eat_generic", { -- minetest.sound_play( "hbhunger_eat_generic", {

View File

@ -35,9 +35,19 @@ mobs.register_mob( "mobs:ghost", {
hunger_params = { offset = -0.1, spread = 3.0 }, hunger_params = { offset = -0.1, spread = 3.0 },
alertness_states = { alertness_states = {
ignore = { view_offset = 2, view_radius = 8, view_height = 8, view_acuity = 0 }, ignore = { view_offset = 2, view_radius = 8, view_height = 8, view_acuity = 0 },
attack = { view_offset = 2, view_radius = 14, view_height = 8, view_acuity = 3 }, search = { view_offset = 2, view_radius = 14, view_height = 8, view_acuity = 3, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "attack"
end },
attack = { view_offset = 2, view_radius = 14, view_height = 8, view_acuity = 3 , view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "attack"
end },
escape = { view_offset = 2, view_radius = 14, view_height = 8, view_acuity = 3 }, escape = { view_offset = 2, view_radius = 14, view_height = 8, view_acuity = 3 },
}, },
awareness_stages = {
attack = { decay = 15.0, abort_state = "ignore" },
escape = { decay = 10.0, abort_state = "ignore" },
},
certainty = 1.0, certainty = 1.0,
sensitivity = 0.2, sensitivity = 0.2,
@ -46,8 +56,9 @@ mobs.register_mob( "mobs:ghost", {
attack_type = "melee", attack_type = "melee",
standoff = 4.0, standoff = 4.0,
attack_range = 6.0, attack_range = 6.0,
search_range = 2.5,
escape_range = 2.5, escape_range = 2.5,
sneak_velocity = 0.2, sneak_velocity = 0.5,
walk_velocity = 0.5, walk_velocity = 0.5,
recoil_velocity = 1.5, recoil_velocity = 1.5,
run_velocity = 1.5, run_velocity = 1.5,
@ -126,9 +137,20 @@ mobs.register_mob( "mobs:spider", {
hunger_params = { offset = 0.3, spread = 4.0 }, hunger_params = { offset = 0.3, spread = 4.0 },
alertness_states = { alertness_states = {
ignore = { view_offset = 6, view_radius = 6, view_height = 6, view_acuity = 3 }, ignore = { view_offset = 6, view_radius = 6, view_height = 6, view_acuity = 3 },
attack = { view_offset = 6, view_radius = 12, view_height = 6, view_acuity = 5 }, search = { view_offset = 6, view_radius = 12, view_height = 6, view_acuity = 5, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "attack"
end },
attack = { view_offset = 6, view_radius = 12, view_height = 6, view_acuity = 5, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "attack"
end },
escape = { view_offset = 6, view_radius = 12, view_height = 6, view_acuity = 5 }, escape = { view_offset = 6, view_radius = 12, view_height = 6, view_acuity = 5 },
}, },
awareness_stages = {
search = { decay = 18.0, abort_state = "ignore" },
attack = { decay = 0.0, abort_state = "search" },
escape = { decay = 18.0, abort_state = "ignore" },
},
certainty = 1.0, certainty = 1.0,
sensitivity = 0.2, sensitivity = 0.2,
@ -136,7 +158,9 @@ mobs.register_mob( "mobs:spider", {
flee_factor = 8, flee_factor = 8,
attack_type = "melee", attack_type = "melee",
attack_range = 3.0, attack_range = 3.0,
search_range = 3.0,
escape_range = 3.0, escape_range = 3.0,
sneak_velocity = 0.5,
walk_velocity = 1.0, walk_velocity = 1.0,
stray_velocity = 1.0, stray_velocity = 1.0,
recoil_velocity = 0.5, recoil_velocity = 0.5,
@ -174,10 +198,7 @@ mobs.register_mob( "mobs:spider", {
damage_hand = "mobs_damage_hand", damage_hand = "mobs_damage_hand",
}, },
drops = { drops = {
{ name = "farming:blueberries", chance = 6, min = 1, max = 2 },
{ name = "farming:raspberries", chance = 6, min = 1, max = 2 },
{ name = "default:grass_1", chance = 8, min = 1, max = 2 }, { name = "default:grass_1", chance = 8, min = 1, max = 2 },
{ name = "default:shrub", chance = 8, min = 1, max = 2 },
}, },
on_rightclick = nil, on_rightclick = nil,
} ) } )
@ -220,9 +241,20 @@ mobs.register_mob( "mobs:bat", {
hunger_params = { offset = 1.0, spread = 1.0 }, hunger_params = { offset = 1.0, spread = 1.0 },
alertness_states = { alertness_states = {
ignore = { view_offset = 10, view_radius = 15, view_height = 15, view_acuity = 3 }, ignore = { view_offset = 10, view_radius = 15, view_height = 15, view_acuity = 3 },
attack = { view_offset = 10, view_radius = 20, view_height = 15, view_acuity = 5 }, search = { view_offset = 10, view_radius = 20, view_height = 15, view_acuity = 5, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "attack"
end },
attack = { view_offset = 10, view_radius = 20, view_height = 15, view_acuity = 5, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "attack"
end },
escape = { view_offset = 10, view_radius = 20, view_height = 15, view_acuity = 3 }, escape = { view_offset = 10, view_radius = 20, view_height = 15, view_acuity = 3 },
}, },
awareness_stages = {
search = { decay = 14.0, abort_state = "ignore" },
attack = { decay = 0.0, abort_state = "search" },
escape = { decay = 14.0, abort_state = "ignore" },
},
certainty = 1.0, certainty = 1.0,
sensitivity = 0.3, sensitivity = 0.3,
@ -230,6 +262,7 @@ mobs.register_mob( "mobs:bat", {
flee_factor = 6, flee_factor = 6,
attack_type = "melee", attack_type = "melee",
attack_range = 3.0, attack_range = 3.0,
search_range = 5.0,
escape_range = 5.0, escape_range = 5.0,
sneak_velocity = 1.0, sneak_velocity = 1.0,
walk_velocity = 1.0, walk_velocity = 1.0,
@ -271,7 +304,6 @@ mobs.register_mob( "mobs:bat", {
}, },
drops = { drops = {
{ name = "default:apple", chance = 4, min = 1, max = 2 }, { name = "default:apple", chance = 4, min = 1, max = 2 },
{ name = "default:orange", chance = 4, min = 1, max = 2 },
}, },
} ) } )
@ -313,9 +345,20 @@ mobs.register_mob( "mobs:griefer_ghost", {
hunger_params = { offset = 0.3, spread = 6.0 }, hunger_params = { offset = 0.3, spread = 6.0 },
alertness_states = { alertness_states = {
ignore = { view_offset = 5, view_radius = 10, view_height = 8, view_acuity = 2 }, ignore = { view_offset = 5, view_radius = 10, view_height = 8, view_acuity = 2 },
attack = { view_offset = 5, view_radius = 20, view_height = 8, view_acuity = 2 }, search = { view_offset = 5, view_radius = 20, view_height = 8, view_acuity = 2, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "attack"
end },
attack = { view_offset = 5, view_radius = 20, view_height = 8, view_acuity = 2, view_filter = function ( self, obj, clarity )
return clarity == 0.0 and "search" or "attack"
end },
escape = { view_offset = 5, view_radius = 20, view_height = 8, view_acuity = 2 }, escape = { view_offset = 5, view_radius = 20, view_height = 8, view_acuity = 2 },
}, },
awareness_stages = {
search = { decay = 8.0, abort_state = "ignore" },
attack = { decay = 25.0, abort_state = "search" },
escape = { decay = 8.0, abort_state = "ignore" },
},
certainty = 1.0, certainty = 1.0,
sensitivity = 0.6, sensitivity = 0.6,
@ -323,7 +366,9 @@ mobs.register_mob( "mobs:griefer_ghost", {
flee_factor = 10, flee_factor = 10,
attack_type = "melee", attack_type = "melee",
attack_range = 3.0, attack_range = 3.0,
search_range = 4.0,
escape_range = 4.0, escape_range = 4.0,
sneak_velocity = 0.5,
walk_velocity = 1.0, walk_velocity = 1.0,
stray_velocity = 1.0, stray_velocity = 1.0,
recoil_velocity = 0.5, recoil_velocity = 0.5,
@ -360,9 +405,6 @@ mobs.register_mob( "mobs:griefer_ghost", {
}, },
drops = { drops = {
{ name = "default:papyrus", chance = 6, min = 1, max = 2 }, { name = "default:papyrus", chance = 6, min = 1, max = 2 },
{ name = "default:cactus", chance = 6, min = 1, max = 2 },
{ name = "farming:pumpkin_slice", chance = 8, min = 1, max = 2 },
{ name = "farming:melon_slice", chance = 8, min = 1, max = 2 },
}, },
} ) } )