diff --git a/README.txt b/README.txt index a78bc71..9ed9c01 100644 --- a/README.txt +++ b/README.txt @@ -34,10 +34,10 @@ Here are some of the other highlights of the Mobs Lite engine: changing their course. * Mobs can be randomly spawned in the vicinity of players, thus relieving the overhead of - ABM-based spawners. + costly ABM-based spawners. - * Specialized Timekeeper class ensures efficient dispatching of mob-related callbacks for - each globalstep cycle. + * Timekeeper helper class ensures efficient dispatching of mob-related callbacks for the + appropriate server cycle. * And of course, much much more! diff --git a/animals.lua b/animals.lua index e3782d5..834ee25 100644 --- a/animals.lua +++ b/animals.lua @@ -56,6 +56,10 @@ mobs.register_mob( "mobs:kitten", { ["mobs:meat_raw"] = "follow", ["mobs:meat"] = "follow", }, + watch_spawnitems = { + ["mobs:meat_raw"] = "follow", + ["mobs:meat"] = "follow", + }, watch_players = { }, hunger = 5, @@ -149,6 +153,10 @@ mobs.register_mob( "mobs:rat", { ["mobs:meat_raw"] = "follow", ["mobs:meat"] = "follow", }, + watch_spawnitems = { + ["mobs:meat_raw"] = "follow", + ["mobs:meat"] = "follow", + }, watch_players = { }, hp_max = 2, @@ -229,6 +237,7 @@ mobs.register_mob( "mobs:hare", { ["default:apple"] = "follow", ["default:orange"] = "follow", }, + watch_spawnitems = { }, watch_players = { }, hp_max = 4, @@ -320,6 +329,7 @@ mobs.register_mob( "mobs:chicken", { ["default:apple"] = "follow", ["default:orange"] = "follow", }, + watch_spawnitems = { }, watch_players = { }, hunger = 10, diff --git a/init.lua b/init.lua index ed0d182..d86d59d 100644 --- a/init.lua +++ b/init.lua @@ -9,8 +9,11 @@ mobs = { } -local source_list = { } -local connected_players = { } +local registry = { + players = { }, + avatars = { }, + spawnitems = { }, +} world_gravity = -10 liquid_density = 0.5 @@ -124,8 +127,8 @@ local function node_locator( pos, size, time, color ) if is_debug then minetest.add_particle( { pos = pos, - velocity = { x=0, y=0, z=0 }, - acceleration = { x=0, y=0, z=0 }, + vel = { x=0, y=0, z=0 }, + acc = { x=0, y=0, z=0 }, exptime = time + 4, size = size, collisiondetection = false, @@ -201,25 +204,49 @@ end -------------------- -minetest.register_on_joinplayer( function( player ) - local name = player:get_player_name( ) - - connected_players[ name ] = player -- maintain our own lookup table for efficiency -end ) - minetest.register_on_leaveplayer( function( player, is_timeout ) local name = player:get_player_name( ) -- delete all target references (if applicable) - for id, this in pairs( source_list ) do + for id, this in pairs( registry.avatars ) do if this.target == player then this:reset_target( ) end end - - connected_players[ name ] = nil + registry.players[ name ] = nil end ) +minetest.register_on_joinplayer( function( player ) + local name = player:get_player_name( ) + registry.players[ name ] = player +end ) + +-------------------- + +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 ) + for id, this in pairs( registry.avatars ) do + if this.target == self.object then + this:reset_target( ) + end + end + registry.spawnitems[ id ] = nil +end + -------------------- local globaltimer = Timekeeper( { } ) @@ -254,6 +281,7 @@ mobs.register_mob = function ( name, def ) attack_range = def.attack_range, escape_range = def.escape_range, follow_range = def.follow_range, + pickup_range = def.pickup_range, sneak_velocity = def.sneak_velocity, walk_velocity = def.walk_velocity, run_velocity = def.run_velocity, @@ -276,6 +304,7 @@ mobs.register_mob = function ( name, def ) 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, sounds = def.sounds, animation = def.animation, @@ -389,24 +418,24 @@ mobs.register_mob = function ( name, def ) -- sensory processing -- check_suspect = function ( self, target, elapsed ) - local player_name = target:get_player_name( ) - local wielditem_name = target:get_wielded_item( ):get_name( ) - local suspect = self.watch_players[ player_name ] or self.watch_wielditems[ wielditem_name ] + local entity = target:get_luaentity( ) + local suspect + + 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 ] + elseif entity.name == "__builtin:item" then + suspect = self.watch_spawnitems[ entity.item_name ] + end if type( suspect ) == "function" then - return suspect( self, player_name, wielditem_name, elapsed or 0.0 ) + return suspect( self, target, elapsed or 0.0 ) else return suspect end end, - 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, - is_paranoid = function ( self, target_pos ) local length = get_vector_length( self.pos, target_pos ) local this = self.alertness @@ -436,6 +465,48 @@ mobs.register_mob = function ( name, def ) end end, + -- 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, + + iterate_registry = function ( self, radius, height, classes ) + local length = radius * radius + local class_idx = 1 + local key + + local function is_inside_area( obj ) + -- perform fast length-squared distance check + local target_pos = obj:get_pos( ) + local a = self.pos.x - target_pos.x + local b = self.pos.z - target_pos.z + + return a * a + b * b < length + end + + return function ( ) + while classes[ class_idx ] do + local obj + + key, obj = next( classes[ class_idx ], key ) + if obj then + if obj:get_hp( ) > 0 and is_inside_area( obj ) then + return obj + end + else + class_idx = class_idx + 1 + key = nil + end + end + + return nil + end + end, + fire_weapon = function ( self, target_pos ) local params = self.weapon_params @@ -723,10 +794,6 @@ mobs.register_mob = function ( name, def ) if self.speed > 0 then self:set_speed( 0 ) self:set_animation( "stand" ) - --- if self.on_wielditem( self, self.target ) --- if self.target:get_entity_name( ) then --- end end else if self.speed == 0 then @@ -961,25 +1028,25 @@ mobs.register_mob = function ( name, def ) end -- when not upset, seek out food or prey at random intervals - for _, player in pairs( connected_players ) do - local player_pos = player:getpos( ) + for obj in self:iterate_registry( 30, 30, { registry.players, registry.spawnitems } ) do + local target_pos = obj:get_pos( ) - if not player:get_attach( ) and self:is_paranoid( player_pos ) then - if random( 10 ) <= self.fear_factor and player:get_hp( ) > 0 then + if self:is_paranoid( target_pos ) then + if random( 10 ) <= self.fear_factor and obj:is_player( ) and not obj:get_attach( ) then if self.type == "monster" then - self:set_attack_state( player ) + self:set_attack_state( obj ) return else - self:set_escape_state( player ) + self:set_escape_state( obj ) return end elseif self.type == "animal" then - local state = self:check_suspect( player ) + local state = self:check_suspect( obj ) if state == "escape" then - self:set_escape_state( player ) + self:set_escape_state( obj ) return elseif state == "follow" then - self:set_follow_state( player ) + self:set_follow_state( obj ) return end end @@ -991,7 +1058,7 @@ mobs.register_mob = function ( name, def ) on_step = function( self, dtime, pos, rot, new_vel, old_vel, move_result ) self.pos = pos - self.yaw = rot.Y + self.yaw = rot.y self.new_vel = new_vel self.move_result = move_result @@ -1037,7 +1104,7 @@ mobs.register_mob = function ( name, def ) end, on_activate = function ( self, staticdata, dtime_s, id ) - source_list[ id ] = self + registry.avatars[ id ] = self self.object:set_armor_groups( { fleshy = self.armor } ) self:set_acceleration_vert( self.gravity ) @@ -1071,7 +1138,7 @@ mobs.register_mob = function ( name, def ) end, on_deactivate = function ( self, id ) - source_list[ id ] = nil + registry.avatars[ id ] = nil end, get_staticdata = function ( self ) @@ -1179,8 +1246,8 @@ mobs.register_spawn_near = function ( name, def ) globaltimer.shift( 0.5 ) globaltimer.start( 10, name, function ( ) - for player_name, player in pairs( connected_players ) do - local player_pos = player:getpos( ) + for player_name, player in pairs( registry.players ) do + local player_pos = player:get_pos( ) if random( chance ) == 1 and is_area_safe == safe_area:containsp( player_pos ) then local positions = minetest.find_nodes_in_area_under_air( @@ -1330,5 +1397,5 @@ dofile( minetest.get_modpath( "mobs" ) .. "/weapons.lua" ) -- compatibility for Minetest S3 engine if not vector.origin then - dofile( minetest.get_modpath( "mobs" ) .. "/compatibility.lua" ) + dofile( minetest.get_modpath( "mobs" ) .. "/compatibility.lua" ) end