Mineclonia/mods/ITEMS/mcl_banners/init.lua
Nils Dagsson Moskopp ee9f49b86e
Remove wrong preview banner crafting recipes
In commit ac5f115f83, preview banners were introduced and given
crafting recipes. Those crafting recipes were the same as for banners without a pattern. That
change made banners without patterns uncraftable and preview banners craftable instead – this
patch makes banners without patterns craftable again and preview banners uncraftable.
2021-05-02 13:01:00 +02:00

666 lines
23 KiB
Lua

local S = minetest.get_translator("mcl_banners")
local N = function(s) return s end
local node_sounds
if minetest.get_modpath("mcl_sounds") then
node_sounds = mcl_sounds.node_sound_wood_defaults()
end
-- Helper function
local function round(num, idp)
local mult = 10^(idp or 0)
return math.floor(num * mult + 0.5) / mult
end
mcl_banners = {}
mcl_banners.colors = {
-- Format:
-- [ID] = { banner description, wool, unified dyes color group, overlay color, dye, color name for emblazonings }
["unicolor_white"] = {"white", S("White Banner"), "mcl_wool:white", "#FFFFFF", "mcl_dye:white", N("White") },
["unicolor_darkgrey"] = {"grey", S("Grey Banner"), "mcl_wool:grey", "#303030", "mcl_dye:dark_grey", N("Grey") },
["unicolor_grey"] = {"silver", S("Light Grey Banner"), "mcl_wool:silver", "#5B5B5B", "mcl_dye:grey", N("Light Grey") },
["unicolor_black"] = {"black", S("Black Banner"), "mcl_wool:black", "#000000", "mcl_dye:black", N("Black") },
["unicolor_red"] = {"red", S("Red Banner"), "mcl_wool:red", "#BC0000", "mcl_dye:red", N("Red") },
["unicolor_yellow"] = {"yellow", S("Yellow Banner"), "mcl_wool:yellow", "#E6CD00", "mcl_dye:yellow", N("Yellow") },
["unicolor_dark_green"] = {"green", S("Green Banner"), "mcl_wool:green", "#006000", "mcl_dye:dark_green", N("Green") },
["unicolor_cyan"] = {"cyan", S("Cyan Banner"), "mcl_wool:cyan", "#00ACAC", "mcl_dye:cyan", N("Cyan") },
["unicolor_blue"] = {"blue", S("Blue Banner"), "mcl_wool:blue", "#0000AC", "mcl_dye:blue", N("Blue") },
["unicolor_red_violet"] = {"magenta", S("Magenta Banner"), "mcl_wool:magenta", "#AC007C", "mcl_dye:magenta", N("Magenta")},
["unicolor_orange"] = {"orange", S("Orange Banner"), "mcl_wool:orange", "#E67300", "mcl_dye:orange", N("Orange") },
["unicolor_violet"] = {"purple", S("Purple Banner"), "mcl_wool:purple", "#6400AC", "mcl_dye:violet", N("Violet") },
["unicolor_brown"] = {"brown", S("Brown Banner"), "mcl_wool:brown", "#603000", "mcl_dye:brown", N("Brown") },
["unicolor_pink"] = {"pink", S("Pink Banner"), "mcl_wool:pink", "#DE557C", "mcl_dye:pink", N("Pink") },
["unicolor_lime"] = {"lime", S("Lime Banner"), "mcl_wool:lime", "#30AC00", "mcl_dye:green", N("Lime") },
["unicolor_light_blue"] = {"light_blue", S("Light Blue Banner"), "mcl_wool:light_blue", "#4040CF", "mcl_dye:lightblue", N("Light Blue") },
}
local pattern_names = {
"",
"border",
"bricks",
"circle",
"creeper",
"cross",
"curly_border",
"diagonal_up_left",
"diagonal_up_right",
"diagonal_right",
"diagonal_left",
"flower",
"gradient",
"gradient_up",
"half_horizontal_bottom",
"half_horizontal",
"half_vertical",
"half_vertical_right",
"thing",
"rhombus",
"skull",
"small_stripes",
"square_bottom_left",
"square_bottom_right",
"square_top_left",
"square_top_right",
"straight_cross",
"stripe_bottom",
"stripe_center",
"stripe_downleft",
"stripe_downright",
"stripe_left",
"stripe_middle",
"stripe_right",
"stripe_top",
"triangle_bottom",
"triangle_top",
"triangles_bottom",
"triangles_top",
}
local colors_reverse = {}
for k,v in pairs(mcl_banners.colors) do
colors_reverse["mcl_banners:banner_item_"..v[1]] = k
end
-- Add pattern/emblazoning crafting recipes
dofile(minetest.get_modpath("mcl_banners").."/patterncraft.lua")
-- Overlay ratios (0-255)
local base_color_ratio = 224
local layer_ratio = 255
local standing_banner_entity_offset = { x=0, y=-0.499, z=0 }
local hanging_banner_entity_offset = { x=0, y=-1.7, z=0 }
local rotation_level_to_yaw = function(rotation_level)
return (rotation_level * (math.pi/8)) + math.pi
end
local on_dig_banner = function(pos, node, digger)
-- Check protection
local name = digger:get_player_name()
if minetest.is_protected(pos, name) then
minetest.record_protection_violation(pos, name)
return
end
-- Drop item
local meta = minetest.get_meta(pos)
local item = meta:get_inventory():get_stack("banner", 1)
if not item:is_empty() then
minetest.handle_node_drops(pos, {item:to_string()}, digger)
else
minetest.handle_node_drops(pos, {"mcl_banners:banner_item_white"}, digger)
end
-- Remove node
minetest.remove_node(pos)
end
local on_destruct_banner = function(pos, hanging)
local offset, nodename
if hanging then
offset = hanging_banner_entity_offset
nodename = "mcl_banners:hanging_banner"
else
offset = standing_banner_entity_offset
nodename = "mcl_banners:standing_banner"
end
-- Find this node's banner entity and remove it
local checkpos = vector.add(pos, offset)
local objects = minetest.get_objects_inside_radius(checkpos, 0.5)
for _, v in ipairs(objects) do
local ent = v:get_luaentity()
if ent and ent.name == nodename then
v:remove()
end
end
end
local on_destruct_standing_banner = function(pos)
return on_destruct_banner(pos, false)
end
local on_destruct_hanging_banner = function(pos)
return on_destruct_banner(pos, true)
end
local make_banner_texture = function(base_color, layers)
local colorize
if mcl_banners.colors[base_color] then
colorize = mcl_banners.colors[base_color][4]
end
if colorize then
-- Base texture with base color
local base = "(mcl_banners_banner_base.png^[mask:mcl_banners_base_inverted.png)^((mcl_banners_banner_base.png^[colorize:"..colorize..":"..base_color_ratio..")^[mask:mcl_banners_base.png)"
-- Optional pattern layers
if layers then
local finished_banner = base
for l=1, #layers do
local layerinfo = layers[l]
local pattern = "mcl_banners_" .. layerinfo.pattern .. ".png"
local color = mcl_banners.colors[layerinfo.color][4]
-- Generate layer texture
local layer = "(("..pattern.."^[colorize:"..color..":"..layer_ratio..")^[mask:"..pattern..")"
finished_banner = finished_banner .. "^" .. layer
end
return { finished_banner }
end
return { base }
else
return { "mcl_banners_banner_base.png" }
end
end
local spawn_banner_entity = function(pos, hanging, itemstack)
local banner
if hanging then
banner = minetest.add_entity(pos, "mcl_banners:hanging_banner")
else
banner = minetest.add_entity(pos, "mcl_banners:standing_banner")
end
if banner == nil then
return banner
end
local imeta = itemstack:get_meta()
local layers_raw = imeta:get_string("layers")
local layers = minetest.deserialize(layers_raw)
local colorid = colors_reverse[itemstack:get_name()]
banner:get_luaentity():_set_textures(colorid, layers)
local mname = imeta:get_string("name")
if mname ~= nil and mname ~= "" then
banner:get_luaentity()._item_name = mname
banner:get_luaentity()._item_description = imeta:get_string("description")
end
return banner
end
local respawn_banner_entity = function(pos, node, force)
local hanging = node.name == "mcl_banners:hanging_banner"
local offset
if hanging then
offset = hanging_banner_entity_offset
else
offset = standing_banner_entity_offset
end
-- Check if a banner entity already exists
local bpos = vector.add(pos, offset)
local objects = minetest.get_objects_inside_radius(bpos, 0.5)
for _, v in ipairs(objects) do
local ent = v:get_luaentity()
if ent and (ent.name == "mcl_banners:standing_banner" or ent.name == "mcl_banners:hanging_banner") then
if force then
v:remove()
else
return
end
end
end
-- Spawn new entity
local meta = minetest.get_meta(pos)
local banner_item = meta:get_inventory():get_stack("banner", 1)
local banner_entity = spawn_banner_entity(bpos, hanging, banner_item)
-- Set rotation
local rotation_level = meta:get_int("rotation_level")
local final_yaw = rotation_level_to_yaw(rotation_level)
banner_entity:set_yaw(final_yaw)
end
-- Banner nodes.
-- These are an invisible nodes which are only used to destroy the banner entity.
-- All the important banner information (such as color) is stored in the entity.
-- It is used only used internally.
-- Standing banner node
-- This one is also used for the help entry to avoid spamming the help with 16 entries.
minetest.register_node("mcl_banners:standing_banner", {
_doc_items_entry_name = S("Banner"),
_doc_items_image = "mcl_banners_item_base.png^mcl_banners_item_overlay.png",
_doc_items_longdesc = S("Banners are tall colorful decorative blocks. They can be placed on the floor and at walls. Banners can be emblazoned with a variety of patterns using a lot of dye in crafting."),
_doc_items_usagehelp = S("Use crafting to draw a pattern on top of the banner. Emblazoned banners can be emblazoned again to combine various patterns. You can draw up to 12 layers on a banner that way. If the banner includes a gradient, only 3 layers are possible.").."\n"..
S("You can copy the pattern of a banner by placing two banners of the same color in the crafting grid—one needs to be emblazoned, the other one must be clean. Finally, you can use a banner on a cauldron with water to wash off its top-most layer."),
walkable = false,
is_ground_content = false,
paramtype = "light",
sunlight_propagates = true,
drawtype = "nodebox",
-- Nodebox is drawn as fallback when the entity is missing, so that the
-- banner node is never truly invisible.
-- If the entity is drawn, the nodebox disappears within the real banner mesh.
node_box = {
type = "fixed",
fixed = { -1/32, -0.49, -1/32, 1/32, 1.49, 1/32 },
},
-- This texture is based on the banner base texture
tiles = { "mcl_banners_fallback_wood.png" },
inventory_image = "mcl_banners_item_base.png",
wield_image = "mcl_banners_item_base.png",
selection_box = {type = "fixed", fixed= {-0.3, -0.5, -0.3, 0.3, 0.5, 0.3} },
groups = {axey=1,handy=1, attached_node = 1, not_in_creative_inventory = 1, not_in_craft_guide = 1, material_wood=1, dig_by_piston=1, flammable=-1 },
stack_max = 16,
sounds = node_sounds,
drop = "", -- Item drops are handled in entity code
on_dig = on_dig_banner,
on_destruct = on_destruct_standing_banner,
on_punch = function(pos, node)
respawn_banner_entity(pos, node)
end,
_mcl_hardness = 1,
_mcl_blast_resistance = 1,
on_rotate = function(pos, node, user, mode, param2)
if mode == screwdriver.ROTATE_FACE then
local meta = minetest.get_meta(pos)
local rot = meta:get_int("rotation_level")
rot = (rot - 1) % 16
meta:set_int("rotation_level", rot)
respawn_banner_entity(pos, node, true)
return true
else
return false
end
end,
})
-- Hanging banner node
minetest.register_node("mcl_banners:hanging_banner", {
walkable = false,
is_ground_content = false,
paramtype = "light",
paramtype2 = "wallmounted",
sunlight_propagates = true,
drawtype = "nodebox",
inventory_image = "mcl_banners_item_base.png",
wield_image = "mcl_banners_item_base.png",
tiles = { "mcl_banners_fallback_wood.png" },
node_box = {
type = "wallmounted",
wall_side = { -0.49, 0.41, -0.49, -0.41, 0.49, 0.49 },
wall_top = { -0.49, 0.41, -0.49, -0.41, 0.49, 0.49 },
wall_bottom = { -0.49, -0.49, -0.49, -0.41, -0.41, 0.49 },
},
selection_box = {type = "wallmounted", wall_side = {-0.5, -0.5, -0.5, -4/16, 0.5, 0.5} },
groups = {axey=1,handy=1, attached_node = 1, not_in_creative_inventory = 1, not_in_craft_guide = 1, material_wood=1, flammable=-1 },
stack_max = 16,
sounds = node_sounds,
drop = "", -- Item drops are handled in entity code
on_dig = on_dig_banner,
on_destruct = on_destruct_hanging_banner,
on_punch = function(pos, node)
respawn_banner_entity(pos, node)
end,
_mcl_hardness = 1,
_mcl_blast_resistance = 1,
on_rotate = function(pos, node, user, mode, param2)
if mode == screwdriver.ROTATE_FACE then
local r = screwdriver.rotate.wallmounted(pos, node, mode)
node.param2 = r
minetest.swap_node(pos, node)
local meta = minetest.get_meta(pos)
local rot = 0
if node.param2 == 2 then
rot = 12
elseif node.param2 == 3 then
rot = 4
elseif node.param2 == 4 then
rot = 0
elseif node.param2 == 5 then
rot = 8
end
meta:set_int("rotation_level", rot)
respawn_banner_entity(pos, node, true)
return true
else
return false
end
end,
})
-- for pattern_name, pattern in pairs(patterns) do
for colorid, colortab in pairs(mcl_banners.colors) do
for i, pattern_name in ipairs(pattern_names) do
local itemid = colortab[1]
local desc = colortab[2]
local wool = colortab[3]
local colorize = colortab[4]
local itemstring
if pattern_name == "" then
itemstring = "mcl_banners:banner_item_" .. itemid
else
itemstring = "mcl_banners:banner_preview" .. "_" .. pattern_name .. "_" .. itemid
end
local inv
local base
local finished_banner
if pattern_name == "" then
if colorize then
-- Base texture with base color
base = "mcl_banners_item_base.png^(mcl_banners_item_overlay.png^[colorize:"..colorize..")^[resize:32x32"
else
base = "mcl_banners_item_base.png^mcl_banners_item_overlay.png^[resize:32x32"
end
finished_banner = base
else
-- Banner item preview background
base = "mcl_banners_item_base.png^(mcl_banners_item_overlay.png^[colorize:#CCCCCC)^[resize:32x32"
desc = S("Preview Banner")
local pattern = "mcl_banners_" .. pattern_name .. ".png"
local color = colorize
-- Generate layer texture
-- TODO: The layer texture in the icon is squished
-- weirdly because the width/height aspect ratio of
-- the banner icon is 1:1.5, whereas the aspect ratio
-- of the banner entity is 1:2. A solution would be to
-- redraw the pattern textures as low-resolution pixel
-- art and use that instead.
local layer = "(([combine:20x40:-2,-2="..pattern.."^[resize:16x24^[colorize:"..color..":"..layer_ratio.."))"
function escape(text)
return text:gsub("%^", "\\%^"):gsub(":", "\\:") -- :gsub("%(", "\\%("):gsub("%)", "\\%)")
end
finished_banner = "[combine:32x32:0,0=" .. escape(base) .. ":8,4=" .. escape(layer)
end
inv = finished_banner
-- Banner items.
-- This is the player-visible banner item. It comes in 16 base colors with a lot of patterns.
-- The multiple items are really only needed for the different item images.
-- TODO: Combine the items into only 1 item.
local groups
if pattern_name == "" then
groups = { banner = 1, deco_block = 1, flammable = -1 }
else
groups = { not_in_creative_inventory = 1 }
end
minetest.register_craftitem(itemstring, {
description = desc,
_tt_help = S("Paintable decoration"),
_doc_items_create_entry = false,
inventory_image = inv,
wield_image = inv,
-- Banner group groups together the banner items, but not the nodes.
-- Used for crafting.
groups = groups,
stack_max = 16,
on_place = function(itemstack, placer, pointed_thing)
local above = pointed_thing.above
local under = pointed_thing.under
local node_under = minetest.get_node(under)
if placer and not placer:get_player_control().sneak then
-- Use pointed node's on_rightclick function first, if present
if minetest.registered_nodes[node_under.name] and minetest.registered_nodes[node_under.name].on_rightclick then
return minetest.registered_nodes[node_under.name].on_rightclick(under, node_under, placer, itemstack) or itemstack
end
if minetest.get_modpath("mcl_cauldrons") then
-- Use banner on cauldron to remove the top-most layer. This reduces the water level by 1.
local new_node
if node_under.name == "mcl_cauldrons:cauldron_3" then
new_node = "mcl_cauldrons:cauldron_2"
elseif node_under.name == "mcl_cauldrons:cauldron_2" then
new_node = "mcl_cauldrons:cauldron_1"
elseif node_under.name == "mcl_cauldrons:cauldron_1" then
new_node = "mcl_cauldrons:cauldron"
elseif node_under.name == "mcl_cauldrons:cauldron_3r" then
new_node = "mcl_cauldrons:cauldron_2r"
elseif node_under.name == "mcl_cauldrons:cauldron_2r" then
new_node = "mcl_cauldrons:cauldron_1r"
elseif node_under.name == "mcl_cauldrons:cauldron_1r" then
new_node = "mcl_cauldrons:cauldron"
end
if new_node then
local imeta = itemstack:get_meta()
local layers_raw = imeta:get_string("layers")
local layers = minetest.deserialize(layers_raw)
if type(layers) == "table" and #layers > 0 then
table.remove(layers)
imeta:set_string("layers", minetest.serialize(layers))
local newdesc = mcl_banners.make_advanced_banner_description(itemstack:get_definition().description, layers)
local mname = imeta:get_string("name")
-- Don't change description if item has a name
if mname == "" then
imeta:set_string("description", newdesc)
end
end
-- Washing off reduces the water level by 1.
-- (It is possible to waste water if the banner had 0 layers.)
minetest.set_node(pointed_thing.under, {name=new_node})
-- Play sound (from mcl_potions mod)
minetest.sound_play("mcl_potions_bottle_pour", {pos=pointed_thing.under, gain=0.5, max_hear_range=16}, true)
return itemstack
end
end
end
-- Place the node!
local hanging = false
-- Standing or hanging banner. The placement rules are enforced by the node definitions
local _, success = minetest.item_place_node(ItemStack("mcl_banners:standing_banner"), placer, pointed_thing)
if not success then
-- Forbidden on ceiling
if pointed_thing.under.y ~= pointed_thing.above.y then
return itemstack
end
_, success = minetest.item_place_node(ItemStack("mcl_banners:hanging_banner"), placer, pointed_thing)
if not success then
return itemstack
end
hanging = true
end
local place_pos
if minetest.registered_nodes[node_under.name].buildable_to then
place_pos = under
else
place_pos = above
end
local bnode = minetest.get_node(place_pos)
if bnode.name ~= "mcl_banners:standing_banner" and bnode.name ~= "mcl_banners:hanging_banner" then
minetest.log("error", "[mcl_banners] The placed banner node is not what the mod expected!")
return itemstack
end
local meta = minetest.get_meta(place_pos)
local inv = meta:get_inventory()
inv:set_size("banner", 1)
local store_stack = ItemStack(itemstack)
store_stack:set_count(1)
inv:set_stack("banner", 1, store_stack)
-- Spawn entity
local entity_place_pos
if hanging then
entity_place_pos = vector.add(place_pos, hanging_banner_entity_offset)
else
entity_place_pos = vector.add(place_pos, standing_banner_entity_offset)
end
local banner_entity = spawn_banner_entity(entity_place_pos, hanging, itemstack)
-- Set rotation
local final_yaw, rotation_level
if hanging then
local pdir = vector.direction(pointed_thing.under, pointed_thing.above)
final_yaw = minetest.dir_to_yaw(pdir)
if pdir.x > 0 then
rotation_level = 4
elseif pdir.z > 0 then
rotation_level = 8
elseif pdir.x < 0 then
rotation_level = 12
else
rotation_level = 0
end
else
-- Determine the rotation based on player's yaw
local yaw = placer:get_look_horizontal()
-- Select one of 16 possible rotations (0-15)
rotation_level = round((yaw / (math.pi*2)) * 16)
if rotation_level >= 16 then
rotation_level = 0
end
final_yaw = rotation_level_to_yaw(rotation_level)
end
meta:set_int("rotation_level", rotation_level)
if banner_entity ~= nil then
banner_entity:set_yaw(final_yaw)
end
if not minetest.is_creative_enabled(placer:get_player_name()) then
itemstack:take_item()
end
minetest.sound_play({name="default_place_node_hard", gain=1.0}, {pos = place_pos}, true)
return itemstack
end,
_mcl_generate_description = function(itemstack)
local meta = itemstack:get_meta()
local layers_raw = meta:get_string("layers")
if not layers_raw then
return nil
end
local layers = minetest.deserialize(layers_raw)
local desc = itemstack:get_definition().description
local newdesc = mcl_banners.make_advanced_banner_description(desc, layers)
meta:set_string("description", newdesc)
return newdesc
end,
})
if minetest.get_modpath("mcl_core") and minetest.get_modpath("mcl_wool") and pattern_name == "" then
minetest.register_craft({
output = itemstring,
recipe = {
{ wool, wool, wool },
{ wool, wool, wool },
{ "", "mcl_core:stick", "" },
}
})
end
if minetest.get_modpath("doc") then
-- Add item to node alias
doc.add_entry_alias("nodes", "mcl_banners:standing_banner", "craftitems", itemstring)
end
end
end
if minetest.get_modpath("doc") then
-- Add item to node alias
doc.add_entry_alias("nodes", "mcl_banners:standing_banner", "nodes", "mcl_banners:hanging_banner")
end
-- Banner entities.
local entity_standing = {
physical = false,
collide_with_objects = false,
visual = "mesh",
mesh = "amc_banner.b3d",
visual_size = { x=2.499, y=2.499 },
textures = make_banner_texture(),
pointable = false,
_base_color = nil, -- base color of banner
_layers = nil, -- table of layers painted over the base color.
-- This is a table of tables with each table having the following fields:
-- color: layer color ID (see colors table above)
-- pattern: name of pattern (see list above)
get_staticdata = function(self)
local out = { _base_color = self._base_color, _layers = self._layers, _name = self._name }
return minetest.serialize(out)
end,
on_activate = function(self, staticdata)
if staticdata and staticdata ~= "" then
local inp = minetest.deserialize(staticdata)
self._base_color = inp._base_color
self._layers = inp._layers
self._name = inp._name
self.object:set_properties({
textures = make_banner_texture(self._base_color, self._layers),
})
end
-- Make banner slowly swing
self.object:set_animation({x=0, y=80}, 25)
self.object:set_armor_groups({immortal=1})
end,
-- Set the banner textures. This function can be used by external mods.
-- Meaning of parameters:
-- * self: Lua entity reference to entity.
-- * other parameters: Same meaning as in make_banner_texture
_set_textures = function(self, base_color, layers)
if base_color then
self._base_color = base_color
end
if layers then
self._layers = layers
end
self.object:set_properties({textures = make_banner_texture(self._base_color, self._layers)})
end,
}
minetest.register_entity("mcl_banners:standing_banner", entity_standing)
local entity_hanging = table.copy(entity_standing)
entity_hanging.mesh = "amc_banner_hanging.b3d"
minetest.register_entity("mcl_banners:hanging_banner", entity_hanging)
-- FIXME: Prevent entity destruction by /clearobjects
minetest.register_lbm({
label = "Respawn banner entities",
name = "mcl_banners:respawn_entities",
run_at_every_load = true,
nodenames = {"mcl_banners:standing_banner", "mcl_banners:hanging_banner"},
action = function(pos, node)
respawn_banner_entity(pos, node)
end,
})
minetest.register_craft({
type = "fuel",
recipe = "group:banner",
burntime = 15,
})