axon/init.lua

333 lines
8.9 KiB
Lua

--------------------------------------------------------
-- Minetest :: Axon Event Propagation Mod (axon)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2019-2020, Leslie E. Krause
--
-- ./games/minetest_game/mods/axon/init.lua
--------------------------------------------------------
--local S1,S1_ = Stopwatch( "axon" )
axon = { }
local sources = { }
local stack_size = 0
--------------------
local random = math.random
local pow = math.pow
local ceil = math.ceil
local min = math.min
local function check_limits( v, min_v, max_v )
return v >= min_v and v <= max_v
end
local function clamp( val, min, max )
return val < min and min or val > max and max or val
end
local function get_signal_strength( max_value, cur_value, slope )
return pow( cur_value / max_value, 1 - clamp( slope, 0, 1 ) )
end
local function punch_object( obj, groups, pos )
stack_size = stack_size + 1
assert( stack_size < 10, "punch_object( ): Aborting callback relay due to possible recursion" )
obj:punch( obj, 1.0, {
full_punch_interval = 1.0,
damage_groups = groups
}, pos and vector.direction( obj:get_pos( ), pos ) or nil )
stack_size = stack_size - 1
end
--------------------
local function ContactStimulus( node_name, group, intensity, chance, period, power )
local self = { }
self.group = group
self.propagator = "contact"
self.period = period
self.on_action = function ( source_pos, target_obj )
if random( chance ) == 1 then
local touch_counts = minetest.count_nodes_in_area(
vector.add( source_pos, -0.5 ), vector.add( source_pos, 0.5 ), { node_name }, true )
local count = touch_counts[ node_name ]
if count > 0 then
local damage = intensity * pow( count, power )
target_obj:punch( target_obj, period, {
full_punch_interval = period,
damage_groups = { [group] = damage },
}, nil )
end
end
end
return self
end
local function ImmersionStimulus( node_name, group, intensity, chance, period )
local self = { }
self.group = group
self.method = "immersion"
self.period = period
self.on_action = function ( source_pos, target_obj )
if random( chance ) == 1 then
local node = minetest.get_node( vector.round( source_pos ) )
local node_group = string.match( node_name, "^group:(.+)" )
if node.name == node_name or node_group and minetest.get_item_group( node.name, node_group ) > 0 then
target_obj:punch( target_obj, period, {
full_punch_interval = period,
damage_groups = { [group] = intensity },
}, nil )
end
end
end
return self
end
local function RadiationStimulus( node_name, group, intensity, chance, period, radius, power )
local self = { }
self.group = group
self.propagator = "radiation"
self.period = period
self.on_action = function ( source_pos, target_obj )
if random( chance ) == 1 then
local touch_counts = minetest.count_nodes_in_area(
vector.add( source_pos, -radius ), vector.add( source_pos, radius ), { node_name }, true )
local count = touch_counts[ node_name ]
if count > 0 then
local damage = intensity * pow( count, power )
target_obj:punch( target_obj, period, {
full_punch_interval = period,
damage_groups = { [group] = damage },
}, nil )
end
end
end
return self
end
local function EmissionStimulus( node_name, group, intensity, chance, period, radius, slope )
local self = { }
self.group = group
self.propagator = "radiation"
self.period = period
self.on_action = function ( source_pos, target_obj )
if random( chance ) == 1 then
-- get nearest node position in a radius, including the center
local node_pos = minetest.find_node_near( source_pos, radius, { node_name }, true )
if node_pos then
local length = min( radius, vector.distance( node_pos, source_pos ) )
local damage = ceil( intensity * get_signal_strength( radius, radius - length, slope ) )
target_obj:punch( target_obj, period, {
full_punch_interval = period,
damage_groups = { [group] = damage },
}, nil )
end
end
end
return self
end
local function AxonPropagator( obj )
local event_defs = { }
local clock = 0.0
local delay = 0.0
local self = { }
self.start = function ( stimulus )
table.insert( event_defs, {
group = stimulus.group,
period = stimulus.period,
expiry = clock + stimulus.period,
started = clock,
on_action = stimulus.on_action
} )
end
self.on_step = function ( dtime, pos )
clock = clock + dtime
for i, v in ipairs( event_defs ) do
if clock >= v.expiry and clock > v.started then
v.expiry = clock + v.period
v.on_action( pos, obj )
end
end
end
return self
end
function AxonObject( self, armor_groups )
local old_on_step = self.on_step
local old_on_punch = self.on_punch
local propagator = AxonPropagator( self.object )
if not self.receptrons then
self.receptrons = { }
end
-- initialize armor groups
if not armor_groups then
armor_groups = { }
end
for k, v in pairs( self.receptrons ) do
armor_groups[ k ] = v.sensitivity or 0
end
self.object:set_armor_groups( armor_groups )
-- start stimulus propagators
for k, v in pairs( sources ) do
for idx, stimulus in ipairs( v ) do
-- ignore stimulii without a receptron
if self.receptrons[ stimulus.group ] then
propagator.start( stimulus )
end
end
end
self.on_step = function ( self, dtime, pos, ... )
--S1()
propagator.on_step( dtime, pos )
--S1_()
old_on_step( self, dtime, pos, ... )
end
self.generate_direct_stimulus = function ( self, obj, groups )
if obj ~= self.object then
punch_object( obj, groups )
end
end
self.generate_radial_stimulus = function ( self, radius, speed, slope, groups, classes )
local pos = self.object:get_pos( )
for obj in mobs.iterate_registry( pos, radius, radius, classes ) do
local length = vector.distance( pos, obj:get_pos( ) )
if obj ~= self.object and length <= radius then
local damage_groups = { }
for k, v in pairs( groups ) do
damage_groups[ k ] = v * get_signal_strength( radius, radius - length, slope )
end
if speed > 0 then
minetest.after( length / speed, function ( )
punch_object( obj, damage_groups )
end )
else
punch_object( obj, damage_groups )
end
end
end
end
self.on_punch = function ( self, puncher, time_from_last_punch, tool_capabilities, direction, damage )
if puncher == self.object then
-- filter and relay the incoming stimulus
for k, v in pairs( tool_capabilities.damage_groups ) do
local receptron = self.receptrons[ k ]
if receptron and check_limits( v, receptron.min_intensity or 1, receptron.max_intensity or 65536 ) then
-- if receptron returns false, then stimulus was handled
if not receptron.on_reaction( self, v, direction ) then return true end
end
end
end
-- if stimulus wasn't handled by a receptron, fallback to on_punch method
return old_on_punch( self, puncher, time_from_last_punch, tool_capabilities, direction, damage )
end
end
--------------------
axon.register_source = function ( node_name, stimulus_list )
if not sources[ node_name ] then
sources[ node_name ] = { }
end
for i, v in pairs( stimulus_list ) do
local stimulus
if v.propagator == "contact" then
stimulus = ContactStimulus( node_name, v.group, v.intensity, v.chance, v.period, v.power )
elseif v.propagator == "immersion" then
stimulus = ImmersionStimulus( node_name, v.group, v.intensity, v.chance, v.period )
elseif v.propagator == "radiation" then
stimulus = RadiationStimulus( node_name, v.group, v.intensity, v.chance, v.period, v.radius, v.power )
elseif v.propagator == "emission" then
stimulus = EmissionStimulus( node_name, v.group, v.intensity, v.chance, v.period, v.radius, v.slope )
else
error( "axon.register_source(): Unknown propagator specified in stimulus property table" )
end
table.insert( sources[ node_name ], stimulus )
end
end
axon.register_source_group = function ( node_group, node_names )
for i, v in ipairs( node_names ) do
local groups = minetest.registered_nodes[ v ].groups
groups[ node_group ] = 1
minetest.override_item( v, { groups = groups } )
end
end
axon.generate_direct_stimulus = function ( obj, groups )
punch_object( obj, groups )
end
axon.generate_radial_stimulus = function ( pos, radius, speed, slope, groups, classes )
for obj in mobs.iterate_registry( pos, radius, radius, classes ) do
local length = vector.distance( pos, obj:get_pos( ) )
if length <= radius then
local damage_groups = { }
for k, v in pairs( groups ) do
damage_groups[ k ] = ceil( v * get_signal_strength( radius, radius - length, slope ) )
end
if speed > 0 then
minetest.after( length / speed, function ( )
punch_object( obj, damage_groups, pos )
end )
else
punch_object( obj, damage_groups, pos )
end
end
end
end
--------------------
dofile( minetest.get_modpath( "axon" ) .. "/sources.lua" )
-- compatibility for Minetest S3 engine
if not minetest.count_nodes_in_area then
dofile( minetest.get_modpath( "axon" ) .. "/compatibility.lua" )
end