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 .. "))" local mask = "(([combine:20x40:-2,-2=" .. pattern .. "^[resize:16x24" .. "))" function escape(text) return text:gsub("%^", "\\%^"):gsub(":", "\\:") -- :gsub("%(", "\\%("):gsub("%)", "\\%)") end local layer_masked = layer .. "[mask:" .. escape(mask) finished_banner = "[combine:32x32:0,0=" .. escape(base) .. ":8,4=" .. escape(layer_masked) 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, })