2024-01-07 05:13:27 +01:00

630 lines
20 KiB
Lua

local S = minetest.get_translator("rp_paint")
local GRAVITY = tonumber(minetest.settings:get("movement_gravity") or 9.81)
local BRUSH_USES = 550
local BRUSH_PAINTS = 100
local BUCKET_HEIGHT_ABOVE_ZERO = 5/16
local BUCKET_RADIUS = 6/16
local BUCKET_LEVELS = 9 -- number of possible "paint levels" in the paint bucket (not counting the empty state)
local BUCKET_FLOWER_ADD = 3 -- number of paint levels added by a single flower
rp_paint = {}
local COLOR_NAMES = {
S("White"), S("Gray"), S("Black"), S("Red"), S("Orange"), S("Tangerine"), S("Yellow"), S("Lime"), S("Green"), S("Bluegreen"), S("Turquoise"), S("Cyan"), S("Skyblue"), S("Azure Blue"), S("Blue"), S("Violet"), S("Magenta"), S("Redviolet"), S("Hot Pink"),
}
rp_paint.COLOR_COUNT = #COLOR_NAMES
rp_paint.COLOR_WHITE = 1
rp_paint.COLOR_GRAY = 2
rp_paint.COLOR_BLACK = 3
rp_paint.COLOR_RED = 4
rp_paint.COLOR_ORANGE = 5
rp_paint.COLOR_TANGERINE = 6
rp_paint.COLOR_YELLOW = 7
rp_paint.COLOR_LIME = 8
rp_paint.COLOR_GREEN = 9
rp_paint.COLOR_BLUEGREEN = 10
rp_paint.COLOR_TURQUOISE = 11
rp_paint.COLOR_CYAN = 12
rp_paint.COLOR_SKYBLUE = 13
rp_paint.COLOR_AZURE_BLUE = 14
rp_paint.COLOR_BLUE = 15
rp_paint.COLOR_VIOLET = 16
rp_paint.COLOR_MAGENTA = 17
rp_paint.COLOR_REDVIOLET = 18
rp_paint.COLOR_HOT_PINK = 19
local FACEDIR_COLOR_WHITE = 0
local FACEDIR_COLOR_GRAY = 1
local FACEDIR_COLOR_RED = 2
local FACEDIR_COLOR_ORANGE = 3
local FACEDIR_COLOR_YELLOW = 4
local FACEDIR_COLOR_GREEN = 5
local FACEDIR_COLOR_BLUE = 6
local FACEDIR_COLOR_VIOLET = 7
local facedir_color_map = {
[rp_paint.COLOR_WHITE] = FACEDIR_COLOR_WHITE,
[rp_paint.COLOR_GRAY] = FACEDIR_COLOR_GRAY,
[rp_paint.COLOR_BLACK] = FACEDIR_COLOR_GRAY,
[rp_paint.COLOR_RED] = FACEDIR_COLOR_RED,
[rp_paint.COLOR_ORANGE] = FACEDIR_COLOR_ORANGE,
[rp_paint.COLOR_TANGERINE] = FACEDIR_COLOR_ORANGE,
[rp_paint.COLOR_YELLOW] = FACEDIR_COLOR_YELLOW,
[rp_paint.COLOR_LIME] = FACEDIR_COLOR_YELLOW,
[rp_paint.COLOR_GREEN] = FACEDIR_COLOR_GREEN,
[rp_paint.COLOR_BLUEGREEN] = FACEDIR_COLOR_GREEN,
[rp_paint.COLOR_TURQUOISE] = FACEDIR_COLOR_GREEN,
[rp_paint.COLOR_CYAN] = FACEDIR_COLOR_BLUE,
[rp_paint.COLOR_SKYBLUE] = FACEDIR_COLOR_BLUE,
[rp_paint.COLOR_AZURE_BLUE] = FACEDIR_COLOR_BLUE,
[rp_paint.COLOR_BLUE] = FACEDIR_COLOR_BLUE,
[rp_paint.COLOR_VIOLET] = FACEDIR_COLOR_VIOLET,
[rp_paint.COLOR_MAGENTA] = FACEDIR_COLOR_VIOLET,
[rp_paint.COLOR_REDVIOLET] = FACEDIR_COLOR_RED,
[rp_paint.COLOR_HOT_PINK] = FACEDIR_COLOR_RED,
}
local change_bucket_level = function(pos, node, level_change)
local paint_level = minetest.get_item_group(node.name, "paint_bucket")
if paint_level <= 0 then
return false
end
paint_level = paint_level - 1
local old_paint_level = paint_level
level_change = math.floor(level_change)
paint_level = paint_level + level_change
paint_level = math.max(0, math.min(BUCKET_LEVELS, paint_level))
if paint_level >= BUCKET_LEVELS then
node.name = "rp_paint:bucket"
else
node.name = "rp_paint:bucket_"..paint_level
end
if old_paint_level == paint_level then
-- No level change
return false
end
minetest.swap_node(pos, node)
if paint_level == 0 then
local meta = minetest.get_meta(pos)
meta:set_string("infotext", S("Paint Bucket (empty)"))
elseif old_paint_level == 0 and paint_level > 0 then
local meta = minetest.get_meta(pos)
local color = bit.rshift(node.param2, 2)
meta:set_string("infotext", S("Paint Bucket (@1)", COLOR_NAMES[color+1]))
end
-- Return when node was changed
return true
end
rp_paint.get_color = function(node)
local color
local def = minetest.registered_nodes[node.name]
if not def then
return nil
end
if def.paramtype2 == "color" then
color = node.param2 + 1
elseif def.paramtype2 == "color4dir" then
color = math.floor(node.param2 / 4) + 1
elseif def.paramtype2 == "colorwallmounted" then
color = math.floor(node.param2 / 8) + 1
elseif def.paramtype2 == "colorfacedir" then
local pre_color = math.floor(node.param2 / 32) + 1
color = facedir_color_map[pre_color] + 1
end
if color < 1 or color > rp_paint.COLOR_COUNT then
return nil
end
return color
end
local get_param2_color = function(node, color)
local def = minetest.registered_nodes[node.name]
if not def then
return nil
end
color = color-1
if def.paramtype2 == "colorfacedir" then
color = facedir_color_map[color+1]
end
if (not color) or color < 0 or color > rp_paint.COLOR_COUNT then
color = 0
end
local new_param2
if def.paramtype2 == "color" then
new_param2 = color
elseif def.paramtype2 == "color4dir" then
local rot = node.param2 % 4
new_param2 = color*4 + rot
elseif def.paramtype2 == "colorwallmounted" then
local rot = node.param2 % 8
new_param2 = color*8 + rot
elseif def.paramtype2 == "colorfacedir" then
local rot = node.param2 % 32
new_param2 = color*32 + rot
else
-- Node coloring is unsupported. Do nothing
return nil
end
return new_param2
end
rp_paint.set_color = function(pos, color)
local node = minetest.get_node(pos)
local paintable = minetest.get_item_group(node.name, "paintable")
if paintable == 0 then
return
end
local def = minetest.registered_nodes[node.name]
local can_paint = true
if paintable == 2 then
if def._rp_painted_node_name then
node.name = def._rp_painted_node_name
else
node.name = node.name .. "_painted"
end
end
local p2color = get_param2_color(node, color)
node.param2 = p2color
if def._on_paint then
can_paint = def._on_paint(pos, p2color)
if can_paint == nil then
can_paint = true
end
end
if can_paint then
minetest.swap_node(pos, node)
return true
end
return false
end
rp_paint.remove_color = function(pos)
local node = minetest.get_node(pos)
local paintable = minetest.get_item_group(node.name, "paintable")
if paintable == 1 then
local olddef = minetest.registered_nodes[node.name]
if not olddef then
return false
end
-- Check if there is an 'unpainted' version of the node
if olddef._rp_unpainted_node_name or string.sub(node.name, -8, -1) == "_painted" then
local newname
if olddef._rp_unpainted_node_name then
-- If name of unpainted node name was specified explicitly,
-- use that one
newname = olddef._rp_unpainted_node_name
else
-- Default: Remove "_painted" suffix
newname = string.sub(node.name, 1, -9)
end
local newdef = minetest.registered_nodes[newname]
if olddef and newdef then
local param2 = 0
if olddef.paramtype2 == "color4dir" then
param2 = node.param2 % 4
elseif olddef.paramtype2 == "colorwallmounted" then
param2 = node.param2 % 8
elseif olddef.paramtype2 == "colorfacedir" then
param2 = node.param2 % 32
end
local can_unpaint = true
local newnode = {name=newname, param2=param2}
if olddef._on_unpaint then
can_unpaint = olddef._on_unpaint(pos, newnode)
if can_unpaint == nil then
can_unpaint = true
end
end
if can_unpaint then
minetest.swap_node(pos, newnode)
return true
end
end
end
end
return false
end
rp_paint.add_scrape_particles = function(pos, oldnode, direction)
local olddef = minetest.registered_nodes[oldnode.name]
if not olddef then
return false
end
-- Spawn particles where we scrape
local offset1, offset2
local SQ = 0.48 -- "radius" of square
local H1 = 0.48 -- min. distance from node
local H2 = 0.49 -- max. distance from node
if direction.y > 0 then
offset1 = {x=-SQ, y=-H2, z=-SQ}
offset2 = {x=SQ, y=-H1, z=SQ}
elseif direction.y < 0 then
offset1 = {x=-SQ, y=H1, z=-SQ}
offset2 = {x=SQ, y=H2, z=SQ}
elseif direction.x > 0 then
offset1 = {x=-H2, y=-SQ, z=-SQ}
offset2 = {x=-H1, y=SQ, z=SQ}
elseif direction.x < 0 then
offset1 = {x=H1, y=-SQ, z=-SQ}
offset2 = {x=H2, y=SQ, z=SQ}
elseif direction.z < 0 then
offset1 = {x=-SQ, y=-SQ, z=H1}
offset2 = {x=SQ, y=SQ, z=H2}
elseif direction.z > 0 then
offset1 = {x=-SQ, y=-SQ, z=-H2}
offset2 = {x=SQ, y=SQ, z=-H1}
else
offset1 = {x=0, y=0, z=0}
offset2 = {x=0, y=0, z=0}
end
local particle_node
if olddef._rp_paint_particle_node == false then
-- Don't spawn particle
return true
elseif olddef._rp_paint_particle_node ~= nil then
local defnode = {name = olddef._rp_paint_particle_node, param2 = oldnode.param2}
local color = rp_paint.get_color(oldnode)
if not color then
minetest.log("error", "[rp_paint] When scraping off color of a node, rp_paint.get_color() for "..oldnode.name.." returned nil!")
color = 0
end
local p2 = get_param2_color(defnode, color)
particle_node = {name = olddef._rp_paint_particle_node, param2 = p2}
else
particle_node = oldnode
end
minetest.add_particlespawner({
amount = math.random(10, 20),
time = 0.1,
minpos = vector.add(pos, offset1),
maxpos = vector.add(pos, offset2),
minvel = {x=-0.2, y=0, z=-0.2},
maxvel = {x=0.2, y=2, z=0.2},
minacc = {x=0, y=-GRAVITY, z=0},
maxacc = {x=0, y=-GRAVITY, z=0},
minexptime = 0.1,
maxexptime = 0.5,
minsize = 0.9,
maxsize = 1.0,
collisiondetection = true,
vertical = false,
node = particle_node,
})
return true
end
rp_paint.scrape_color = function(pos, pointed_thing)
local oldnode = minetest.get_node(pos)
local olddef = minetest.registered_nodes[oldnode.name]
if not olddef then
return false
end
local scraped = rp_paint.remove_color(pos)
if scraped then
local node = minetest.get_node(pos)
local def = minetest.registered_nodes[node.name]
if not def then
return false
end
if def.sounds and def.sounds._rp_scrape then
minetest.sound_play(def.sounds._rp_scrape, {pos=pos, max_hear_distance=8}, true)
end
if pointed_thing and pointed_thing.type == "node" then
-- Spawn particles where we scrape
local particlepos = pointed_thing.above
local direction = { x=0, y=0, z=0 }
if pointed_thing.above.y > pointed_thing.under.y then
direction.y = 1
elseif pointed_thing.above.y < pointed_thing.under.y then
direction.y = -1
elseif pointed_thing.above.x > pointed_thing.under.x then
direction.x = 1
elseif pointed_thing.above.x < pointed_thing.under.x then
direction.x = -1
elseif pointed_thing.above.z > pointed_thing.under.z then
direction.z = 1
elseif pointed_thing.above.z < pointed_thing.under.z then
direction.z = -1
end
rp_paint.add_scrape_particles(particlepos, oldnode, direction)
end
return true
end
return false
end
minetest.register_tool("rp_paint:brush", {
description = S("Paint Brush"),
_tt_help = S("Changes color of paintable blocks").."\n"..S("Punch paint bucket to change brush color").."\n"..S("Refill with flowers"),
inventory_image = "rp_paint_brush.png",
inventory_overlay = "rp_paint_brush_overlay.png",
wield_image = "rp_paint_brush.png",
wield_overlay = "rp_paint_brush_overlay.png",
palette = "rp_paint_palette_256.png",
on_use = function(itemstack, user, pointed_thing)
if pointed_thing == nil or pointed_thing.type ~= "node" then
return
end
local pos = pointed_thing.under
if minetest.is_protected(pos, user:get_player_name()) and
not minetest.check_player_privs(user, "protection_bypass") then
minetest.record_protection_violation(pos, user:get_player_name())
return
end
local node = minetest.get_node(pos)
local imeta = itemstack:get_meta()
-- Get color from paint bucket
if minetest.get_item_group(node.name, "paint_bucket") > 1 then
local color = bit.rshift(node.param2, 2)
if color > rp_paint.COLOR_COUNT or color < 0 then
-- Invalid paint bucket color!
return
end
change_bucket_level(pos, node, -1)
imeta:set_int("palette_index", color)
minetest.sound_play({name="rp_paint_brush_dip", gain=0.3}, {pos=pos, max_hear_distance = 8}, true)
return itemstack
end
-- Paint paintable node (if not paintable, fail)
local color = imeta:get_int("palette_index") + 1
local painted = rp_paint.set_color(pointed_thing.under, color)
if painted then
minetest.sound_play({name="rp_paint_brush_paint", gain=0.2}, {pos=pos, max_hear_distance = 8}, true)
if not minetest.is_creative_enabled(user:get_player_name()) then
itemstack:add_wear_by_uses(BRUSH_USES)
end
end
return itemstack
end,
groups = { disable_repair = 1 },
})
local on_bucket_construct = function(pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", S("Paint Bucket (@1)", COLOR_NAMES[1]))
end
local bucket_flower_add = function(pos, node, clicker, itemstack, pointed_thing)
if itemstack and itemstack:get_name() == "rp_default:flower" then
if change_bucket_level(pos, node, BUCKET_FLOWER_ADD) then
minetest.sound_play({name="rp_paint_bucket_select_color", gain=0.20, pitch=0.7}, {pos = pos}, true)
if clicker and clicker:is_player() and not minetest.is_creative_enabled(clicker:get_player_name()) then
itemstack:take_item()
return true, itemstack
end
end
return true, itemstack
end
return false, itemstack
end
local on_bucket_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
if not pointed_thing or util.handle_node_protection(clicker, pointed_thing) then
return
end
-- ++ If holding a flower, add paint level ++
local flower_used, itemstack = bucket_flower_add(pos, node, clicker, itemstack, pointed_thing)
if flower_used then
return itemstack
end
-- ++ Switch color on rightclick ++
-- "direction" of color change (1 = next color, -1 = previous color)
local direction = 1
if clicker and clicker:is_player() then
local props = clicker:get_properties()
local eye_pos = clicker:get_pos()
eye_pos.y = eye_pos.y + props.eye_height
eye_pos = vector.add(eye_pos, clicker:get_eye_offset())
local lookdir = clicker:get_look_dir()
local handrange = minetest.registered_items[""].range
lookdir = vector.multiply(lookdir, handrange+1)
local look_pos = vector.add(eye_pos, lookdir)
-- do a raycast from the player to the look direction,
-- using the hand range + 1 as vector length.
-- (+1 serves as a small buffer)
-- With this method we can find the precise click location.
local rc = Raycast(eye_pos, look_pos, false, false)
local exact_pos
for rpt in rc do
if rpt.type == "node" then
local rptn = minetest.get_node(rpt.under)
if rptn.name == node.name then
exact_pos = rpt.intersection_point
break
end
end
end
local fine_pos = exact_pos
-- Fallback if raycast didn't find our paint bucket for some reason
if not fine_pos then
fine_pos = minetest.pointed_thing_to_face_pos(clicker, pointed_thing)
minetest.log("warning", "[rp_paint] "..clicker:get_player_name().." rightclicked paint bucket at "..minetest.pos_to_string(pos).." but the raycast failed to find it. Using less accurate fallback to find click position")
end
-- Depending on what was clicked and where the player stood, the paint bucket
-- will choose either the next or previous color.
-- Basically, if you click the left side of the face, the previous color
-- will be selected, and the right side gets you the next color.
-- The side textures should have subtle engraved small arrows
if pointed_thing.above.y ~= pointed_thing.under.y then
local cpos = clicker:get_pos()
local xdist = math.abs(pos.x-cpos.x)
local zdist = math.abs(pos.z-cpos.z)
if xdist > zdist then
if cpos.x < pos.x then
if fine_pos.z > pos.z then
direction = -1
end
else
if fine_pos.z < pos.z then
direction = -1
end
end
else
if cpos.z < pos.z then
if fine_pos.x < pos.x then
direction = -1
end
else
if fine_pos.x > pos.x then
direction = -1
end
end
end
else
if pointed_thing.above.z < pointed_thing.under.z and fine_pos.x < pos.x then
direction = -1
elseif pointed_thing.above.z > pointed_thing.under.z and fine_pos.x > pos.x then
direction = -1
elseif pointed_thing.above.x < pointed_thing.under.x and fine_pos.z > pos.z then
direction = -1
elseif pointed_thing.above.x > pointed_thing.under.x and fine_pos.z < pos.z then
direction = -1
end
end
end
local rot = node.param2 % 4
local color = bit.rshift(node.param2, 2)
color = color + direction
if color >= rp_paint.COLOR_COUNT then
color = 0
elseif color < 0 then
color = rp_paint.COLOR_COUNT - 1
end
local meta = minetest.get_meta(pos)
meta:set_string("infotext", S("Paint Bucket (@1)", COLOR_NAMES[color+1]))
node.param2 = color*4 + rot
minetest.swap_node(pos, node)
minetest.sound_play({name="rp_paint_bucket_select_color", gain=0.15}, {pos = pos}, true)
end
local on_bucket_construct_empty = function(pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", S("Paint Bucket (empty)"))
end
local on_bucket_rightclick_empty = function(pos, node, clicker, itemstack, pointed_thing)
if not pointed_thing or util.handle_node_protection(clicker, pointed_thing) then
return
end
-- ++ If holding a flower, add paint level ++
local flower_used, itemstack = bucket_flower_add(pos, node, clicker, itemstack, pointed_thing)
if flower_used then
return itemstack
end
end
for i=0, BUCKET_LEVELS do
local id, desc, tt, mesh, img, nici, ws, overlay, painttile, paintover, construct, rightclick
local paint_level = i + 1
if i == 0 then
-- empty bucket
id = "rp_paint:bucket_"..i
desc = S("Paint Bucket")
tt = S("Use place key to change color").."\n"..S("Point at left/right part to get previous/next color")
mesh = "rp_paint_bucket_empty.obj"
rightclick = on_bucket_rightclick_empty
construct = on_bucket_construct_empty
elseif i == BUCKET_LEVELS then
-- full bucket
id = "rp_paint:bucket"
desc = S("Paint Bucket with Paint")
tt = S("Use place key to change color").."\n"..S("Point at left/right part to get previous/next color")
mesh = "rp_paint_bucket_m0.obj"
img = "rp_paint_bucket.png"
ws = {x=1,y=1,z=2}
rightclick = on_bucket_rightclick
construct = on_bucket_construct
else
-- bucket with other paint level
id = "rp_paint:bucket_"..i
local m = BUCKET_LEVELS-i
mesh = "rp_paint_bucket_m"..m..".obj"
nici = 1
rightclick = on_bucket_rightclick
construct = on_bucket_construct
end
if i > 0 then
paintover = "([combine:16x16:0,"..i.."=rp_paint_bucket_node_inside_paint_overlay.png\\^[transformFY)^[mask:(rp_paint_bucket_node_inside_paint_overlay_mask.png^[transformFY)"
painttile = "rp_paint_bucket_node_paint.png"
else
paintover = ""
painttile = "blank.png"
end
minetest.register_node(id, {
description = desc,
_tt_help = tt,
drawtype = "mesh",
mesh = mesh,
tiles = {
{name="rp_paint_bucket_node_side_1.png",backface_culling=true,color="white"},
{name="rp_paint_bucket_node_side_2.png",backface_culling=true,color="white"},
{name="rp_paint_bucket_node_top_handle.png",backface_culling=true,color="white"},
{name="rp_paint_bucket_node_bottom_inside.png",backface_culling=true,color="white"},
{name="rp_paint_bucket_node_bottom_outside.png",backface_culling=true,color="white"},
painttile,
},
overlay_tiles = {
"","","",paintover,"","",
},
use_texture_alpha = "blend",
paramtype = "light",
paramtype2 = "color4dir",
palette = "rp_paint_palette_64.png",
is_ground_content = false,
selection_box = {
type = "fixed",
fixed = { -BUCKET_RADIUS, -0.5, -BUCKET_RADIUS, BUCKET_RADIUS, BUCKET_HEIGHT_ABOVE_ZERO, BUCKET_RADIUS },
},
sounds = rp_sounds.node_sound_metal_defaults(),
walkable = false,
floodable = true,
on_flood = function(pos, oldnode, newnode)
minetest.add_item(pos, "rp_paint:bucket")
end,
inventory_image = img,
wield_image = img,
wield_scale = ws,
groups = { bucket = 3, paint_bucket = paint_level, tool = 1, dig_immediate = 3, attached_node = 1, not_in_creative_inventory = nici },
on_construct = construct,
on_rightclick = rightclick,
-- Erase node metadata (e.g. palette_index) on drop
drop = "rp_paint:bucket",
})
end
crafting.register_craft({
output = "rp_paint:bucket",
items = {
"rp_default:ingot_tin 5",
"rp_default:flower 4",
},
})
crafting.register_craft({
output = "rp_paint:brush",
items = {
"rp_default:stick",
"rp_farming:cotton 3",
},
})