Files

459 lines
17 KiB
Lua

-- this file contains the code that is used by scythes
local is_farming_redo = core.get_modpath("farming") ~= nil and farming ~= nil and farming.mod == "redo"
local mod_mcl_farming = core.get_modpath("mcl_farming") ~= nil
local is_voxelibre = mod_mcl_tools and (mcl_tools == nil)
local S = farmtools.i18n
local DEFAULT_SCYTHE_USES = 90
local farming_redo_exceptions = {
-- this is the list of plants that don't drop their seeds when harvested
-- instead, the player normally has to craft the seeds from the item dropped during harvesting
-- however, this causes a problem with scythes, since they require the player to have the seeds
-- in the inventory to replant the plant after harvesting.
-- the approach taken here is to automatically craft the seeds from the item when no seed is
-- available.
-- for that, we need the list below
["farming:garlic_clove"] = { { item = "farming:garlic", seeds_in_item = 8 } },
["farming:seed_sunflower"] = { { item = "farming:sunflower", seeds_in_item = 5 } },
["farming:peppercorn"] = {
{ item = "farming:pepper", seeds_in_item = 1 },
{ item = "farming:pepper_red", seeds_in_item = 1 },
{ item = "farming:pepper_yellow", seeds_in_item = 1 },
},
["farming:pumpkin_slice"] = { { item = "farming:pumpkin_8", seeds_in_item = 4 } },
["farming:melon_slice"] = { { item = "farming:melon_8", seeds_in_item = 4 } },
}
local item_craftable_to_seeds_in_inv = function(seeds, invref)
-- this function should run when seeds required to replant the harvested plant are
-- not available in the player inventory
-- it checks whether an item from the inventory can be crafted into the required seeds and
-- returns the item name along the number of seeds that can be crafted from it if it is the case
if is_farming_redo and farming_redo_exceptions[seeds] ~= nil then
for _, def in pairs(farming_redo_exceptions[seeds]) do
if invref:contains_item("main", def) then
return def
end
end
return nil
end
end
local get_item_group = function(def, group)
if def == nil or def.groups == nil or def.groups[group] == nil then
return 0
end
return def.groups[group]
end
local get_plant_definition = function(plant)
-- this function returns the plant definition from the farming API
-- x_farming has its own API
-- if the plant is registered in the x_farming API, we return its definition
local modname = plant:split(":")[1] or ""
if modname == "x_farming" then
-- note that we do not check whether x_farming is enabled,
-- because if there is an item whose modname is x_farming,
-- it means that the mod is enabled.
-- if this is incorrect or may lead to bugs, please let me know :)
local name = plant:split(":")[2] or ""
local pname = name:gsub("(.*)_.*$", "%1")
return x_farming.registered_plants[pname]
end
-- next we need to check whether the farming API is provided by the default
-- farming mod or by the Farming Redo version
if is_farming_redo then
-- if we are using Farming Redo, we need to iterate through the registered plants
-- to check if the given plant is registered. if it is, we return its definition
for _, data in pairs(farming.registered_plants) do
if data.crop == plant then
return data
end
end
end
-- if no definition was found previously, we try to see whether the plant can be found
-- through the farming API (without checking whether it is the one from the default farming mod
-- or the Redo mod, since the latter is fully compatible with the default one).
-- if a plant is found, we return its definition
if farming ~= nil and farming.registered_plants[plant] ~= nil then
return farming.registered_plants[plant]
end
-- finally, if no definition was found as of yet, we try one last time to see if the plant was
-- registered without its mod name, and return the result.
-- if no plant is found, the result will be nil.
local name = plant:split(":")[2] or ""
local pname = name:gsub("(.*)_.*$", "%1")
if mod_mcl_farming then
return mcl_farming.plant_lists["plant_" .. pname]
end
if farming ~= nil then
return farming.registered_plants[pname]
else
return ""
end
end
local get_seed_name = function(plant)
-- this function returns the plant's seeds using various methods
-- it should be executed only IF WE KNOW that the plant exists
-- (i.e. the function get_plant_definition returned a value that is different from nil)
-- TODO: maybe we could optimize this function by passing the plant definition directly,
-- instead of having to use the API two times.
-- if Farming Redo is enabled, we can use its API to find the seeds
-- (as long as the plant was registered through the farming API)
-- if we find one, we return the seed name
if is_farming_redo then
for _, data in pairs(farming.registered_plants) do
if data.crop == plant then
return data.seed
end
end
end
if mod_mcl_farming then
local orig_plant = plant
local seeds = plant
local this_seeds = core.registered_craftitems[seeds]
-- technically this is for other mods too but it covers mineclonia
local suffixes = { "_seeds", "_seed", "_item", "" }
local continue = true
local x = 0
while this_seeds == nil and continue do
for i, suffix in ipairs(suffixes) do
seeds = plant .. suffix
this_seeds = core.registered_craftitems[seeds]
if this_seeds ~= nil then
-- return plain text name of item
return seeds
end
x = x + 1
if x == 1 then
-- strip "_plant" and try again
plant = plant:gsub("_plant.*$", "")
elseif x > 2 then
continue = false
end
end
end
-- if we get here, there is no seed we could find.
core.log("warning", "No mcl-based seeds calculated for item " .. orig_plant)
end
-- if no plant was found previously, we try to see whether the plant can be found
-- through the farming API (without checking whether it is the one from the default farming mod
-- or the Redo mod, since the latter is fully compatible with the default one).
-- if a plant is found and if its definition contains a "seed" property, we return its seed name
if farming.registered_plants[plant] ~= nil and farming.registered_plants[plant].seed ~= nil then
return farming.registered_plants[plant].seed
end
-- finally, if no plant was found as of yet, we return an item named exactly like the plant,
-- but with "seed_" appended right before the plant name
local mod = plant:split(":")[1]
local name = plant:split(":")[2]
local pname = name:gsub("(.*)_.*$", "%1")
return mod .. ":seed_" .. pname
end
local harvest_and_replant = function(pos, user)
-- this is the function used to harvest and replant a single node using the scythe
-- it returns a boolean value that indicates whether it was able to harvest (and hopefully replant)
-- the node at a given position
local playername = user:get_player_name()
local node = core.get_node(pos)
local node_id = node.name:gsub("(.*)_.*$", "%1")
if mod_mcl_farming then
node_id = node.name
end
-- try to get a plant definition for the given node
-- if none is found, we will exit this function and return false
local plantdef = get_plant_definition(node_id)
if plantdef == nil then
return false
end
-- try to guess current stage of the plant from its node ID
local stage = tonumber(node.name:gsub(".*_(.*)$", "%1") or 0)
-- if either:
-- - the definition does not contain a "steps" property to indicate how many stages the plant goes through when maturing
-- - the current stage of the plant couldn't be detected from the node ID
-- we exit the function by returning false
if mod_mcl_farming then
if plantdef.full_grown == nil then
return false
elseif plantdef.full_grown == node_id then
else
return false
end
else
-- now we test the current stage of the plant against its total number of steps
-- if the value of "stage" is lower than "steps", it means that the plant isn't yet ready for harvesting
-- thus, we exit this function and return false
if plantdef.steps == nil or stage == nil or stage < plantdef.steps then
return false
end
end
-- if the node is protected or the given user isn't allowed to break it,
-- we record the violation attempt and return false
if core.is_protected(pos, playername) then
core.record_protection_violation(pos, playername)
return false
end
-- otherwise we can go ahead and break the plant, while playing a nice sound
core.node_dig(pos, node, user)
core.sound_play("default_dig_snappy", { pos = pos, gain = 0.5, max_hear_distance = 8 }, true)
-- once the plant is broken, we need to replant it
core.after(0, function()
-- this function replants the plant after is is harvested
-- it also takes care of removing the correct amount of seeds from the player inventory
local invref = user:get_inventory()
local seeds = get_seed_name(node_id)
-- this is required to prevent trellis and bean poles duplication
if is_farming_redo then
if node_id == "farming:grapes" then
if invref:contains_item("main", "farming:trellis") then
invref:remove_item("main", "farming:trellis")
end
elseif node_id == "farming:beanpole" then
if invref:contains_item("main", "farming:beanpole") then
invref:remove_item("main", "farming:beanpole")
end
end
end
-- if seeds are not present in the inventory, and if no item from the inventory can be
-- crafted into the seeds, we won't go further. it means that we cannot replant the plant
local item_in_inv = item_craftable_to_seeds_in_inv(seeds, invref)
if (not mod_mcl_farming) and (core.get_node(pos).name ~= "air" or not invref:contains_item("main", seeds)) then
if item_in_inv == nil then
return true
end
end
-- we will only remove seeds from the player inventory if they are not in creative mod
if not core.is_creative_enabled(playername) and not mod_mcl_farming then
if not invref:contains_item("main", seeds) and item_in_inv ~= nil then
-- if no seed is found in the inventory but an item from the inventory can be crafted
-- into the seeds:
-- first remove one of these items from the inventory
invref:remove_item("main", item_in_inv.item)
-- then add the number of seeds normally returned by the craft, less one (because we
-- use one to replant the plant)
local stack = ItemStack(seeds .. " " .. item_in_inv.seeds_in_item)
invref:add_item("main", stack)
else
-- if seeds are already present in the inventory, then just remove one of them
-- from the inventory
invref:remove_item("main", seeds)
end
end
-- finally, we need to replant the crop
-- it is a bit different, depending on the version of the farming mod used.
-- TODO: the behavior of this code depends on whether Farming Redo is used, but
-- not on the mod that provides the plant. If the plant is from x_farming, the behavior
-- will be different if Farming Redo is enabled and if it is disabled.
if is_farming_redo then
-- plant first crop for farming redo
local crop_name = node_id .. "_1"
local crop_def = core.registered_nodes[crop_name]
if crop_def == nil then
return
end
core.set_node(pos, { name = crop_name, param2 = crop_def.place_param2 })
else
if mod_mcl_farming then
-- plant seeds for Mineclonia farming
-- try level 0
local level1 = node_id .. "_0"
local crop_def = core.registered_nodes[level1]
if crop_def == nil then
level1 = node_id .. "_1"
crop_def = core.registered_nodes[level1]
end
-- keep the same paramtype2 = "meshoptions" from lua_api.md
core.set_node(pos, { name = level1, param2 = node.param2 })
-- need to remove seed afterwards for Mineclonia, because it dropped seeds
-- and not immediately placed them in player inventory. 2 seconds
-- ought to be enough for the player to walk over the seeds.
core.after(2, function()
if not core.is_creative_enabled(playername) then
invref:remove_item("main", { name = seeds, count = 1 })
end
end)
else
-- plant seeds for MTG farming
core.set_node(pos, { name = seeds, param2 = 1 })
end
-- timer values taken from farming mod (see tick function in api.lua)
core.get_node_timer(pos):start(math.random(166, 286))
end
end)
-- finally, we return true, because we were able to harvest (and hopefully replant the crop)
return true
end
farmtools.scythe_on_use = function(itemstack, user, pointed_thing, uses)
-- this function is called when the scythe is used on a node
-- if the scythe is used when the player isn't pointing at anything, we won't do anything
if pointed_thing == nil then
return
end
-- if pointing at an object/entity, punch it with the scythe
local itemdef = itemstack:get_definition()
if pointed_thing.type == "object" then
local tool_capabilities = itemstack:get_tool_capabilities()
local meta = itemstack:get_meta()
local last_punch = meta:get_float("last_punch") or 0
local now = core.get_gametime()
meta:set_float("last_punch", now)
pointed_thing.ref:punch(user, now - last_punch, tool_capabilities)
end
-- if pointing at something other than a node, don't go further
if pointed_thing.type ~= "node" then
return
end
-- the scythe group allows us to specify how big the area to harvest should be
-- if scythe = 2, then the area is a 3x3 area
-- if scythe = 3, then the area is a 5x5 area
-- and so on...
-- this does not correspond to the actual level of the scythe,
-- unfortunately we need to keep it this way because doing otherwise would break compatibility with
-- mods that are using the scythe group as provided by the original sickles mod
-- in short: the value of the scythe group = level of the scythe + 1
local range = (get_item_group(itemdef, "scythe") or 1) - 1
-- TODO: is all of the code below really supposed to be executed when pointed_thing.type == "object"?
-- if not, we should maybe wrap this in an if pointed_thing.type == "node" condition
-- get the position of the node that the user is pointing at
local pos = pointed_thing.under
-- try to harvest (and replant) the node
local harvested = harvest_and_replant(pos, user)
-- if it couldn't be harvested, we won't go any further
if not harvested then
return
end
-- this will be required for wearing tools depending on the number of nodes harvested
local total_nodes_changed = 1
-- otherwise we harvest and replant the adjacent nodes within the range specified in the item definition
if range > 0 then
local pos1 = vector.add(pos, { x = -range, y = 0, z = -range })
local pos2 = vector.add(pos, { x = range, y = 0, z = range })
local positions = core.find_nodes_in_area(pos1, pos2, "group:plant")
for _, check_pos in ipairs(positions) do
if pos ~= check_pos then
if harvest_and_replant(check_pos, user) then
total_nodes_changed = total_nodes_changed + 1
end
end
end
end
-- wear out tools if the player is not in creative mode
local playername = user and user:get_player_name() or ""
if not core.is_creative_enabled(playername) then
if mod_mcl_farming then
local calc_uses = get_item_group(itemdef, "scythe_uses")
-- if mineclonia
if not is_voxelibre then
calc_uses = mcl_util.calculate_durability(itemstack) or calc_uses
end
itemstack:add_wear_by_uses(calc_uses / total_nodes_changed)
else
itemstack:add_wear_by_uses(uses / total_nodes_changed)
end
end
-- finally return the worn out scythe
return itemstack
end
farmtools.register_scythe = function(name, def)
-- this function is used for registering new scythes in the easiest way possible
-- register new scythes in your mod's namespace
if name:sub(1, 1) ~= ":" then
name = ":" .. name
end
-- check definition table
if def.description == nil then
def.description = S("Scythe")
end
if def.inventory_image == nil then
def.inventory_image = "unknown_item.png"
end
if def.max_uses == nil then
def.max_uses = DEFAULT_SCYTHE_USES
end
if def.level == nil then
def.level = 1
end
-- support the definition providing groups, primarily for enchantability for voxelibre
local groups = { scythe = def.level + 1 }
if def.groups then
for k, v in pairs(def.groups) do
groups[k] = v
end
end
-- now register the tool through the Luanti API
core.register_tool(name, {
description = def.description,
inventory_image = def.inventory_image,
tool_capabilities = {
full_punch_interval = 1.2,
damage_groups = { fleshy = 5 },
punch_attack_uses = def.max_uses * 2,
},
range = 12,
on_use = function(itemstack, user, pointed_thing)
return farmtools.scythe_on_use(itemstack, user, pointed_thing, def.max_uses)
end,
groups = groups,
sound = { breaks = "default_tool_breaks" },
_mcl_uses = groups["scythe_uses"] or def.max_uses,
})
-- now register recipe
if def.recipe then
core.register_craft({
output = name:sub(2),
recipe = def.recipe,
})
elseif def.material then
core.register_craft({
output = name:sub(2),
recipe = {
{ "", def.material, def.material },
{ def.material, "", "group:stick" },
{ "", "", "group:stick" },
},
})
end
end