459 lines
17 KiB
Lua
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
|