diff --git a/README.txt b/README.txt index 8039f5f..44ebc91 100644 --- a/README.txt +++ b/README.txt @@ -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 returns to the field of view. - * Creatures avoid running into most obstacles by analyzing the surroundings and steadily - adjusting their course. + * Animals can be programmed to follow players that are wielding food and to eat directly + from the player's hand. - * Mobs can be randomly spawned in the vicinity of players, thus relieving the overhead of - costly ABM-based spawners. + * Seamless integration with Axon allows Mobs to react to various environmental stimulii + like smells, sounds, etc. * Timekeeper helper class ensures efficient dispatching of mob-related callbacks at the 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 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! 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) https://github.com/minetest-game-mods/default +Axon Mod (optional) + https://bitbucket.org/sorcerykid/axon + Installation ---------------------- diff --git a/animals.lua b/animals.lua index 3f190b0..4b1fc16 100644 --- a/animals.lua +++ b/animals.lua @@ -34,21 +34,32 @@ mobs.register_mob( "mobs:kitten", { hunger_params = { offset = -0.5, spread = 2.5 }, alertness_states = { 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 }, 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, sensitivity = 0.6, - fear_factor = 0, + fear_factor = 6, flee_factor = 10, + sneak_velocity = 0.8, walk_velocity = 0.8, stray_velocity = 1.0, recoil_velocity = 1.0, run_velocity = 1.2, - escape_range = 3.0, + search_range = 3.0, follow_range = 2.0, pickup_range = 2.0, + escape_range = 3.0, can_jump = false, can_walk = true, enable_fall_damage = true, @@ -129,21 +140,32 @@ mobs.register_mob( "mobs:rat", { hunger_params = { offset = 0.0, spread = 2.5 }, alertness_states = { 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 }, 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, sensitivity = 0.0, fear_factor = 8, flee_factor = 10, + sneak_velocity = 0.5, walk_velocity = 0.5, stray_velocity = 0.5, recoil_velocity = 0.5, run_velocity = 1.2, - escape_range = 0.0, + search_range = 2.0, follow_range = 2.0, pickup_range = 2.0, + escape_range = 0.0, can_jump = false, can_walk = true, enable_fall_damage = true, @@ -212,21 +234,32 @@ mobs.register_mob( "mobs:hare", { hunger_params = { offset = 0.0, spread = 2.0 }, alertness_states = { 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 }, 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, sensitivity = 0.5, fear_factor = 6, flee_factor = 10, + sneak_velocity = 2.0, walk_velocity = 3.0, stray_velocity = 1.0, recoil_velocity = 2.0, run_velocity = 3.5, - escape_range = 3.0, + search_range = 3.0, follow_range = 3.0, pickup_range = 2.0, + escape_range = 3.0, can_jump = true, can_walk = true, enable_fall_damage = true, @@ -306,18 +339,29 @@ mobs.register_mob( "mobs:chicken", { hunger_params = { offset = 0.0, spread = 2.0 }, alertness_states = { 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 }, 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, sensitivity = 0.3, fear_factor = 2, flee_factor = 10, + sneak_velocity = 1.5, walk_velocity = 1.5, stray_velocity = 1.0, recoil_velocity = 2.0, run_velocity = 2.0, + search_range = 3.0, follow_range = 3.0, pickup_range = 2.0, escape_range = 3.0, diff --git a/init.lua b/init.lua index b725f7f..d8ab6f0 100644 --- a/init.lua +++ b/init.lua @@ -20,6 +20,8 @@ local world_gravity = -10 local liquid_density = 0.5 local liquid_viscosity = 0.6 +local next_noise_id = 1 + -------------------- local random = math.random @@ -66,7 +68,7 @@ function Timekeeper( this ) end 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 } end end @@ -256,8 +258,9 @@ minetest.register_on_leaveplayer( function( player, is_timeout ) -- delete all target references (if applicable) for id, obj in pairs( registry.avatars ) do - if obj:get_luaentity( ).target == player then - this:reset_target( ) + local this = obj:get_luaentity( ) + if this.target and this.target.obj == player then + this:reset_alertness( "ignore" ) end end registry.players[ name ] = nil @@ -287,8 +290,9 @@ end builtin_item.on_deactivate = function ( self, id ) for id, obj in pairs( registry.avatars ) do - if obj:get_luaentity( ).target == self.object then - this:reset_target( ) + local this = obj:get_luaentity( ) + if this.target and this.target.obj == self.object then + this:reset_alertness( "ignore" ) end end registry.spawnitems[ id ] = nil @@ -324,11 +328,13 @@ mobs.register_mob = function ( name, def ) receptrons = def.receptrons, death_message = def.death_message, alertness_states = def.alertness_states, + awareness_stages = def.awareness_stages, 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, + search_range = def.search_range, pickup_range = def.pickup_range, sneak_velocity = def.sneak_velocity, walk_velocity = def.walk_velocity, @@ -351,9 +357,9 @@ mobs.register_mob = function ( name, def ) shoot_period = def.shoot_period, shoot_chance = def.shoot_chance, weapon_params = def.weapon_params, - watch_wielditems = def.watch_wielditems, - watch_spawnitems = def.watch_spawnitems, - watch_players = def.watch_players, + watch_wielditems = def.watch_wielditems or { }, + watch_spawnitems = def.watch_spawnitems or { }, + watch_players = def.watch_players or { }, sounds = def.sounds, animation = def.animation, fear_factor = def.fear_factor, @@ -365,11 +371,13 @@ mobs.register_mob = function ( name, def ) enable_swimming = true, shoot_count = 0, timeout = def.timeout, + neutral_state = "ignore", + defense_state = def.type == "monster" and "attack" or "ignore", is_tamed = false, description = def.description, after_activate = def.after_activate, before_deactivate = def.before_deactivate, - after_state_change = def.after_state_change, + before_state_change = def.before_state_change, before_punch = def.before_punch, -- 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 ) local yaw = self:get_direct_yaw( pos ) 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, - get_target_yaw_delta = function ( self, pos ) - local yaw = self:get_direct_yaw( pos ) + 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 ) + end, + + get_target_yaw_delta = function ( self ) + local yaw = self:get_direct_yaw( self.target.pos ) return abs( normalize_angle( yaw - self.yaw ) ) end, @@ -482,7 +490,7 @@ mobs.register_mob = function ( name, def ) end, play_sound = function ( self, name ) - minetest.sound_play( name, { object = self.object } ) + minetest.sound_play( name, { object = self.object, loop = false } ) end, play_sound_repeat = function ( self, name ) @@ -504,31 +512,34 @@ mobs.register_mob = function ( name, def ) -- sensory processing -- - check_suspect = function ( self, target, elapsed ) - local entity = target:get_luaentity( ) - local suspect + check_suspect = function ( self, target_obj, clarity, elapsed ) + -- default to defense or neutral state if not in any watch list + 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 - local player_name = target:get_player_name( ) - local item_name = target:get_wielded_item( ):get_name( ) - suspect = self.watch_players[ player_name ] or self.watch_wielditems[ item_name ] + 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 elseif entity.name == "__builtin:item" then - suspect = self.watch_spawnitems[ entity.item_name ] + suspect = self.watch_spawnitems[ entity.item_name ] or suspect end 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 return suspect end end, - is_paranoid = function ( self, target_pos ) + get_visibility = function ( self, target_pos ) local length = get_vector_length( self.pos, target_pos ) + local height = get_vector_height( self.pos, target_pos ) local this = self.alertness - if not this or length > 48 then - return false + if not this or length > 48 or height > 48 then -- immediately eliminate far objects + return 0.0, false, false + else local view_range = this.view_radius + this.view_offset 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 height = get_vector_height( 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" ) - --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 - clarity > self.sensitivity and clarity * self.certainty > random( ) + local is_visible = radius <= this.view_radius and height <= this.view_height + local is_evident = clarity > self.sensitivity and clarity * self.certainty > random( ) + + return clarity, is_visible, is_evident end end, - -- state-change handlers -- + -- target functions -- - reset_target = function ( self ) - if self.target then - self:set_ignore_state( ) + 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 + 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, - set_ignore_state = function ( self ) - if self.state == "ignore" then return end + 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 ] - self.state = "ignore" - self.target = nil - self.alertness = self.alertness_states.ignore - self.is_tamed = false + -- 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 - if self.custom.after_state_change then - self.custom.after_state_change( self ) + if alertness and alertness.view_filter then + 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 - self:start_ignore_action( ) + return self.state end, - set_escape_state = function ( self, target ) - if self.state == "escape" then return end - self.state = "escape" + 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_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.alertness = self.alertness_states.escape - self.is_tamed = false + self.alertness = self.alertness_states[ state ] - if random( 2 ) == 1 then - -- this is a bad player, so keep watch - 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( ) + self:start_awareness( ) + self.action_funcs[ state ]( self ) end, -- action hooks -- start_ignore_action = function ( self ) + self.target = nil -- forget any target + if not self.move_result.is_standing then -- always stand when in mid-air self:set_speed( 0 ) @@ -637,7 +678,7 @@ mobs.register_mob = function ( name, def ) self:set_animation( "stand" ) 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 ) end, @@ -727,6 +768,8 @@ mobs.register_mob = function ( name, def ) end, start_follow_action = function ( self ) + assert( self.target.pos ) -- sanity check + self:turn_to( self:get_target_yaw( rad_20 ), 10 ) self:set_speed( self.recoil_velocity ) self:set_animation( "walk" ) @@ -736,13 +779,10 @@ mobs.register_mob = function ( name, def ) end, on_follow_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 - local next_state = validate_target( ) - if next_state ~= self.state then - self:reset_state( next_state, self.target ) + 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 @@ -753,6 +793,9 @@ mobs.register_mob = function ( name, def ) end end + local target_pos = self.target.pos + local dist = vector.distance( self.pos, target_pos ) + if dist <= self.follow_range then if self.speed > 0 then self:set_speed( 0 ) @@ -786,10 +829,71 @@ mobs.register_mob = function ( name, def ) end end, - start_escape_action = function ( self ) - local target_pos = vector.offset_y( self.target:get_pos( ) ) - local dist = vector.distance( self.pos, self.target.pos ) + start_search_action = function ( self ) + assert( self.target.pos ) -- sanity check + 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 -- recoil if facing intruder self:set_speed( -self.recoil_velocity ) @@ -804,6 +908,11 @@ mobs.register_mob = function ( name, def ) self:set_animation( "walk" ) 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 minetest.sound_play( self.sounds.escape, { object = self.object } ) end @@ -813,16 +922,17 @@ mobs.register_mob = function ( name, def ) end, 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 not self:is_paranoid( elapsed ) or random( 10 ) > self.flee_factor then - self:lower_awareness( ) + 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 ) return end end + local target_pos = self.target.pos + local dist = vector.distance( self.pos, target_pos ) + if dist <= self.escape_range then -- if close, keep backtracking if cycles % 2 == 0 then @@ -867,6 +977,8 @@ mobs.register_mob = function ( name, def ) end, start_attack_action = function ( self ) + assert( self.target.pos and self.target.obj ) -- sanity check + self:set_speed( self.run_velocity ) self:set_animation( "run" ) self:turn_to( self:get_target_yaw( rad_90 ), 10 ) @@ -880,23 +992,24 @@ mobs.register_mob = function ( name, def ) end, on_attack_action = function ( self, cycles, period, elapsed ) - if self.target:get_hp( ) == 0 or self.target:get_attach( ) then - self:set_ignore_state( ) + if self.target.obj:get_hp( ) == 0 or self.target.obj:get_attach( ) then + self:reset_alertness( "ignore" ) return end if cycles % 5 == 0 then -- validate target once per second - if not self:is_paranoid( elapsed ) then - self:lower_awareness( ) + local goal_state = self:verify_target( ) + if goal_state ~= self.state then + self:reset_alertness( goal_state, self.target ) return end end - self:turn_to( self:get_target_yaw( rad_0 ), 5 ) - - local target_pos = vector.offset_y( self.target.pos, 0.5 ) + local target_pos = self.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 self.attack_type == "shoot" and self.fire_weapon 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 } ) end -- 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, damage_groups = { fleshy=self.damage } }, vector.direction( self.pos, target_pos ) ) @@ -1027,7 +1140,7 @@ mobs.register_mob = function ( name, def ) self.cur_animation = type end, - -- damage and hunger routines -- + -- damage handler -- handle_damage = function ( self ) -- handle environmental damage (light, water, and lava) @@ -1066,24 +1179,6 @@ mobs.register_mob = function ( name, def ) 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 -- 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.pos = self.object:get_pos( ) - if self.type == "monster" then - self.on_create_target = function ( obj, clarity ) - return clarity < 0.5 and "search" or "attack" - end - else - self:set_escape_state( obj ) - return - end - - self.reset_funcs = { - ignore = self.set_ignore_state, - remark = self.set_remark_state, - search = self.set_search_state, + 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, } if staticdata then @@ -1191,7 +1280,7 @@ mobs.register_mob = function ( name, def ) self.after_activate( self, id ) end - self:set_ignore_state( ) + self:reset_alertness( self.neutral_state ) end, on_deactivate = function ( self, id ) @@ -1238,9 +1327,9 @@ mobs.register_mob = function ( name, def ) end 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 - self:set_attack_state( hitter ) + self:reset_alertness( "attack", { obj = hitter, pos = hitter:get_pos( ) } ) end end end, @@ -1373,18 +1462,18 @@ end -------------------- -mobs.play_sound = function ( obj, name ) - minetest.sound_play( name, { loop = false }, true ) +mobs.play_sound = function ( pos, name ) + minetest.sound_play( name, { loop = false }, true ) end 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 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 - 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 ) next_noise_id = next_noise_id + 1 end @@ -1412,11 +1501,11 @@ mobs.presets = { 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 target:is_player( ) then - target:get_wielded_item( ):take_item( ) - target:set_wielded_item( "" ) - elseif target:get_entity_name( ) == "__builtin:item" then - target:remove( ) + 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( ) end if can_eat then -- minetest.sound_play( "hbhunger_eat_generic", { diff --git a/monsters.lua b/monsters.lua index b610a9d..53c50bb 100644 --- a/monsters.lua +++ b/monsters.lua @@ -35,9 +35,19 @@ mobs.register_mob( "mobs:ghost", { hunger_params = { offset = -0.1, spread = 3.0 }, alertness_states = { 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 }, }, + awareness_stages = { + attack = { decay = 15.0, abort_state = "ignore" }, + escape = { decay = 10.0, abort_state = "ignore" }, + }, + certainty = 1.0, sensitivity = 0.2, @@ -46,8 +56,9 @@ mobs.register_mob( "mobs:ghost", { attack_type = "melee", standoff = 4.0, attack_range = 6.0, + search_range = 2.5, escape_range = 2.5, - sneak_velocity = 0.2, + sneak_velocity = 0.5, walk_velocity = 0.5, recoil_velocity = 1.5, run_velocity = 1.5, @@ -126,9 +137,20 @@ mobs.register_mob( "mobs:spider", { hunger_params = { offset = 0.3, spread = 4.0 }, alertness_states = { 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 }, }, + 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, sensitivity = 0.2, @@ -136,7 +158,9 @@ mobs.register_mob( "mobs:spider", { flee_factor = 8, attack_type = "melee", attack_range = 3.0, + search_range = 3.0, escape_range = 3.0, + sneak_velocity = 0.5, walk_velocity = 1.0, stray_velocity = 1.0, recoil_velocity = 0.5, @@ -174,10 +198,7 @@ mobs.register_mob( "mobs:spider", { damage_hand = "mobs_damage_hand", }, 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:shrub", chance = 8, min = 1, max = 2 }, }, on_rightclick = nil, } ) @@ -220,9 +241,20 @@ mobs.register_mob( "mobs:bat", { hunger_params = { offset = 1.0, spread = 1.0 }, alertness_states = { 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 }, }, + 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, sensitivity = 0.3, @@ -230,6 +262,7 @@ mobs.register_mob( "mobs:bat", { flee_factor = 6, attack_type = "melee", attack_range = 3.0, + search_range = 5.0, escape_range = 5.0, sneak_velocity = 1.0, walk_velocity = 1.0, @@ -271,7 +304,6 @@ mobs.register_mob( "mobs:bat", { }, drops = { { 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 }, alertness_states = { 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 }, }, + 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, sensitivity = 0.6, @@ -323,7 +366,9 @@ mobs.register_mob( "mobs:griefer_ghost", { flee_factor = 10, attack_type = "melee", attack_range = 3.0, + search_range = 4.0, escape_range = 4.0, + sneak_velocity = 0.5, walk_velocity = 1.0, stray_velocity = 1.0, recoil_velocity = 0.5, @@ -360,9 +405,6 @@ mobs.register_mob( "mobs:griefer_ghost", { }, drops = { { 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 }, }, } )