1 local S
= minetest
.get_translator("mcl_banners")
2 local N
= function(s
) return s
end
5 if minetest
.get_modpath("mcl_sounds") then
6 node_sounds
= mcl_sounds
.node_sound_wood_defaults()
10 local function round(num
, idp
)
11 local mult
= 10^
(idp
or 0)
12 return math
.floor(num
* mult
+ 0.5) / mult
17 mcl_banners
.colors
= {
19 -- [ID] = { banner description, wool, unified dyes color group, overlay color, dye, color name for emblazonings }
20 ["unicolor_white"] = {"white", S("White Banner"), "mcl_wool:white", "#FFFFFF", "mcl_dye:white", N("White") },
21 ["unicolor_darkgrey"] = {"grey", S("Grey Banner"), "mcl_wool:grey", "#303030", "mcl_dye:dark_grey", N("Grey") },
22 ["unicolor_grey"] = {"silver", S("Light Grey Banner"), "mcl_wool:silver", "#5B5B5B", "mcl_dye:grey", N("Light Grey") },
23 ["unicolor_black"] = {"black", S("Black Banner"), "mcl_wool:black", "#000000", "mcl_dye:black", N("Black") },
24 ["unicolor_red"] = {"red", S("Red Banner"), "mcl_wool:red", "#BC0000", "mcl_dye:red", N("Red") },
25 ["unicolor_yellow"] = {"yellow", S("Yellow Banner"), "mcl_wool:yellow", "#E6CD00", "mcl_dye:yellow", N("Yellow") },
26 ["unicolor_dark_green"] = {"green", S("Green Banner"), "mcl_wool:green", "#006000", "mcl_dye:dark_green", N("Green") },
27 ["unicolor_cyan"] = {"cyan", S("Cyan Banner"), "mcl_wool:cyan", "#00ACAC", "mcl_dye:cyan", N("Cyan") },
28 ["unicolor_blue"] = {"blue", S("Blue Banner"), "mcl_wool:blue", "#0000AC", "mcl_dye:blue", N("Blue") },
29 ["unicolor_red_violet"] = {"magenta", S("Magenta Banner"), "mcl_wool:magenta", "#AC007C", "mcl_dye:magenta", N("Magenta")},
30 ["unicolor_orange"] = {"orange", S("Orange Banner"), "mcl_wool:orange", "#E67300", "mcl_dye:orange", N("Orange") },
31 ["unicolor_violet"] = {"purple", S("Purple Banner"), "mcl_wool:purple", "#6400AC", "mcl_dye:violet", N("Violet") },
32 ["unicolor_brown"] = {"brown", S("Brown Banner"), "mcl_wool:brown", "#603000", "mcl_dye:brown", N("Brown") },
33 ["unicolor_pink"] = {"pink", S("Pink Banner"), "mcl_wool:pink", "#DE557C", "mcl_dye:pink", N("Pink") },
34 ["unicolor_lime"] = {"lime", S("Lime Banner"), "mcl_wool:lime", "#30AC00", "mcl_dye:green", N("Lime") },
35 ["unicolor_light_blue"] = {"light_blue", S("Light Blue Banner"), "mcl_wool:light_blue", "#4040CF", "mcl_dye:lightblue", N("Light Blue") },
38 local colors_reverse
= {}
39 for k
,v
in pairs(mcl_banners
.colors
) do
40 colors_reverse
["mcl_banners:banner_item_"..v
[1]]
= k
43 -- Add pattern/emblazoning crafting recipes
44 dofile(minetest
.get_modpath("mcl_banners").."/patterncraft.lua")
46 -- Overlay ratios (0-255)
47 local base_color_ratio
= 224
48 local layer_ratio
= 255
50 local standing_banner_entity_offset
= { x
=0, y
=-0.499, z
=0 }
51 local hanging_banner_entity_offset
= { x
=0, y
=-1.7, z
=0 }
53 local rotation_level_to_yaw
= function(rotation_level
)
54 return (rotation_level
* (math
.pi
/8)) + math
.pi
57 local on_dig_banner
= function(pos
, node
, digger
)
59 local name
= digger
:get_player_name()
60 if minetest
.is_protected(pos
, name
) then
61 minetest
.record_protection_violation(pos
, name
)
65 local meta
= minetest
.get_meta(pos
)
66 local item
= meta
:get_inventory():get_stack("banner", 1)
67 if not item
:is_empty() then
68 minetest
.handle_node_drops(pos
, {item
:to_string()}, digger
)
70 minetest
.handle_node_drops(pos
, {"mcl_banners:banner_item_white"}, digger
)
73 minetest
.remove_node(pos
)
76 local on_destruct_banner
= function(pos
, hanging
)
77 local offset
, nodename
79 offset
= hanging_banner_entity_offset
80 nodename
= "mcl_banners:hanging_banner"
82 offset
= standing_banner_entity_offset
83 nodename
= "mcl_banners:standing_banner"
85 -- Find this node's banner entity and remove it
86 local checkpos
= vector
.add(pos
, offset
)
87 local objects
= minetest
.get_objects_inside_radius(checkpos
, 0.5)
88 for _
, v
in ipairs(objects
) do
89 local ent
= v
:get_luaentity()
90 if ent
and ent
.name
== nodename
then
96 local on_destruct_standing_banner
= function(pos
)
97 return on_destruct_banner(pos
, false)
100 local on_destruct_hanging_banner
= function(pos
)
101 return on_destruct_banner(pos
, true)
104 local make_banner_texture
= function(base_color
, layers
)
106 if mcl_banners
.colors
[base_color
] then
107 colorize
= mcl_banners
.colors
[base_color
][4]
110 -- Base texture with base color
111 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)"
113 -- Optional pattern layers
115 local finished_banner
= base
117 local layerinfo
= layers
[l
]
118 local pattern
= "mcl_banners_" .. layerinfo
.pattern
.. ".png"
119 local color
= mcl_banners
.colors
[layerinfo
.color
][4]
121 -- Generate layer texture
122 local layer
= "(("..pattern
.."^[colorize:"..color
..":"..layer_ratio
..")^[mask:"..pattern
..")"
124 finished_banner
= finished_banner
.. "^" .. layer
126 return { finished_banner
}
130 return { "mcl_banners_banner_base.png" }
134 local spawn_banner_entity
= function(pos
, hanging
, itemstack
)
137 banner
= minetest
.add_entity(pos
, "mcl_banners:hanging_banner")
139 banner
= minetest
.add_entity(pos
, "mcl_banners:standing_banner")
141 if banner
== nil then
144 local imeta
= itemstack
:get_meta()
145 local layers_raw
= imeta
:get_string("layers")
146 local layers
= minetest
.deserialize(layers_raw
)
147 local colorid
= colors_reverse
[itemstack
:get_name()]
148 banner
:get_luaentity():_set_textures(colorid
, layers
)
149 local mname
= imeta
:get_string("name")
150 if mname
~= nil and mname
~= "" then
151 banner
:get_luaentity()._item_name
= mname
152 banner
:get_luaentity()._item_description
= imeta
:get_string("description")
158 local respawn_banner_entity
= function(pos
, node
, force
)
159 local hanging
= node
.name
== "mcl_banners:hanging_banner"
162 offset
= hanging_banner_entity_offset
164 offset
= standing_banner_entity_offset
166 -- Check if a banner entity already exists
167 local bpos
= vector
.add(pos
, offset
)
168 local objects
= minetest
.get_objects_inside_radius(bpos
, 0.5)
169 for _
, v
in ipairs(objects
) do
170 local ent
= v
:get_luaentity()
171 if ent
and (ent
.name
== "mcl_banners:standing_banner" or ent
.name
== "mcl_banners:hanging_banner") then
180 local meta
= minetest
.get_meta(pos
)
181 local banner_item
= meta
:get_inventory():get_stack("banner", 1)
182 local banner_entity
= spawn_banner_entity(bpos
, hanging
, banner_item
)
185 local rotation_level
= meta
:get_int("rotation_level")
186 local final_yaw
= rotation_level_to_yaw(rotation_level
)
187 banner_entity
:set_yaw(final_yaw
)
191 -- These are an invisible nodes which are only used to destroy the banner entity.
192 -- All the important banner information (such as color) is stored in the entity.
193 -- It is used only used internally.
195 -- Standing banner node
196 -- This one is also used for the help entry to avoid spamming the help with 16 entries.
197 minetest
.register_node("mcl_banners:standing_banner", {
198 _doc_items_entry_name
= S("Banner"),
199 _doc_items_image
= "mcl_banners_item_base.png^mcl_banners_item_overlay.png",
200 _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."),
201 _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"..
202 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."),
204 is_ground_content
= false,
206 sunlight_propagates
= true,
207 drawtype
= "nodebox",
208 -- Nodebox is drawn as fallback when the entity is missing, so that the
209 -- banner node is never truly invisible.
210 -- If the entity is drawn, the nodebox disappears within the real banner mesh.
213 fixed
= { -1/32, -0.49, -1/32, 1/32, 1.49, 1/32 },
215 -- This texture is based on the banner base texture
216 tiles
= { "mcl_banners_fallback_wood.png" },
218 inventory_image
= "mcl_banners_item_base.png",
219 wield_image
= "mcl_banners_item_base.png",
221 selection_box
= {type = "fixed", fixed
= {-0.3, -0.5, -0.3, 0.3, 0.5, 0.3} },
222 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 },
224 sounds
= node_sounds
,
225 drop
= "", -- Item drops are handled in entity code
227 on_dig
= on_dig_banner
,
228 on_destruct
= on_destruct_standing_banner
,
229 on_punch
= function(pos
, node
)
230 respawn_banner_entity(pos
, node
)
233 _mcl_blast_resistance
= 1,
234 on_rotate
= function(pos
, node
, user
, mode
, param2
)
235 if mode
== screwdriver
.ROTATE_FACE
then
236 local meta
= minetest
.get_meta(pos
)
237 local rot
= meta
:get_int("rotation_level")
239 meta
:set_int("rotation_level", rot
)
240 respawn_banner_entity(pos
, node
, true)
248 -- Hanging banner node
249 minetest
.register_node("mcl_banners:hanging_banner", {
251 is_ground_content
= false,
253 paramtype2
= "wallmounted",
254 sunlight_propagates
= true,
255 drawtype
= "nodebox",
256 inventory_image
= "mcl_banners_item_base.png",
257 wield_image
= "mcl_banners_item_base.png",
258 tiles
= { "mcl_banners_fallback_wood.png" },
260 type = "wallmounted",
261 wall_side
= { -0.49, 0.41, -0.49, -0.41, 0.49, 0.49 },
262 wall_top
= { -0.49, 0.41, -0.49, -0.41, 0.49, 0.49 },
263 wall_bottom
= { -0.49, -0.49, -0.49, -0.41, -0.41, 0.49 },
265 selection_box
= {type = "wallmounted", wall_side
= {-0.5, -0.5, -0.5, -4/16, 0.5, 0.5} },
266 groups
= {axey
=1,handy
=1, attached_node
= 1, not_in_creative_inventory
= 1, not_in_craft_guide
= 1, material_wood
=1, flammable
=-1 },
268 sounds
= node_sounds
,
269 drop
= "", -- Item drops are handled in entity code
271 on_dig
= on_dig_banner
,
272 on_destruct
= on_destruct_hanging_banner
,
273 on_punch
= function(pos
, node
)
274 respawn_banner_entity(pos
, node
)
277 _mcl_blast_resistance
= 1,
278 on_rotate
= function(pos
, node
, user
, mode
, param2
)
279 if mode
== screwdriver
.ROTATE_FACE
then
280 local r
= screwdriver
.rotate
.wallmounted(pos
, node
, mode
)
282 minetest
.swap_node(pos
, node
)
283 local meta
= minetest
.get_meta(pos
)
285 if node
.param2
== 2 then
287 elseif node
.param2
== 3 then
289 elseif node
.param2
== 4 then
291 elseif node
.param2
== 5 then
294 meta
:set_int("rotation_level", rot
)
295 respawn_banner_entity(pos
, node
, true)
303 for colorid
, colortab
in pairs(mcl_banners
.colors
) do
304 local itemid
= colortab
[1]
305 local desc
= colortab
[2]
306 local wool
= colortab
[3]
307 local colorize
= colortab
[4]
309 local itemstring
= "mcl_banners:banner_item_"..itemid
312 inv
= "mcl_banners_item_base.png^(mcl_banners_item_overlay.png^[colorize:"..colorize
..")"
314 inv
= "mcl_banners_item_base.png^mcl_banners_item_overlay.png"
318 -- This is the player-visible banner item. It comes in 16 base colors.
319 -- The multiple items are really only needed for the different item images.
320 -- TODO: Combine the items into only 1 item.
321 minetest
.register_craftitem(itemstring
, {
323 _tt_help
= S("Paintable decoration"),
324 _doc_items_create_entry
= false,
325 inventory_image
= inv
,
327 -- Banner group groups together the banner items, but not the nodes.
328 -- Used for crafting.
329 groups
= { banner
= 1, deco_block
= 1, flammable
= -1 },
332 on_place
= function(itemstack
, placer
, pointed_thing
)
333 local above
= pointed_thing
.above
334 local under
= pointed_thing
.under
336 local node_under
= minetest
.get_node(under
)
337 if placer
and not placer
:get_player_control().sneak
then
338 -- Use pointed node's on_rightclick function first, if present
339 if minetest
.registered_nodes
[node_under
.name
] and minetest
.registered_nodes
[node_under
.name
].on_rightclick
then
340 return minetest
.registered_nodes
[node_under
.name
].on_rightclick(under
, node_under
, placer
, itemstack
) or itemstack
343 if minetest
.get_modpath("mcl_cauldrons") then
344 -- Use banner on cauldron to remove the top-most layer. This reduces the water level by 1.
346 if node_under
.name
== "mcl_cauldrons:cauldron_3" then
347 new_node
= "mcl_cauldrons:cauldron_2"
348 elseif node_under
.name
== "mcl_cauldrons:cauldron_2" then
349 new_node
= "mcl_cauldrons:cauldron_1"
350 elseif node_under
.name
== "mcl_cauldrons:cauldron_1" then
351 new_node
= "mcl_cauldrons:cauldron"
352 elseif node_under
.name
== "mcl_cauldrons:cauldron_3r" then
353 new_node
= "mcl_cauldrons:cauldron_2r"
354 elseif node_under
.name
== "mcl_cauldrons:cauldron_2r" then
355 new_node
= "mcl_cauldrons:cauldron_1r"
356 elseif node_under
.name
== "mcl_cauldrons:cauldron_1r" then
357 new_node
= "mcl_cauldrons:cauldron"
360 local imeta
= itemstack
:get_meta()
361 local layers_raw
= imeta
:get_string("layers")
362 local layers
= minetest
.deserialize(layers_raw
)
363 if type(layers
) == "table" and #layers
> 0 then
365 imeta
:set_string("layers", minetest
.serialize(layers
))
366 local newdesc
= mcl_banners
.make_advanced_banner_description(itemstack
:get_definition().description
, layers
)
367 local mname
= imeta
:get_string("name")
368 -- Don't change description if item has a name
370 imeta
:set_string("description", newdesc
)
374 -- Washing off reduces the water level by 1.
375 -- (It is possible to waste water if the banner had 0 layers.)
376 minetest
.set_node(pointed_thing
.under
, {name
=new_node
})
378 -- Play sound (from mcl_potions mod)
379 minetest
.sound_play("mcl_potions_bottle_pour", {pos
=pointed_thing
.under
, gain
=0.5, max_hear_range
=16}, true)
387 local hanging
= false
389 -- Standing or hanging banner. The placement rules are enforced by the node definitions
390 local _
, success
= minetest
.item_place_node(ItemStack("mcl_banners:standing_banner"), placer
, pointed_thing
)
392 -- Forbidden on ceiling
393 if pointed_thing
.under
.y
~= pointed_thing
.above
.y
then
396 _
, success
= minetest
.item_place_node(ItemStack("mcl_banners:hanging_banner"), placer
, pointed_thing
)
403 if minetest
.registered_nodes
[node_under
.name
].buildable_to
then
408 local bnode
= minetest
.get_node(place_pos
)
409 if bnode
.name
~= "mcl_banners:standing_banner" and bnode
.name
~= "mcl_banners:hanging_banner" then
410 minetest
.log("error", "[mcl_banners] The placed banner node is not what the mod expected!")
413 local meta
= minetest
.get_meta(place_pos
)
414 local inv
= meta
:get_inventory()
415 inv
:set_size("banner", 1)
416 local store_stack
= ItemStack(itemstack
)
417 store_stack
:set_count(1)
418 inv
:set_stack("banner", 1, store_stack
)
421 local entity_place_pos
423 entity_place_pos
= vector
.add(place_pos
, hanging_banner_entity_offset
)
425 entity_place_pos
= vector
.add(place_pos
, standing_banner_entity_offset
)
427 local banner_entity
= spawn_banner_entity(entity_place_pos
, hanging
, itemstack
)
429 local final_yaw
, rotation_level
431 local pdir
= vector
.direction(pointed_thing
.under
, pointed_thing
.above
)
432 final_yaw
= minetest
.dir_to_yaw(pdir
)
435 elseif pdir
.z
> 0 then
437 elseif pdir
.x
< 0 then
443 -- Determine the rotation based on player's yaw
444 local yaw
= placer
:get_look_horizontal()
445 -- Select one of 16 possible rotations (0-15)
446 rotation_level
= round((yaw
/ (math
.pi
*2)) * 16)
447 if rotation_level
>= 16 then
450 final_yaw
= rotation_level_to_yaw(rotation_level
)
452 meta
:set_int("rotation_level", rotation_level
)
454 if banner_entity
~= nil then
455 banner_entity
:set_yaw(final_yaw
)
458 if not minetest
.settings
:get_bool("creative_mode") then
459 itemstack
:take_item()
461 minetest
.sound_play({name
="default_place_node_hard", gain
=1.0}, {pos
= place_pos
}, true)
466 _mcl_generate_description
= function(itemstack
)
467 local meta
= itemstack
:get_meta()
468 local layers_raw
= meta
:get_string("layers")
469 if not layers_raw
then
472 local layers
= minetest
.deserialize(layers_raw
)
473 local desc
= itemstack
:get_definition().description
474 local newdesc
= mcl_banners
.make_advanced_banner_description(desc
, layers
)
475 meta
:set_string("description", newdesc
)
480 if minetest
.get_modpath("mcl_core") and minetest
.get_modpath("mcl_wool") then
481 minetest
.register_craft({
484 { wool
, wool
, wool
},
485 { wool
, wool
, wool
},
486 { "", "mcl_core:stick", "" },
491 if minetest
.get_modpath("doc") then
492 -- Add item to node alias
493 doc
.add_entry_alias("nodes", "mcl_banners:standing_banner", "craftitems", itemstring
)
497 if minetest
.get_modpath("doc") then
498 -- Add item to node alias
499 doc
.add_entry_alias("nodes", "mcl_banners:standing_banner", "nodes", "mcl_banners:hanging_banner")
504 local entity_standing
= {
506 collide_with_objects
= false,
508 mesh
= "amc_banner.b3d",
509 visual_size
= { x
=2.499, y
=2.499 },
510 textures
= make_banner_texture(),
513 _base_color
= nil, -- base color of banner
514 _layers
= nil, -- table of layers painted over the base color.
515 -- This is a table of tables with each table having the following fields:
516 -- color: layer color ID (see colors table above)
517 -- pattern: name of pattern (see list above)
519 get_staticdata
= function(self
)
520 local out
= { _base_color
= self
._base_color
, _layers
= self
._layers
, _name
= self
._name
}
521 return minetest
.serialize(out
)
523 on_activate
= function(self
, staticdata
)
524 if staticdata
and staticdata
~= "" then
525 local inp
= minetest
.deserialize(staticdata
)
526 self
._base_color
= inp
._base_color
527 self
._layers
= inp
._layers
528 self
._name
= inp
._name
529 self
.object
:set_properties({
530 textures
= make_banner_texture(self
._base_color
, self
._layers
),
533 -- Make banner slowly swing
534 self
.object
:set_animation({x
=0, y
=80}, 25)
535 self
.object
:set_armor_groups({immortal
=1})
538 -- Set the banner textures. This function can be used by external mods.
539 -- Meaning of parameters:
540 -- * self: Lua entity reference to entity.
541 -- * other parameters: Same meaning as in make_banner_texture
542 _set_textures
= function(self
, base_color
, layers
)
544 self
._base_color
= base_color
547 self
._layers
= layers
549 self
.object
:set_properties({textures
= make_banner_texture(self
._base_color
, self
._layers
)})
552 minetest
.register_entity("mcl_banners:standing_banner", entity_standing
)
554 local entity_hanging
= table.copy(entity_standing
)
555 entity_hanging
.mesh
= "amc_banner_hanging.b3d"
556 minetest
.register_entity("mcl_banners:hanging_banner", entity_hanging
)
558 -- FIXME: Prevent entity destruction by /clearobjects
559 minetest
.register_lbm({
560 label
= "Respawn banner entities",
561 name
= "mcl_banners:respawn_entities",
562 run_at_every_load
= true,
563 nodenames
= {"mcl_banners:standing_banner", "mcl_banners:hanging_banner"},
564 action
= function(pos
, node
)
565 respawn_banner_entity(pos
, node
)
569 minetest
.register_craft({
571 recipe
= "group:banner",