Minetest_Game_Mushroom_Fork/mods/specific to Mushroom Fork/runes/tool_crafting.lua

531 lines
21 KiB
Lua

-- runes mod for Minetest
-- Copyright © 2020 Alex Yst <mailto:copyright@y.st>
-- This program is free software; you can redistribute it and/or
-- modify it under the terms of the GNU Lesser General Public
-- License as published by the Free Software Foundation; either
-- version 2.1 of the License, or (at your option) any later version.
-- This software is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-- Lesser General Public License for more details.
-- You should have received a copy of the GNU Lesser General Public
-- License along with this program. If not, see
-- <https://www.gnu.org./licenses/>.
-- For readability, no more than 29 lines of text should be used in the
-- item description. For a better appearance, no more than 28 lines
-- should be used. I know this seems like an excessive amount of text
-- and we'd never need that much, but keep in mind that the tool
-- description is the only way to convey instance-specific information
-- about the tool to players in the main inventory menu tab and other
-- inventory pages such as those presented by chests and furnaces. A
-- separate analysis tab could be added to player menus that that
-- conveys information in a cleaner way, but this requires players to
-- switch to another tab just to figure out what the tool is, and if
-- they have it in storage, they have to additionally take it out of
-- storage before checking and put it back after. That's a bit of a
-- pain. As such, the description is a better place for it. As for
-- what information would take up that much space, it'd be nice to have
-- all 31 runes provide separate buffs and have each buff - if present
-- - have its own line. The repair count and the tools actual name
-- should probably each get their own lines too, for a total of up to
-- 33 lines. Obviously, this is too many.
--
-- For now, there aren't enough buffs, so we can ignore the problem.
-- Later on though, if we get more buffs, we're going to need to
-- conditionally conserve lines based on the number of buffs a tool
-- has. If it has enough to push the line count above 28, perhaps two
-- buffs should be put on each line.
local S = minetest.get_translator("runes")
-- It's easier just to store this table in another file for maintenance
-- purposes.
local rune_and_tool_to_buff = dofile(minetest.get_modpath("runes").."/buff_table.lua")
local buff_name = {
debug = S("Debug Buff"),
thirst = S("Level Thirst"),
pruner = S("Pruner"),
spreader = S("Spreader"),
durable = S("Durable"),
rapid = S("Rapid Digger"),
toter = S("Toter"),
transplanter = S("Transplanter"),
}
local rune_element_modname = {
sand = "default",
coal_lump = "default",
iron_lump = "default",
sapling = "default",
mese_crystal = "default",
copper_lump = "default",
diamond = "default",
gold_lump = "default",
junglesapling = "default",
seed_cotton = "farming",
seed_wheat = "farming",
string = "farming",
wheat = "farming",
cotton = "farming",
pine_sapling = "default",
acacia_sapling = "default",
mushroom_brown = "flowers",
mushroom_red = "flowers",
mushroom_spores_brown = "flowers",
mushroom_spores_red = "flowers",
aspen_sapling = "default",
flint = "default",
coral_skeleton = "default",
acacia_bush_sapling = "default",
bush_sapling = "default",
tin_lump = "default",
blueberries = "default",
blueberry_bush_sapling = "default",
ice = "default",
pine_bush_sapling = "default",
dry_dirt = "default",
}
-- I don't really know the right way to choose a random hexadecimal
-- digit. Choosing a random number from zero to fifteen and then using
-- a conversion lookup table seems like the wrong way to do it, but
-- it's simple and it works.
local hexadecimal = {
[0] = "0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
}
-- All aura colours are chosen randomly.
local function add_colour(meta)
local colour = "#"
for i = 0, 5 do
colour = colour..hexadecimal[math.random(0, 15)]
end
meta:set_string("color", colour)
end
-- This function applies the initial buffs to a tool upon crafting.
local function generate_tool(tool_name, player_name, runes)
local tool = ItemStack("runes:"..tool_name)
local description = minetest.registered_tools["runes:"..tool_name].description.."\nRepair count: 0"
local meta = tool:get_meta()
add_colour(meta)
meta:set_int("repair", 0)
local used_runes = {}
local buffs = {}
for _, rune in next, runes do
local rune_name = rune:get_name():sub(12)
-- Duplicate runes in the same tool are disallowed.
if used_runes[rune_name] then
return
else
used_runes[rune_name] = true
end
local buff_key = rune_and_tool_to_buff[tool_name][rune_name]
local level_key = rune_element_modname[rune_name]..":"..rune_name
buffs[buff_key] = buffs[buff_key] or -1
buffs[buff_key] = buffs[buff_key] + liblevelup.get.player_material_level(player_name, level_key)
end
for buff, strength in next, buffs do
if strength > -1 then
-- Currently, a feature in the stable version but not the development
-- version of Minetest prevents setting the value to integer zero from
-- working correctly, but setting to string zero should work just fine.
-- meta:set_int(buff, strength)
meta:set_string(buff, strength)
description = description.."\n"..buff_name[buff].." ("..strength..")"
end
end
meta:set_string("description", description)
-- The durability buff and rapid-digging buff need to make changes to
-- the tool capabilities.
if buffs.durability or buffs.rapid then
local overwrite = false
local caps = tool:get_tool_capabilities()
for _, group in next, caps.groupcaps do
if buffs.durability then
group.uses = group.uses * 1.5
end
if buffs.rapid then
for key, value in next, group.times do
group.times[key] = value / 2
end
end
overwrite = true
end
if overwrite then
meta:set_tool_capabilities(caps)
end
end
return tool
end
-- When the crafting, players expect the result to be what they saw
-- before they finalised the crafting process. However, a random aura
-- colour is assigned to each tool. To make things make more sense to
-- the player, the crafted item is decided when the craft prediction is
-- made and that result is cached. When the player finalises the craft,
-- the craft result override function doesn't recalculate the result,
-- but uses the cached item instead, so what the player saw beforehand
-- is what they get when they finalise the process.
--
-- Oddly enough, Minetest calls the crafting-prediction callbacks right
-- before performing the craft, so even returning the cached value
-- results in the player getting a random colour of tool that they
-- weren't expecting. To *REALLY* fix the issue and remove output
-- randomness, we actually need a *SECOND* cache. The first cache holds
-- the last-seen value, which then gets copied to the second cache
-- before getting overwritten with the next last-seen value. The value
-- in the first cache needs to be returned
local cache0 = {}
local cache1 = {}
-- Tool repair is handled here to prevent the over-repairing of tools.
-- Over-repair can allow players to pack a large number of buffs onto
-- their tools very quickly without first wearing down the weaker
-- component tools. As such, standard tool repair is disabled and
-- handled here manually. Experiments show that the tool repair bonus
-- set by the default mod adds 1311 points of durability, regardless of
-- the state of repair of the component tools. I checked this with
-- tools in multiple states of disrepair as I wasn't sure whether the
-- "additional_wear" referred to a fraction of the component tools'
-- respective levels of wear or a fraction of the tool's maximum
-- durability. As the numbers stay the same regardless of the level of
-- wear, it must be the tool's maximum durability. Checking the
-- numbers, default sets the additional_wear to -0.02 and -0.02
-- multiplied by the maximum durability (65536) is 1310.72, which would
-- round to the observed 1311.
--
-- Because Minetest records tool state as wear (lost health) rather
-- than remaining durability (remaining health), we can't directly add
-- the values from the two tools or it'd cause the result to become
-- even *more* worn, rather than repaired. With that in mind, we take
-- the negated repair bonus, subtract the full health of one of the
-- tools, then add the wear of both tools. All tools have the same
-- maximum health, so we might as well keep that value here in the same
-- variable as the repair bonus.
local repair_wear = -1311 - 65536
-- There's no way to specify that each item in a group-based crafting
-- recipe must use a different item from the group. You either have to
-- define a boatload of nearly-identical recipes using specific
-- ingredient names to cover each and every possible valid
-- configuration or you have to work outside the normal crafting recipe
-- system. Covering each and every valid case would be more than doable
-- using a set of no more than three nested loops with a registration
-- function call in the deepest loop, but that seems like having that
-- many registered recipes could put heavy strain on Minetest. In the
-- case of picks, for example, that's 26970 separate registered
-- recipes. And that's just for the picks. The axes would take 26970
-- more; the shovels, fire starters, and hoes would take 930 recipes
-- each; and the shovels and screwdrivers would take 31 each. Clearly,
-- this isn't the best solution. That's why this function here uses
-- pseudo-recipes to handle crafting manually instead of relying on the
-- regular recipe-registration system.
--
-- Additionally, of course, this function handles the tool repair bonus
-- mentioned above, the passing on of buffs to combined tools when
-- repairing them, and the deciding of which buffs and at what strength
-- the tool gets in the case of crafting new tools from runes.
local function calculate_craft_result(itemstack, player, old_craft_grid, craft_inv)
-- If the itemstack isn't empty, it means a crafting recipe has already
-- defined the output. In theory, the rune tool recipes shouldn't be
-- overlapping with any engine-handled recipes, so if there's output,
-- we should just assume the crafting grid isn't in a state we care
-- about at the moment.
if not itemstack:is_empty() then
return
end
-- Let's check for tool repair recipes new tool recipes just because
-- they're the easiest to look for.
local repair_tool = true
local tools = {}
for i = 1, 9 do
local item = craft_inv:get_stack("craft", i)
if not item:is_empty() then
if item:get_definition().type == "tool" and item:get_name():sub(1, 6) == "runes:" then
-- If the item is a rune tool, we need to add it to the list of tools
-- we've found.
table.insert(tools, item)
else
-- If it's not a runic tool, it means this is either not a tool repair
-- recipe or it's not a tool repair recipe that we should interfere
-- with.
repair_tool = false
end
end
end
-- For tool repair to work out, we need there to be exactly two runic
-- tools with the exact same base tool type (for example, not a sword
-- and a screwdriver, but two swords) and no other items in the
-- crafting grid.
if repair_tool and #tools == 2 and tools[1]:get_name() == tools[2]:get_name() then
local tool_wear = repair_wear + tools[1]:get_wear() + tools[2]:get_wear()
if tool_wear < 0 then
-- Over-repair isn't allowed, so if the resulting tool has less than
-- zero damage, the repair is aborted.
return
else
-- Create a new tool.
local tool_type = tools[1]:get_name()
local tool = ItemStack(tool_type)
-- Set the tool's wear.
tool:set_wear(tool_wear)
-- We're going to need to work with the metadata of all three tools:
-- the two component tools and their merged form.
local meta = tool:get_meta()
local meta0 = tools[1]:get_meta()
local meta1 = tools[2]:get_meta()
-- Set a random aura colour.
local colour = "#"
for i = 0, 5 do
colour = colour..hexadecimal[math.random(0, 15)]
end
meta:set_string("color", colour)
-- Repair count is the number of repairs done on the first tool, plus
-- the number of repairs done on the second tool, plus the repair being
-- currently performed to merge the two tools.
local repair_count = meta0:get_int("repair") + meta1:get_int("repair") + 1
meta:set_int("repair", repair_count)
local description = minetest.registered_tools[tool_type].description.."\nRepair count: "..repair_count
local buff_durability = false
local buff_rapid = false
-- Merge the buffs, then wear them by subtracting one from each. If any
-- buff drops below zero, remove it. Otherwise, add it to the repaired
-- tool.
--
-- It's important to know that while a buff below zero is simply
-- removed, a buff *AT* zero is retained. The buff level signifies how
-- many generations the buff will be passed down to. At buff level
-- zero, the buff has been successfully passed to the current
-- generation of the tool, but the tool will pass it zero generations
-- further, so its descendants won't inherit it. Also, if both parent
-- tools have the buff, a single level is only consumed from merging
-- and a bonus level is granted if both tools have the buff.
-- Technically, two tools with the same level zero buff will therefore
-- pass that buff on to the child tool as a level zero buff. If only
-- one tool has it though, it won't be passed on.
for buff, name in next, buff_name do
local strength = -2
if meta0:get(buff) then
strength = strength + 1 + meta0:get_int(buff)
if buff == "durability" then
buff_durability = true
elseif buff == "rapid" then
buff_rapid = true
end
end
if meta1:get(buff) then
strength = strength + 1 + meta1:get_int(buff)
if buff == "durability" then
buff_durability = true
elseif buff == "rapid" then
buff_rapid = true
end
end
if strength > -1 then
-- Currently, a feature in the stable version but not the development
-- version of Minetest prevents setting the value to integer zero from
-- working correctly, but setting to string zero should work just fine.
-- meta:set_int(buff, strength)
meta:set_string(buff, strength)
description = description.."\n"..name.." ("..strength..")"
end
end
meta:set_string("description", description)
-- The durability buff and rapid-digging buff need to make changes to
-- the tool capabilities.
if buff_durability or buff_rapid then
local overwrite = false
local caps = tool:get_tool_capabilities()
for _, group in next, caps.groupcaps do
if buff_durability then
group.uses = group.uses * 1.5
end
if buff_rapid then
for key, value in next, group.times do
group.times[key] = value / 2
end
end
overwrite = true
end
if overwrite then
meta:set_tool_capabilities(caps)
end
end
-- Provide the crafting result.
return tool
end
end
-- Now, let's try new tool recipes.
--
-- First, let's get some information about the state of the crafting
-- grid. The thing is that the crafting grid can't simply be read like
-- a list. When recipes are smaller than three by three, they can be
-- placed anywhere in the grid that would allow them to have the
-- correct shape and correct orientation. As such, we need to know
-- which rows and which columns of the grid are even relevant to us.
local row_0 = not (old_craft_grid[1]:is_empty() and old_craft_grid[2]:is_empty() and old_craft_grid[3]:is_empty())
local row_2 = not (old_craft_grid[7]:is_empty() and old_craft_grid[8]:is_empty() and old_craft_grid[9]:is_empty())
local row_1 = not (old_craft_grid[4]:is_empty() and old_craft_grid[5]:is_empty() and old_craft_grid[6]:is_empty())
or (row_0 and row_2)
local col_0 = not (old_craft_grid[1]:is_empty() and old_craft_grid[4]:is_empty() and old_craft_grid[7]:is_empty())
local col_2 = not (old_craft_grid[3]:is_empty() and old_craft_grid[6]:is_empty() and old_craft_grid[9]:is_empty())
local col_1 = not (old_craft_grid[2]:is_empty() and old_craft_grid[5]:is_empty() and old_craft_grid[8]:is_empty())
or (col_0 and col_2)
local width = 0
local height = 0
local startrow
local startcol
if row_2 then
height = height + 1
startrow = 2
end
if row_1 then
height = height + 1
startrow = 1
end
if row_0 then
height = height + 1
startrow = 0
end
if col_2 then
width = width + 1
startcol = 2
end
if col_1 then
width = width + 1
startcol = 1
end
if col_0 then
width = width + 1
startcol = 0
end
-- Now that we know the information about how big the recipe is and
-- where it starts, we can check it against rune tool recipes.
if height == 3 then
if width == 3 then
if old_craft_grid[1]:get_name():sub(1, 11) == "runes:rune_"
and old_craft_grid[2]:get_name():sub(1, 11) == "runes:rune_"
and old_craft_grid[3]:get_name():sub(1, 11) == "runes:rune_"
and old_craft_grid[5]:get_name() == "default:stick"
and old_craft_grid[8]:get_name() == "default:stick"
and old_craft_grid[4]:is_empty() and old_craft_grid[6]:is_empty()
and old_craft_grid[7]:is_empty() and old_craft_grid[9]:is_empty() then
return generate_tool("pick", player:get_player_name(), {
old_craft_grid[1],
old_craft_grid[2],
old_craft_grid[3],
})
end
elseif width == 2 then
if old_craft_grid[1+startcol]:get_name():sub(1, 11) == "runes:rune_"
and old_craft_grid[2+startcol]:get_name():sub(1, 11) == "runes:rune_"
and old_craft_grid[5+startcol]:get_name() == "default:stick"
and old_craft_grid[8+startcol]:get_name() == "default:stick"
and old_craft_grid[9]:is_empty() then
if old_craft_grid[4+startcol]:is_empty() then
return generate_tool("hoe", player:get_player_name(), {
old_craft_grid[1+startcol],
old_craft_grid[2+startcol],
})
elseif old_craft_grid[4+startcol]:get_name():sub(1, 11) == "runes:rune_" then
return generate_tool("axe", player:get_player_name(), {
old_craft_grid[1+startcol],
old_craft_grid[2+startcol],
old_craft_grid[4+startcol],
})
end
end
elseif width == 1 then
if old_craft_grid[1+startcol]:get_name():sub(1, 11) == "runes:rune_"
and old_craft_grid[7+startcol]:get_name() == "default:stick" then
if old_craft_grid[4+startcol]:get_name() == "default:stick" then
return generate_tool("shovel", player:get_player_name(), {
old_craft_grid[1+startcol],
})
elseif old_craft_grid[4+startcol]:get_name():sub(1, 11) == "runes:rune_" then
return generate_tool("sword", player:get_player_name(), {
old_craft_grid[1+startcol],
old_craft_grid[4+startcol],
})
end
end
end
elseif height == 2 then
if width == 1 then
if old_craft_grid[1+startcol+3*startrow]:get_name():sub(1, 11) == "runes:rune_"
and old_craft_grid[4+startcol+3*startrow]:get_name() == "default:stick" then
return generate_tool("screwdriver", player:get_player_name(), {
old_craft_grid[1+startcol+3*startrow],
})
end
end
elseif height == 1 then
if width == 2 then
if old_craft_grid[1+startcol+3*startrow]:get_name():sub(1, 11) == "runes:rune_"
and old_craft_grid[2+startcol+3*startrow]:get_name():sub(1, 11) == "runes:rune_" then
return generate_tool("fire_starter", player:get_player_name(), {
old_craft_grid[1+startcol+3*startrow],
old_craft_grid[2+startcol+3*startrow],
})
end
end
end
end
-- Originally, calculate_craft_result() was directly registered as the
-- prediction function and it both set the cache appropriately and
-- returned the same value that it cached. As I worked though, I
-- realised that I could easily miss a spot when I was supposed to do
-- both and instead only do one, which would lead to a confusing bug to
-- try to fix. Having calculate_craft_result() ignore the cache and
-- using this wrapper to make sure the returned result is always put in
-- the cache first eliminates the chance of such a bug before it even
-- occurs.
--
-- This wrapper was set up before the need for a second cache was
-- discovered. With two caches to manage, I'm glad only to have to
-- manage them once here in this wrapper instead of repeatedly
-- throughout the calculation function.
minetest.register_craft_predict(function(itemstack, player, old_craft_grid, craft_inv)
local player_name = player:get_player_name()
cache1[player_name] = cache0[player_name]
cache0[player_name] = calculate_craft_result(itemstack, player, old_craft_grid, craft_inv)
return cache0[player_name]
end)
minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv)
-- When setting craft results like this, the ingredients don't get
-- automatically used up like they would in a normal craft. As such, we
-- need to remove them manually.
if cache1[player:get_player_name()] then
for i = 1, 9 do
local stack = craft_inv:get_stack("craft", i)
stack:take_item()
craft_inv:set_stack("craft", i, stack)
end
return cache1[player:get_player_name()]
end
end)