Merge branch 'mcl_explosions'
[MineClone/MineClone2.git] / mods / ITEMS / mcl_banners / init.lua
blob57adbed863b6f506fd7315d5cf65e39ca23eb199
1 local S = minetest.get_translator("mcl_banners")
2 local N = function(s) return s end
4 local node_sounds
5 if minetest.get_modpath("mcl_sounds") then
6 node_sounds = mcl_sounds.node_sound_wood_defaults()
7 end
9 -- Helper function
10 local function round(num, idp)
11 local mult = 10^(idp or 0)
12 return math.floor(num * mult + 0.5) / mult
13 end
15 mcl_banners = {}
17 mcl_banners.colors = {
18 -- Format:
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
41 end
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
55 end
57 local on_dig_banner = function(pos, node, digger)
58 -- Check protection
59 local name = digger:get_player_name()
60 if minetest.is_protected(pos, name) then
61 minetest.record_protection_violation(pos, name)
62 return
63 end
64 -- Drop item
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)
69 else
70 minetest.handle_node_drops(pos, {"mcl_banners:banner_item_white"}, digger)
71 end
72 -- Remove node
73 minetest.remove_node(pos)
74 end
76 local on_destruct_banner = function(pos, hanging)
77 local offset, nodename
78 if hanging then
79 offset = hanging_banner_entity_offset
80 nodename = "mcl_banners:hanging_banner"
81 else
82 offset = standing_banner_entity_offset
83 nodename = "mcl_banners:standing_banner"
84 end
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
91 v:remove()
92 end
93 end
94 end
96 local on_destruct_standing_banner = function(pos)
97 return on_destruct_banner(pos, false)
98 end
100 local on_destruct_hanging_banner = function(pos)
101 return on_destruct_banner(pos, true)
104 local make_banner_texture = function(base_color, layers)
105 local colorize
106 if mcl_banners.colors[base_color] then
107 colorize = mcl_banners.colors[base_color][4]
109 if colorize then
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
114 if layers then
115 local finished_banner = base
116 for l=1, #layers do
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 }
128 return { base }
129 else
130 return { "mcl_banners_banner_base.png" }
134 local spawn_banner_entity = function(pos, hanging, itemstack)
135 local banner
136 if hanging then
137 banner = minetest.add_entity(pos, "mcl_banners:hanging_banner")
138 else
139 banner = minetest.add_entity(pos, "mcl_banners:standing_banner")
141 if banner == nil then
142 return banner
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")
155 return banner
158 local respawn_banner_entity = function(pos, node, force)
159 local hanging = node.name == "mcl_banners:hanging_banner"
160 local offset
161 if hanging then
162 offset = hanging_banner_entity_offset
163 else
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
172 if force then
173 v:remove()
174 else
175 return
179 -- Spawn new entity
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)
184 -- Set rotation
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)
190 -- Banner nodes.
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."),
203 walkable = false,
204 is_ground_content = false,
205 paramtype = "light",
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.
211 node_box = {
212 type = "fixed",
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 },
223 stack_max = 16,
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)
231 end,
232 _mcl_hardness = 1,
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")
238 rot = (rot - 1) % 16
239 meta:set_int("rotation_level", rot)
240 respawn_banner_entity(pos, node, true)
241 return true
242 else
243 return false
245 end,
248 -- Hanging banner node
249 minetest.register_node("mcl_banners:hanging_banner", {
250 walkable = false,
251 is_ground_content = false,
252 paramtype = "light",
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" },
259 node_box = {
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 },
267 stack_max = 16,
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)
275 end,
276 _mcl_hardness = 1,
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)
281 node.param2 = r
282 minetest.swap_node(pos, node)
283 local meta = minetest.get_meta(pos)
284 local rot = 0
285 if node.param2 == 2 then
286 rot = 12
287 elseif node.param2 == 3 then
288 rot = 4
289 elseif node.param2 == 4 then
290 rot = 0
291 elseif node.param2 == 5 then
292 rot = 8
294 meta:set_int("rotation_level", rot)
295 respawn_banner_entity(pos, node, true)
296 return true
297 else
298 return false
300 end,
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
310 local inv
311 if colorize then
312 inv = "mcl_banners_item_base.png^(mcl_banners_item_overlay.png^[colorize:"..colorize..")"
313 else
314 inv = "mcl_banners_item_base.png^mcl_banners_item_overlay.png"
317 -- Banner items.
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, {
322 description = desc,
323 _tt_help = S("Paintable decoration"),
324 _doc_items_create_entry = false,
325 inventory_image = inv,
326 wield_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 },
330 stack_max = 16,
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.
345 local new_node
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"
359 if new_node then
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
364 table.remove(layers)
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
369 if mname == "" then
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)
381 return itemstack
386 -- Place the node!
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)
391 if not success then
392 -- Forbidden on ceiling
393 if pointed_thing.under.y ~= pointed_thing.above.y then
394 return itemstack
396 _, success = minetest.item_place_node(ItemStack("mcl_banners:hanging_banner"), placer, pointed_thing)
397 if not success then
398 return itemstack
400 hanging = true
402 local place_pos
403 if minetest.registered_nodes[node_under.name].buildable_to then
404 place_pos = under
405 else
406 place_pos = above
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!")
411 return itemstack
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)
420 -- Spawn entity
421 local entity_place_pos
422 if hanging then
423 entity_place_pos = vector.add(place_pos, hanging_banner_entity_offset)
424 else
425 entity_place_pos = vector.add(place_pos, standing_banner_entity_offset)
427 local banner_entity = spawn_banner_entity(entity_place_pos, hanging, itemstack)
428 -- Set rotation
429 local final_yaw, rotation_level
430 if hanging then
431 local pdir = vector.direction(pointed_thing.under, pointed_thing.above)
432 final_yaw = minetest.dir_to_yaw(pdir)
433 if pdir.x > 0 then
434 rotation_level = 4
435 elseif pdir.z > 0 then
436 rotation_level = 8
437 elseif pdir.x < 0 then
438 rotation_level = 12
439 else
440 rotation_level = 0
442 else
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
448 rotation_level = 0
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)
463 return itemstack
464 end,
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
470 return nil
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)
476 return newdesc
477 end,
480 if minetest.get_modpath("mcl_core") and minetest.get_modpath("mcl_wool") then
481 minetest.register_craft({
482 output = itemstring,
483 recipe = {
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")
503 -- Banner entities.
504 local entity_standing = {
505 physical = false,
506 collide_with_objects = false,
507 visual = "mesh",
508 mesh = "amc_banner.b3d",
509 visual_size = { x=2.499, y=2.499 },
510 textures = make_banner_texture(),
511 pointable = false,
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)
522 end,
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})
536 end,
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)
543 if base_color then
544 self._base_color = base_color
546 if layers then
547 self._layers = layers
549 self.object:set_properties({textures = make_banner_texture(self._base_color, self._layers)})
550 end,
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)
566 end,
569 minetest.register_craft({
570 type = "fuel",
571 recipe = "group:banner",
572 burntime = 15,