531 lines
21 KiB
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)
|