2018-12-23 17:42:51 -05:00

796 lines
29 KiB
Lua

-- painting - in-game painting for minetest
-- THIS MOD CODE AND TEXTURES LICENSED
-- <3 TO YOU <3
-- UNDER TERMS OF WTFPL LICENSE
-- 2012, 2013, 2014 obneq aka jin xi
-- picture is drawn using a nodebox to draw the canvas
-- and an entity which has the painting as its texture.
-- this texture is created by minetests internal image
-- compositing engine (see tile.cpp).
-- Edited by Jasper den Ouden (a few commits now)
dofile(minetest.get_modpath("painting").."/crafts.lua")
local hexcols = {
white = "ffffff", yellow = "fff000",
orange = "ff6c00", red = "ff0000",
violet = "8a00ff", blue = "000cff",
green = "0cff00", magenta = "fc00ff",
cyan = "00ffea", grey = "bebebe",
dark_grey = "7b7b7b", black = "000000",
dark_green = "006400", brown="964b00",
pink = "ffc0cb"
}
local colors = {}
local revcolors = {
"white", "dark_green", "grey", "red", "brown", "cyan", "orange", "violet",
"dark_grey", "pink", "green", "magenta", "yellow", "black", "blue"
}
local thickness = 0.1
-- picture node
local picbox = {
type = "fixed",
fixed = { -0.499, -0.499, 0.499, 0.499, 0.499, 0.499 - thickness }
}
local picbox2 = {
type="fixed",
fixed={
{-0.5, -0.5, -0.5, 0.5, -0.5+thickness, 0.5},
{-0.5, -0.5, -0.5, -0.5, -0.39, 0.5},
{-0.5, -0.5, -0.5, 0.5, -0.39, -0.5},
{ 0.5, -0.5, 0.5, -0.5, -0.39, 0.5},
{ 0.5, -0.5, 0.5, 0.5, -0.39, -0.5}
}
}
local current_version = "0.1-juli"
local legacy = {}
-- puts the version before the data
local function get_metastring(data)
return current_version.."(version)"..data
end
-- Initiate a white grid.
local function initgrid(res)
local grid, a, x, y = {}, res-1
for x = 0, a do
grid[x] = {}
for y = 0, a do
grid[x][y] = colors["white"]
end
end
return grid
end
local function to_imagestring(data, res)
if not data then
minetest.log("error", "[painting] missing data")
return
end
local cols = {}
for y = 0, res - 1 do
local xs = data[y]
for x = 0, res - 1 do
local col = revcolors[xs[x]]
--if col ~= "white" then
cols[col] = cols[col] or {}
cols[col][#cols[col]+1] = {y, x}
--end
end
end
local t,n = {},1
local groupopen = "([combine:"..res.."x"..res
for colour,ps in pairs(cols) do
t[n] = groupopen
n = n+1
for _,p in pairs(ps) do
local y,x = unpack(p)
t[n] = ":"..p[1]..","..p[2].."=white.png"
n = n+1
end
t[n] = "^[colorize:#"..hexcols[colour]..")^"
n = n+1
end
n = n-1
if n == 0 then
minetest.log("error", "[painting] no texels")
return "white.png"
end
t[n] = t[n]:sub(1,-2)
return table.concat(t)
end
local function dot(v, w) -- Inproduct.
return v.x * w.x + v.y * w.y + v.z * w.z
end
local function intersect(pos, dir, origin, normal)
local t = -(dot(vector.subtract(pos, origin), normal)) / dot(dir, normal)
return vector.add(pos, vector.multiply(dir, t))
end
local function clamp(x, min,max)
return math.max(math.min(x, max),min)
end
minetest.register_node("painting:pic", {
description = "Picture",
tiles = { "white.png" },
inventory_image = "painted.png",
drawtype = "nodebox",
sunlight_propagates = true,
paramtype = "light",
paramtype2 = "facedir",
node_box = picbox2,
selection_box = { type="fixed", fixed = {-0.5, -0.5, -0.5, 0.5, -0.39, 0.5} },
groups = {snappy = 2, choppy = 2, oddly_breakable_by_hand = 2,
not_in_creative_inventory=1},
on_rotate = function(pos) return false end,
--handle that right below, don't drop anything
drop = "",
after_dig_node = function(pos, _, oldmetadata, digger)
--find and remove the entity
for _,o in pairs(minetest.get_objects_inside_radius(pos, 0.5)) do
if not o:is_player() then
if o:get_luaentity().name == "painting:picent"
or o:get_luaentity().name == "painting:picent_notupright" then
o:remove()
end
end
end
if not oldmetadata.fields["painting:picturedata"] then return end
local data = legacy.load_itemmeta(oldmetadata.fields["painting:picturedata"])
--put picture data back into inventory item
digger:get_inventory():add_item("main", {
name = "painting:paintedcanvas",
count = 1,
metadata = get_metastring(data)
})
end,
--copy pictures
on_punch = function(pos, node, player, pointed_thing)
local meta = minetest.get_meta(pos):to_table()
if meta == nil then return end
if not meta.fields["painting:picturedata"] then return end
local data = legacy.load_itemmeta(meta.fields["painting:picturedata"])
if not minetest.deserialize(data) then
data = minetest.decompress(data)
minetest.log("action", "tried to copy old data, convert it to new format")
end
--compare resulutions of picture and canvas the player wields
--if it isn't the same don't copy
local wname = player:get_wielded_item():get_name()
local res = tonumber(string.sub(wname, #"painting:canvas_"+1))
if res == nil then return end
data_res = minetest.deserialize(data).res
if data_res == nil then return end
if res ~= data_res then
minetest.chat_send_player(player:get_player_name(),
"not same canvas type!")
return
end
--remove canvas, add picture
player:get_inventory():remove_item("main", {
name = wname,
count = 1,
})
player:get_inventory():add_item("main", {
name = "painting:paintedcanvas",
count = 1,
metadata = get_metastring(data)
})
end
})
local on_activate = function(self, staticdata)
local pos = self.object:getpos()
local ldata = legacy.load_itemmeta(minetest.get_meta(pos):get_string("painting:picturedata"))
if not ldata then return end
local data = minetest.deserialize(ldata)
-- for backwards compatiblity
if not data then
data = minetest.deserialize(minetest.decompress(ldata))
if data then
minetest.log("action", "loaded old data, converted to new uncompressed")
end
end
-- end backwards compatiblity
if not data
or not data.grid then
return
end
self.object:set_properties({textures = { to_imagestring(data.grid, data.res) }})
local pos = self.object:getpos()
local node = minetest.get_node(pos)
local param2 = node.param2 --wallmounted = first 3 bits
if node.name == "air" then
minetest.swap_node(pos, {name="painting:pic"})
end
if param2 == 20 then -- if ceiling
self.object:set_properties({textures = {"white.png", to_imagestring(data.grid, data.res) }})
end
if data.version ~= current_version then
minetest.log("legacy", "[painting] updating placed picture data")
if data.version == "nopairs" then
legacy.change_to_wallmounted(pos, self.object:getyaw())
end
data.version = current_version
data = minetest.serialize(data)
minetest.get_meta(pos):set_string("painting:picturedata", get_metastring(data))
end
end
-- picture texture entity
minetest.register_entity("painting:picent", {
collisionbox = { 0, 0, 0, 0, 0, 0 },
visual = "upright_sprite",
textures = { "white.png" },
on_activate = on_activate
})
minetest.register_entity("painting:picent_notupright", {
collisionbox = { 0, 0, 0, 0, 0, 0 },
visual = "cube",
visual_size = { x = 1, y = 0},
textures = { "white.png" },
on_activate = on_activate
})
-- Figure where it hits the canvas, in fraction given position and direction.
local function figure_paint_pos_raw(pos, d,od, ppos, l)
--get player eye level, see player.h line 129
local player_eye_h = 1.625
ppos.y = ppos.y + player_eye_h
local normal = { x = d.x, y = 0, z = d.z }
local p = intersect(ppos, l, pos, normal)
local off = -0.5
pos = vector.add(pos, {x=off*od.x, y=off, z=off*od.z})
p = vector.subtract(p, pos)
return math.abs(p.x + p.z), 1 - p.y
end
local dirs = { -- Directions the painting may be.
[0] = { x = 0, z = 1 },
[1] = { x = 1, z = 0 },
[2] = { x = 0, z =-1 },
[3] = { x =-1, z = 0 }
}
-- .. idem .. given self and puncher.
local function figure_paint_pos(self, puncher)
local x,y = figure_paint_pos_raw(self.object:getpos(),
dirs[self.fd], dirs[(self.fd + 1) % 4],
puncher:getpos(), puncher:get_look_dir())
return math.floor(self.res*clamp(x, 0, 1)), math.floor(self.res*clamp(y, 0, 1))
end
local function draw_input(self, name, x,y, as_line)
if x == nil or y == nil then return end
local x0 = self.x0
if as_line and x0 then -- Draw line if requested *and* have a previous position.
local y0 = self.y0
local line = vector.twoline(x0-x, y0-y) -- This figures how to do the line.
for _,coord in pairs(line) do
if self.grid then
self.grid[x+coord[1]][y+coord[2]] = colors[name]
end
end
else -- Draw just single point.
if self.grid and self.grid[x] and self.grid[x][y] then
self.grid[x][y] = colors[name]
end
end
self.x0, self.y0 = x, y -- Update previous position.
-- Actually update the grid.
self.object:set_properties{textures = { to_imagestring(self.grid, self.res) }}
end
local paintbox = {
[0] = { -0.5,-0.5,0,0.5,0.5,0 },
[1] = { 0,-0.5,-0.5,0,0.5,0.5 }
}
-- Painting as being painted.
minetest.register_entity("painting:paintent", {
collisionbox = { 0, 0, 0, 0, 0, 0 },
visual = "upright_sprite",
textures = { "white.png" },
on_punch = function(self, puncher)
--check for brush.
local name = string.sub(puncher:get_wielded_item():get_name(), #"painting:brush_"+1)
if not colors[name] then -- Not one of the brushes; can't paint.
return
end
assert(self.object)
local x,y = figure_paint_pos(self, puncher)
draw_input(self, name, x,y, puncher:get_player_control().sneak)
local wielded = puncher:get_wielded_item() -- Wear down the tool.
wielded:add_wear(65535/256)
puncher:set_wielded_item(wielded)
end,
on_activate = function(self, staticdata)
local data = minetest.deserialize(staticdata)
-- for backwards compatiblity
if not data then
data = minetest.deserialize(minetest.decompress(staticdata))
if data then
minetest.log("action", "loaded old data, converted to new uncompressed")
end
end
-- end backwards compatiblity
if not data then
return
end
self.fd = data.fd
self.x0, self.y0 = data.x0, data.y0
self.res = data.res
self.version = data.version
self.grid = data.grid
legacy.fix_grid(self.grid, self.version)
self.object:set_properties{ textures = { to_imagestring(self.grid, self.res) }}
if not self.fd then
return
end
self.object:set_properties{ collisionbox = paintbox[self.fd%2] }
self.object:set_armor_groups{immortal=1}
end,
get_staticdata = function(self)
return minetest.serialize{fd = self.fd, res = self.res,
grid = self.grid, x0 = self.x0, y0 = self.y0, version = self.version
}
end
})
--paintedcanvas picture inventory item
minetest.register_craftitem("painting:paintedcanvas", {
description = "Painted Canvas",
inventory_image = "painted.png",
stack_max = 1,
groups = { snappy = 2, choppy = 2, oddly_breakable_by_hand = 2, not_in_creative_inventory=1 },
on_place = function(itemstack, placer, pointed_thing)
-- copied from default:torch
local under = pointed_thing.under
local above = pointed_thing.above
local node = minetest.get_node(under)
local def = minetest.registered_nodes[node.name]
if def and def.on_rightclick and
not (placer and placer:is_player() and
placer:get_player_control().sneak) then
return def.on_rightclick(under, node, placer, itemstack,
pointed_thing) or itemstack
end
if def.buildable_to then
return itemstack
end
--place node
local pos = pointed_thing.above
if minetest.is_protected(pos, placer:get_player_name()) then
return
end
-- taken from core.rotate_and_place
local fdir = minetest.dir_to_facedir(vector.subtract(under, above))
local iswall = (above.y == under.y)
local isceiling = not iswall and (above.y < under.y)
local param2 = fdir
if iswall then
local dirs = {9, 18, 7, 12}
param2 = dirs[fdir+1]
elseif isceiling then
param2 = 20
end
minetest.item_place_node(ItemStack("painting:pic"), placer, pointed_thing, param2)
local dir = vector.subtract(under, above)
local wallmounted = minetest.dir_to_wallmounted(dir)
local yaw = {nil, -- y
nil, -- -y
math.pi/2, -- x
3/2*math.pi, -- -x
0, -- z
math.pi -- -z
}
yaw = yaw[wallmounted+1]
--save metadata
local data = legacy.load_itemmeta(itemstack:get_metadata())
-- for backwards compatiblity
if not minetest.deserialize(data) then
status, data = pcall(minetest.decompress, data)
if (status and data) then
minetest.log("action", "tried to save old data"..
"converted to new uncompressed, save")
elseif not status then
minetest.log("error", "loading picture data unsuccessfull")
end
end
-- end backwards compatiblity
if data == nil then return ItemStack("") end
minetest.get_meta(pos):set_string("painting:picturedata", get_metastring(data))
--add entity
local off = (0.5 - thickness - 0.01)
pos.x = pos.x + dir.x * off
pos.y = pos.y + dir.y * off
pos.z = pos.z + dir.z * off
data = minetest.deserialize(data)
if data == nil then return ItemStack("") end
local img = to_imagestring(data.grid, data.res)
if wallmounted == 1 then
local obj = minetest.add_entity(pos, "painting:picent_notupright")
obj:set_properties{ textures = { img }}
elseif wallmounted == 0 then
local obj = minetest.add_entity(pos, "painting:picent_notupright")
obj:set_properties{ textures = { "white.png", img }}
else
local obj = minetest.add_entity(pos, "painting:picent")
obj:set_properties{ textures = { img }}
obj:setyaw(yaw)
end
return ItemStack("")
end
})
--canvas inventory items
for i = 4,6 do
minetest.register_craftitem("painting:canvas_"..2^i, {
description = "Canvas(" .. 2^i .. ")",
inventory_image = "default_paper.png",
stack_max = 99,
})
end
--canvas for drawing
local canvasbox = {
type = "fixed",
fixed = { -0.5, -0.5, 0, 0.5, 0.5, thickness }
}
minetest.register_node("painting:canvasnode", {
description = "Canvas",
tiles = { "white.png" },
inventory_image = "painted.png",
drawtype = "nodebox",
sunlight_propagates = true,
paramtype = "light",
paramtype2 = "facedir",
node_box = canvasbox,
selection_box = canvasbox,
groups = {snappy = 2, choppy = 2, oddly_breakable_by_hand = 2,
not_in_creative_inventory=1},
drop = "",
after_dig_node = function(pos, oldnode, oldmetadata, digger)
--get data and remove pixels
local data = {}
for _,e in pairs(minetest.get_objects_inside_radius(pos, 0.1)) do
e = e:get_luaentity()
if e.grid then
data.grid = e.grid
data.version = e.version
data.res = e.res
e.object:remove()
break
end
end
pos.y = pos.y-1
minetest.get_meta(pos):set_int("has_canvas", 0)
if not data.grid then
return
end
legacy.fix_grid(data.grid, data.version)
digger:get_inventory():add_item("main", {
name = "painting:paintedcanvas",
count = 1,
metadata = get_metastring(minetest.serialize(data))
})
end
})
local easelbox = { -- Specifies 3d model.
type = "fixed",
fixed = {
--feet
{-0.4, -0.5, -0.5, -0.3, -0.4, 0.5 },
{ 0.3, -0.5, -0.5, 0.4, -0.4, 0.5 },
--legs
{-0.4, -0.4, 0.1, -0.3, 1.5, 0.2 },
{ 0.3, -0.4, 0.1, 0.4, 1.5, 0.2 },
--shelf
{-0.5, 0.35, -0.3, 0.5, 0.45, 0.1 }
}
}
minetest.register_node("painting:easel", {
description = "Easel",
tiles = { "default_wood.png" },
drawtype = "nodebox",
sunlight_propagates = true,
paramtype = "light",
paramtype2 = "facedir",
node_box = easelbox,
selection_box = easelbox,
groups = { snappy = 2, choppy = 2, oddly_breakable_by_hand = 2 },
on_punch = function(pos, node, player)
local wield_name = player:get_wielded_item():get_name()
local name, res = string.match(wield_name, "^([^_]+)_([^_]+)")
if name ~= "painting:canvas" then -- Can only put the canvas on there.
return
end
local meta = minetest.get_meta(pos)
pos.y = pos.y+1
if minetest.get_node(pos).name ~= "air" then
-- this is not likely going to happen
return
end
local fd = node.param2
minetest.add_node(pos, { name = "painting:canvasnode", param2 = fd})
local dir = dirs[fd]
pos.x = pos.x - 0.01 * dir.x
pos.z = pos.z - 0.01 * dir.z
local obj = minetest.add_entity(pos, "painting:paintent")
obj:set_properties{ collisionbox = paintbox[fd%2] }
obj:set_armor_groups{immortal=1}
obj:setyaw(math.pi * fd / -2)
local res = tonumber(res) -- Was still string from matching.
local ent = obj:get_luaentity()
ent.grid = initgrid(res)
ent.version = current_version
ent.res = res
ent.fd = fd
meta:set_int("has_canvas", 1)
player:get_inventory():remove_item("main", ItemStack(wield_name))
end,
can_dig = function(pos)
return minetest.get_meta(pos):get_int("has_canvas") == 0
end
})
--brushes
local function table_copy(t)
local t2 = {}
for k,v in pairs(t) do
t2[k] = v
end
return t2
end
local brush = {
wield_image = "",
tool_capabilities = {
full_punch_interval = 1.0,
max_drop_level=0,
groupcaps = {}
}
}
local textures = {
white = "white.png", yellow = "yellow.png",
orange = "orange.png", red = "red.png",
violet = "violet.png", blue = "blue.png",
green = "green.png", magenta = "magenta.png",
cyan = "cyan.png", grey = "grey.png",
dark_grey = "darkgrey.png", black = "black.png",
dark_green = "darkgreen.png", brown="brown.png",
pink = "pink.png"
}
local vage_revcolours = {} -- colours in pairs order
for color, _ in pairs(textures) do
vage_revcolours[#vage_revcolours+1] = color
local brush_new = table_copy(brush)
brush_new.description = color:gsub("^%l", string.upper).." brush"
brush_new.inventory_image = "painting_brush_stem.png^(painting_brush_head.png^[colorize:#"..hexcols[color]..":255)^painting_brush_head.png"
minetest.register_tool("painting:brush_"..color, brush_new)
minetest.register_craft{
output = "painting:brush_"..color,
recipe = {
{"dye:"..color},
{"default:stick"},
{"default:stick"}
}
}
end
for i, color in ipairs(revcolors) do
colors[color] = i
end
-- legacy
minetest.register_alias("easel", "painting:easel")
minetest.register_alias("canvas", "painting:canvas_16")
-- fixes the colours which were set by pairs
local function fix_eldest_grid(data)
if data == nil then
return
end
for y in pairs(data) do
local xs = data[y]
for x in pairs(xs) do
-- it was done in pairs order
xs[x] = colors[vage_revcolours[xs[x]]]
end
end
return data
end
-- possibly updates grid
function legacy.fix_grid(grid, version)
if version == current_version then
return
end
--[[
if version == "nopairs" then
return
end--]]
minetest.log("action", "[painting] updating grid")
fix_eldest_grid(grid)
end
-- gets the data from meta
function legacy.load_itemmeta(data)
--if not data then
-- data = current_version.."(version)"..
local vend = data:find"(version)"
if not vend then -- the oldest version
local t = minetest.deserialize(data)
if t == nil then return end
if t.version then
minetest.log("error", "[painting] this musn't happen!")
end
minetest.log("action", "[painting] updating painting meta")
legacy.fix_grid(t.grid)
return minetest.serialize(t)
end
local version = data:sub(1, vend-2)
data = data:sub(vend+8)
--if version == current_version then
return data
--end
end
minetest.register_node("painting:pic_legacy_facedir", {
description = "Picture",
tiles = { "white.png" },
inventory_image = "painted.png",
drawtype = "nodebox",
sunlight_propagates = true,
paramtype = "light",
paramtype2 = "facedir",
node_box = picbox,
selection_box = picbox,
groups = {snappy = 2, choppy = 2, oddly_breakable_by_hand = 2,
not_in_creative_inventory=1},
--handle that right below, don't drop anything
drop = "",
after_dig_node = function(pos, _, oldmetadata, digger)
--find and remove the entity
for _,e in pairs(minetest.get_objects_inside_radius(pos, 0.5)) do
if e:get_luaentity().name == "painting:picent" then
e:remove()
end
end
if not oldmetadata.fields["painting:picturedata"] then return end
local data = legacy.load_itemmeta(oldmetadata.fields["painting:picturedata"])
--put picture data back into inventory item
digger:get_inventory():add_item("main", {
name = "painting:paintedcanvas",
count = 1,
metadata = get_metastring(data)
})
end,
--copy pictures
on_punch = function(pos, node, player, pointed_thing)
local meta = minetest.get_meta(pos):to_table()
if meta == nil then return end
if not meta.fields["painting:picturedata"] then return end
local data = legacy.load_itemmeta(meta.fields["painting:picturedata"])
if not minetest.deserialize(data) then
data = minetest.decompress(data)
minetest.log("action", "tried to copy old data, convert it to new format")
end
--compare resulutions of picture and canvas the player wields
--if it isn't the same don't copy
local wname = player:get_wielded_item():get_name()
local res = tonumber(string.sub(wname, #"painting:canvas_"+1))
if res == nil then return end
data_res = minetest.deserialize(data).res
if data_res == nil then return end
if res ~= data_res then
minetest.chat_send_player(player:get_player_name(),
"not same canvas type!")
return
end
--remove canvas, add picture
player:get_inventory():remove_item("main", {
name = wname,
count = 1,
})
player:get_inventory():add_item("main", {
name = "painting:paintedcanvas",
count = 1,
metadata = get_metastring(data)
})
end
})
function legacy.change_to_wallmounted(pos, yaw)
local param2 = minetest.get_node(pos).param2
minetest.set_node(pos, {name="painting:pic_legacy_facedir", param2=param2})
end
--[[ allows using many colours, doesn't work
function to_imagestring(data, res)
if not data then
return
end
local t,n = {},1
local sbc = {}
for y = 0, res - 1 do
for x = 0, res - 1 do
local col = revcolors[data[x][y] ]
sbc[col] = sbc[col] or {}
sbc[col][#sbc[col] ] = {x,y}
end
end
for col,ps in pairs(sbc) do
t[n] = "([combine:"..res.."x"..res..":"
n = n+1
for _,p in pairs(ps) do
t[n] = p[1]..","..p[2].."=white.png:"
n = n+1
end
t[n-1] = string.sub(t[n-1], 1,-2)
t[n] = "^[colorize:"..col..")^"
n = n+1
end
t[n-1] = string.sub(t[n-1], 1,-2)
return table.concat(t)
end--]]