2024-09-20 14:00:49 +02:00
-- Possible future improvements:
-- * rewrite to use node timers instead of ABMs, but needs benchmarking
-- * redesign the catch-up logic
-- * switch to exponentially-weighted moving average for light instead using a single variable to conserve IO
--
2021-05-29 16:12:33 +02:00
local math = math
2024-09-20 14:00:49 +02:00
local vector = vector
2021-05-29 16:12:33 +02:00
2021-03-12 00:10:50 +01:00
local plant_lists = { }
2024-09-20 14:00:49 +02:00
mcl_farming.plant_lists = plant_lists -- export
2020-09-05 19:49:12 +02:00
local plant_nodename_to_id_list = { }
2024-09-20 14:00:49 +02:00
local time_speed = tonumber ( minetest.settings : get ( " time_speed " ) ) or 72
local time_multiplier = time_speed > 0 and ( 86400 / time_speed ) or 0
2020-09-05 19:49:12 +02:00
2024-09-20 14:00:49 +02:00
local function get_intervals_counter ( pos , interval , chance )
if time_multiplier == 0 then return 0 end
-- "wall clock time", so plants continue to grow while sleeping
local current_game_time = ( minetest.get_day_count ( ) + minetest.get_timeofday ( ) ) * time_multiplier
2020-09-05 19:49:12 +02:00
local approx_interval = math.max ( interval , 1 ) * math.max ( chance , 1 )
2024-09-20 14:00:49 +02:00
local meta = minetest.get_meta ( pos )
local last_game_time = meta : get_float ( " last_gametime " )
if last_game_time < 1 then
last_game_time = current_game_time - approx_interval * 0.5
2020-09-05 19:49:12 +02:00
elseif last_game_time == current_game_time then
current_game_time = current_game_time + approx_interval
end
2024-09-20 14:00:49 +02:00
meta : set_float ( " last_gametime " , current_game_time )
return ( current_game_time - last_game_time ) / approx_interval
2020-09-05 19:49:12 +02:00
end
local function get_avg_light_level ( pos )
local meta = minetest.get_meta ( pos )
2024-09-20 14:00:49 +02:00
-- EWMA would use a single variable:
-- local avg = meta:get_float("avg_light")
-- avg = avg + (node_light - avg) * 0.985
-- meta.set_float("avg_light", avg)
2020-09-05 19:49:12 +02:00
local summary = meta : get_int ( " avg_light_summary " )
2024-09-20 14:00:49 +02:00
local counter = meta : get_int ( " avg_light_count " )
2020-09-05 19:49:12 +02:00
if counter > 99 then
2024-09-20 14:00:49 +02:00
summary , counter = math.ceil ( summary * 0.5 ) , 50
2020-09-05 19:49:12 +02:00
end
2024-09-20 14:00:49 +02:00
local node_light = minetest.get_node_light ( pos )
if node_light ~= nil then
summary , counter = summary + node_light , counter + 1
meta : set_int ( " avg_light_summary " , summary )
meta : set_int ( " avg_light_count " , counter )
end
return math.ceil ( summary / counter )
2020-09-05 19:49:12 +02:00
end
2017-04-01 03:54:58 +02:00
function mcl_farming : add_plant ( identifier , full_grown , names , interval , chance )
2024-09-20 14:00:49 +02:00
local plant_info = { }
plant_info.full_grown = full_grown
plant_info.names = names
plant_info.interval = interval
plant_info.chance = chance
for _ , nodename in pairs ( names ) do
plant_nodename_to_id_list [ nodename ] = identifier
end
plant_info.step_from_name = { }
for i , name in ipairs ( names ) do
plant_info.step_from_name [ name ] = i
end
plant_lists [ identifier ] = plant_info
2017-03-13 23:09:27 +01:00
minetest.register_abm ( {
2017-05-15 00:45:54 +02:00
label = string.format ( " Farming plant growth (%s) " , identifier ) ,
2017-03-13 23:09:27 +01:00
nodenames = names ,
interval = interval ,
chance = chance ,
action = function ( pos , node )
2024-09-20 14:00:49 +02:00
local low_speed = minetest.get_node ( vector.offset ( pos , 0 , - 1 , 0 ) ) . name ~= " mcl_farming:soil_wet "
mcl_farming : grow_plant ( identifier , pos , node , 1 , false , low_speed )
2017-04-01 03:54:58 +02:00
end ,
2017-03-13 23:09:27 +01:00
} )
end
2017-04-01 03:54:58 +02:00
-- Attempts to advance a plant at pos by one or more growth stages (if possible)
-- identifier: Identifier of plant as defined by mcl_farming:add_plant
-- pos: Position
-- node: Node table
-- stages: Number of stages to advance (optional, defaults to 1)
2019-02-08 05:36:43 +01:00
-- ignore_light: if true, ignore light requirements for growing
2024-09-20 14:00:49 +02:00
-- low_speed: grow more slowly (not wet), default false
2017-05-25 03:24:11 +02:00
-- Returns true if plant has been grown by 1 or more stages.
-- Returns false if nothing changed.
2020-09-05 19:49:12 +02:00
function mcl_farming : grow_plant ( identifier , pos , node , stages , ignore_light , low_speed )
2024-09-20 14:00:49 +02:00
stages = stages or 1
2020-09-05 19:49:12 +02:00
local plant_info = plant_lists [ identifier ]
local intervals_counter = get_intervals_counter ( pos , plant_info.interval , plant_info.chance )
2024-10-09 18:01:20 +02:00
if stages > 0 then intervals_counter = intervals_counter - 1 end
2024-09-20 14:00:49 +02:00
if low_speed then -- 10% speed approximately
if intervals_counter < 1.01 and math.random ( 0 , 9 ) > 0 then return false end
intervals_counter = intervals_counter / 10
2020-09-05 19:49:12 +02:00
end
2024-09-20 14:00:49 +02:00
if not ignore_light and intervals_counter < 1.5 then
local light = minetest.get_node_light ( pos )
if not light or light < 10 then return false end
2017-04-01 03:54:58 +02:00
end
2020-09-05 19:49:12 +02:00
if intervals_counter >= 1.5 then
2024-09-20 14:00:49 +02:00
local average_light_level = get_avg_light_level ( pos )
if average_light_level < 0.1 then return false end
2020-09-05 19:49:12 +02:00
if average_light_level < 10 then
intervals_counter = intervals_counter * average_light_level / 10
end
end
2024-09-20 14:00:49 +02:00
local step = plant_info.step_from_name [ node.name ]
if step == nil then return false end
stages = stages + math.floor ( intervals_counter )
if stages == 0 then return false end
local new_node = { name = plant_info.names [ step + stages ] or plant_info.full_grown }
2017-04-01 03:54:58 +02:00
new_node.param = node.param
new_node.param2 = node.param2
minetest.set_node ( pos , new_node )
2017-05-25 03:24:11 +02:00
return true
2017-04-01 03:54:58 +02:00
end
2017-03-13 23:09:27 +01:00
function mcl_farming : place_seed ( itemstack , placer , pointed_thing , plantname )
local pt = pointed_thing
2024-09-20 14:00:49 +02:00
if not pt or pt.type ~= " node " then return end
2017-03-13 23:09:27 +01:00
-- Use pointed node's on_rightclick function first, if present
local node = minetest.get_node ( pt.under )
if placer and not placer : get_player_control ( ) . sneak then
if minetest.registered_nodes [ node.name ] and minetest.registered_nodes [ node.name ] . on_rightclick then
return minetest.registered_nodes [ node.name ] . on_rightclick ( pt.under , node , placer , itemstack ) or itemstack
end
end
2017-03-14 03:56:33 +01:00
2024-09-20 14:00:49 +02:00
if minetest.get_node ( pt.above ) . name ~= " air " then return end
local farmland = minetest.registered_nodes [ minetest.get_node ( vector.offset ( pt.above , 0 , - 1 , 0 ) ) . name ]
if not farmland or ( farmland.groups . soil or 0 ) < 2 then return end
minetest.sound_play ( minetest.registered_nodes [ plantname ] . sounds.place , { pos = pt.above } , true )
minetest.add_node ( pt.above , { name = plantname , param2 = minetest.registered_nodes [ plantname ] . place_param2 } )
2017-03-13 23:09:27 +01:00
2024-09-20 14:00:49 +02:00
if not minetest.is_creative_enabled ( placer : get_player_name ( ) ) then itemstack : take_item ( ) end
2017-03-13 23:09:27 +01:00
return itemstack
end
2017-03-13 22:30:37 +01:00
--[[ Helper function to create a gourd (e.g. melon, pumpkin), the connected stem nodes as
2022-11-16 13:17:51 +01:00
- full_unconnected_stem : itemstring of the full - grown but unconnected stem node . This node must already be done
2017-03-13 22:30:37 +01:00
- connected_stem_basename : prefix of the itemstrings used for the 4 connected stem nodes to create
2017-03-14 04:17:35 +01:00
- stem_itemstring : Desired itemstring of the fully - grown unconnected stem node
- stem_def : Partial node definition of the fully - grown unconnected stem node . Many fields are already defined . You need to add ` tiles ` and ` description ` at minimum . Don ' t define on_construct without good reason
- stem_drop : Drop probability table for all stem
2017-03-13 22:30:37 +01:00
- gourd_itemstring : Desired itemstring of the full gourd node
2022-02-23 18:14:44 +01:00
- gourd_def : ( almost ) full definition of the gourd node . This function will add on_construct and after_destruct to the definition for unconnecting any connected stems
2017-03-13 22:43:47 +01:00
- grow_interval : Will attempt to grow a gourd periodically at this interval in seconds
- grow_chance : Chance of 1 / grow_chance to grow a gourd next to the full unconnected stem after grow_interval has passed . Must be a natural number
2017-07-17 14:26:25 +02:00
- connected_stem_texture : Texture of the connected stem
2017-03-13 22:30:37 +01:00
] ]
2024-09-20 14:00:49 +02:00
function mcl_farming : add_gourd ( full_unconnected_stem , connected_stem_basename , stem_itemstring , stem_def , stem_drop , gourd_itemstring , gourd_def , grow_interval , grow_chance , connected_stem_texture )
2021-05-22 23:50:28 +02:00
local connected_stem_names = {
2017-03-13 22:30:37 +01:00
connected_stem_basename .. " _r " ,
connected_stem_basename .. " _l " ,
connected_stem_basename .. " _t " ,
2024-09-20 14:00:49 +02:00
connected_stem_basename .. " _b " }
2017-03-14 03:56:33 +01:00
2017-03-13 22:30:37 +01:00
-- Register gourd
2022-02-23 18:14:44 +01:00
if not gourd_def.after_destruct then
gourd_def.after_destruct = function ( blockpos , oldnode )
2017-03-13 22:30:37 +01:00
-- Disconnect any connected stems, turning them back to normal stems
2024-09-20 14:00:49 +02:00
-- four directions, but avoid using a table
-- opposite directions to above, as we go from groud to stem now!
local stempos = vector.offset ( blockpos , - 1 , 0 , 0 )
if minetest.get_node ( stempos ) . name == connected_stem_names [ 1 ] then
minetest.swap_node ( stempos , { name = full_unconnected_stem } )
end
local stempos = vector.offset ( blockpos , 1 , 0 , 0 )
if minetest.get_node ( stempos ) . name == connected_stem_names [ 2 ] then
minetest.swap_node ( stempos , { name = full_unconnected_stem } )
end
local stempos = vector.offset ( blockpos , 0 , 0 , - 1 )
if minetest.get_node ( stempos ) . name == connected_stem_names [ 3 ] then
minetest.swap_node ( stempos , { name = full_unconnected_stem } )
end
local stempos = vector.offset ( blockpos , 0 , 0 , 1 )
if minetest.get_node ( stempos ) . name == connected_stem_names [ 4 ] then
minetest.swap_node ( stempos , { name = full_unconnected_stem } )
2017-03-13 22:30:37 +01:00
end
end
end
minetest.register_node ( gourd_itemstring , gourd_def )
2017-03-14 04:17:35 +01:00
-- Register unconnected stem
-- Default values for the stem definition
if not stem_def.selection_box then
2024-09-20 14:00:49 +02:00
stem_def.selection_box = { type = " fixed " , fixed = { { - 0.15 , - 0.5 , - 0.15 , 0.15 , 0.5 , 0.15 } } }
end
stem_def.paramtype = stem_def.paramtype or " light "
stem_def.drawtype = stem_def.drawtype or " plantlike "
stem_def.walkable = stem_def.walkable or false
stem_def.sunlight_propagates = stem_def.sunlight_propagates == nil or stem_def.sunlight_propagates
stem_def.drop = stem_def.drop or stem_drop
stem_def.groups = stem_def.groups or { dig_immediate = 3 , not_in_creative_inventory = 1 , plant = 1 , attached_node = 1 , dig_by_water = 1 , destroy_by_lava_flow = 1 }
stem_def.sounds = stem_def.sounds or mcl_sounds.node_sound_leaves_defaults ( )
2017-03-14 04:17:35 +01:00
minetest.register_node ( stem_itemstring , stem_def )
2017-03-13 22:30:37 +01:00
-- Register connected stems
local connected_stem_tiles = {
2024-09-20 14:00:49 +02:00
{ " blank.png " , -- top
2022-11-16 13:17:51 +01:00
" blank.png " , -- bottom
" blank.png " , -- right
" blank.png " , -- left
connected_stem_texture , -- back
2024-09-20 14:00:49 +02:00
connected_stem_texture .. " ^[transformFX " -- front
2017-03-13 22:30:37 +01:00
} ,
2024-09-20 14:00:49 +02:00
{ " blank.png " , -- top
2022-11-16 13:17:51 +01:00
" blank.png " , -- bottom
" blank.png " , -- right
" blank.png " , -- left
2024-09-20 14:00:49 +02:00
connected_stem_texture .. " ^[transformFX " , -- back
2022-11-16 13:17:51 +01:00
connected_stem_texture , -- front
2017-03-13 22:30:37 +01:00
} ,
2024-09-20 14:00:49 +02:00
{ " blank.png " , -- top
2022-11-16 13:17:51 +01:00
" blank.png " , -- bottom
2023-05-27 17:27:01 +02:00
connected_stem_texture .. " ^[transformFX " , -- right
2022-11-16 13:17:51 +01:00
connected_stem_texture , -- left
2024-09-20 14:00:49 +02:00
" blank.png " , -- back
2022-11-16 13:17:51 +01:00
" blank.png " , -- front
2017-03-13 22:30:37 +01:00
} ,
2024-09-20 14:00:49 +02:00
{ " blank.png " , -- top
2022-11-16 13:17:51 +01:00
" blank.png " , -- bottom
connected_stem_texture , -- right
2023-05-27 17:27:01 +02:00
connected_stem_texture .. " ^[transformFX " , -- left
2024-09-20 14:00:49 +02:00
" blank.png " , -- back
2022-11-16 13:17:51 +01:00
" blank.png " , -- front
2017-03-13 22:30:37 +01:00
}
}
local connected_stem_nodebox = {
2022-11-16 13:17:51 +01:00
{ - 0.5 , - 0.5 , 0 , 0.5 , 0.5 , 0 } ,
{ - 0.5 , - 0.5 , 0 , 0.5 , 0.5 , 0 } ,
{ 0 , - 0.5 , - 0.5 , 0 , 0.5 , 0.5 } ,
{ 0 , - 0.5 , - 0.5 , 0 , 0.5 , 0.5 } ,
2017-03-13 22:30:37 +01:00
}
2017-03-14 02:43:48 +01:00
local connected_stem_selectionbox = {
2022-11-16 13:17:51 +01:00
{ - 0.1 , - 0.5 , - 0.1 , 0.5 , 0.2 , 0.1 } ,
{ - 0.5 , - 0.5 , - 0.1 , 0.1 , 0.2 , 0.1 } ,
{ - 0.1 , - 0.5 , - 0.1 , 0.1 , 0.2 , 0.5 } ,
{ - 0.1 , - 0.5 , - 0.5 , 0.1 , 0.2 , 0.1 } ,
2017-03-14 02:43:48 +01:00
}
2017-03-13 22:30:37 +01:00
2022-11-16 13:17:51 +01:00
for i = 1 , 4 do
2017-03-13 22:30:37 +01:00
minetest.register_node ( connected_stem_names [ i ] , {
_doc_items_create_entry = false ,
paramtype = " light " ,
sunlight_propagates = true ,
walkable = false ,
2017-03-14 04:17:35 +01:00
drop = stem_drop ,
2017-03-13 22:30:37 +01:00
drawtype = " nodebox " ,
2024-09-20 14:00:49 +02:00
node_box = { type = " fixed " , fixed = connected_stem_nodebox [ i ] } ,
selection_box = { type = " fixed " , fixed = connected_stem_selectionbox [ i ] } ,
2017-03-13 22:30:37 +01:00
tiles = connected_stem_tiles [ i ] ,
2021-02-18 14:00:17 +01:00
use_texture_alpha = minetest.features . use_texture_alpha_string_modes and " clip " or true ,
2024-09-20 14:00:49 +02:00
groups = { dig_immediate = 3 , not_in_creative_inventory = 1 , plant = 1 , attached_node = 1 , dig_by_water = 1 , destroy_by_lava_flow = 1 } ,
2017-03-13 22:30:37 +01:00
sounds = mcl_sounds.node_sound_leaves_defaults ( ) ,
_mcl_blast_resistance = 0 ,
} )
2017-03-21 04:56:16 +01:00
if minetest.get_modpath ( " doc " ) then
doc.add_entry_alias ( " nodes " , full_unconnected_stem , " nodes " , connected_stem_names [ i ] )
end
2017-03-13 22:30:37 +01:00
end
minetest.register_abm ( {
2022-11-16 13:17:51 +01:00
label = " Grow gourd stem to gourd ( " .. full_unconnected_stem .. " → " .. gourd_itemstring .. " ) " ,
nodenames = { full_unconnected_stem } ,
neighbors = { " air " } ,
2017-03-13 22:43:47 +01:00
interval = grow_interval ,
chance = grow_chance ,
2017-03-13 22:30:37 +01:00
action = function ( stempos )
local light = minetest.get_node_light ( stempos )
2024-09-20 14:00:49 +02:00
if not light or light <= 10 then return end
2024-10-09 18:01:20 +02:00
-- Pick one neighbor and check if it can be used to grow
local dir = math.random ( 1 , 4 ) -- pick direction at random
local neighbor = ( dir == 1 and vector.offset ( stempos , 1 , 0 , 0 ) )
or ( dir == 2 and vector.offset ( stempos , - 1 , 0 , 0 ) )
or ( dir == 3 and vector.offset ( stempos , 0 , 0 , 1 ) )
or vector.offset ( stempos , 0 , 0 , - 1 )
if minetest.get_node ( neighbor ) . name ~= " air " then return end -- occupied
-- check for suitable floor: grass, dirt, or soil
local floorpos = vector.offset ( neighbor , 0 , - 1 , 0 )
local floorname = minetest.get_node ( floorpos ) . name
local floordef = minetest.registered_nodes [ floorname ]
if not floordef then return end
if ( floordef.groups . grass_block or 0 ) == 0 and ( floordef.groups . dirt or 0 ) == 0 and ( floordef.groups . soil or 0 ) < 2 then return end -- not suitable for growing
2024-09-20 14:00:49 +02:00
minetest.swap_node ( stempos , { name = connected_stem_names [ dir ] } )
if gourd_def.paramtype2 == " facedir " then
local p2 = ( dir == 1 and 3 ) or ( dir == 2 and 1 ) or ( dir == 3 and 2 ) or 0
minetest.add_node ( neighbor , { name = gourd_itemstring , param2 = p2 } )
else
minetest.add_node ( neighbor , { name = gourd_itemstring } )
end
-- Reset farmland, etc. to dirt when the gourd grows on top
2024-10-09 18:01:20 +02:00
if ( floordef.groups . dirtifies_below_solid or 0 ) > 0 then
2024-09-20 14:00:49 +02:00
minetest.set_node ( floorpos , { name = " mcl_core:dirt " } )
end
2017-03-13 22:30:37 +01:00
end ,
} )
end
2017-07-21 19:47:20 +02:00
-- Used for growing gourd stems. Returns the intermediate color between startcolor and endcolor at a step
-- * startcolor: ColorSpec in table form for the stem in its lowest growing stage
-- * endcolor: ColorSpec in table form for the stem in its final growing stage
-- * step: The nth growth step. Counting starts at 1
-- * step_count: The number of total growth steps
function mcl_farming : stem_color ( startcolor , endcolor , step , step_count )
2024-09-20 14:00:49 +02:00
local mix = ( step - 1 ) / ( step_count - 1 )
return string.format ( " #%02X%02X%02X " ,
math.max ( 0 , math.min ( 255 , math.round ( ( 1 - mix ) * startcolor.r + mix * endcolor.r ) ) ) ,
math.max ( 0 , math.min ( 255 , math.round ( ( 1 - mix ) * startcolor.g + mix * endcolor.g ) ) ) ,
math.max ( 0 , math.min ( 255 , math.round ( ( 1 - mix ) * startcolor.b + mix * endcolor.b ) ) ) )
2017-07-21 19:47:20 +02:00
end
2020-09-05 19:49:12 +02:00
2022-12-23 15:05:23 +01:00
--[[Get a callback that either eats the item or plants it.
Used for on_place callbacks for craft items which are seeds that can also be consumed .
2022-12-24 19:38:32 +01:00
] ]
2022-12-23 15:05:23 +01:00
function mcl_farming : get_seed_or_eat_callback ( plantname , hp_change )
return function ( itemstack , placer , pointed_thing )
2024-09-20 14:00:49 +02:00
return mcl_farming : place_seed ( itemstack , placer , pointed_thing , plantname )
or minetest.do_item_eat ( hp_change , nil , itemstack , placer , pointed_thing )
2022-12-23 15:05:23 +01:00
end
end
2020-09-05 19:49:12 +02:00
minetest.register_lbm ( {
label = " Add growth for unloaded farming plants " ,
name = " mcl_farming:growth " ,
2022-11-16 13:17:51 +01:00
nodenames = { " group:plant " } ,
2020-09-05 19:49:12 +02:00
run_at_every_load = true ,
2024-09-20 14:00:49 +02:00
action = function ( pos , node , dtime_s )
2020-09-05 19:49:12 +02:00
local identifier = plant_nodename_to_id_list [ node.name ]
2024-09-20 14:00:49 +02:00
if not identifier then return end
local low_speed = minetest.get_node ( vector.offset ( pos , 0 , - 1 , 0 ) ) . name ~= " mcl_farming:soil_wet "
mcl_farming : grow_plant ( identifier , pos , node , 0 , false , low_speed )
2020-09-05 19:49:12 +02:00
end ,
} )
2024-09-20 14:00:49 +02:00