local SIZE_SMALL = 20; local SIZE_LARGE = 40; local SCALE_SMALL = 1; local SCALE_LARGE = 4; -- Draw background elements in the same arrangement as inventory slots -- x: The x position of the inventory -- y: The y position of the inventory -- cols: The width of the inventory, in columns -- rows: The height of the inventory, in rows -- skin: A 9-slice background skin table -- -- Returns a formspec string local function inventory_bg(x, y, cols, rows, skin) local data = ""; for i = 0,cols - 1 do for j = 0,rows - 1 do data = data .. string.format("background9[%f,%f;1,1;%s.png;false;%s]", x + (i * 1.25), y + (j * 1.25), skin.texture, tostring(skin.radius)); end end return data; end local function get_material_cost(size, detail) local paper = 0; local pigment = 0; if size == SIZE_SMALL then paper = paper + 4; pigment = pigment + 5; elseif size == SIZE_LARGE then paper = paper + 16; pigment = pigment + 20; end pigment = pigment + (detail * 5); return { paper = math.max(paper, 0), pigment = math.max(pigment, 0), }; end -- Get the material cost of the craft settings from the given table metadata -- meta: The metadata to read -- -- Returns a table with the material costs, and a boolean indicating if the -- costs were positive or negative before clamping. local function get_craft_material_cost(meta) local cost = get_material_cost(meta:get_int("size") or SIZE_SMALL, meta:get_int("detail") or 0); local stack = meta:get_inventory():get_stack("output", 1); local is_positive = true; if stack:get_name() == "cartographer:map" then local smeta = stack:get_meta(); local sub_cost = get_material_cost(smeta:get_int("cartographer:size") or SIZE_SMALL, (smeta:get_int("cartographer:detail") or 1) - 1); is_positive = cost.paper >= sub_cost.paper and cost.pigment >= sub_cost.pigment; cost.paper = math.max(cost.paper - sub_cost.paper, 0); cost.pigment = math.max(cost.pigment - sub_cost.pigment, 0); end return cost, is_positive; end -- Check if the given table metadata has enough materials to cover the given -- cost table. -- cost: A table of material costs -- meta: The metadata -- -- Returns true if the table's materials can cover the cost local function can_afford(cost, meta) return cost.paper + cost.pigment > 0 and cost.paper <= meta:get_int("paper") and cost.pigment <= meta:get_int("pigment"); end -- Get the material cost of the copy settings from the given table metadata -- meta: The metadata to read -- -- Returns a table with the material costs local function get_copy_material_cost(meta) local inv = meta:get_inventory(); local in_stack = inv:get_stack("copy_input", 1); local out_stack = inv:get_stack("copy_output", 1); if out_stack:is_empty() and in_stack:get_name() == "cartographer:map" then local smeta = in_stack:get_meta(); local size = smeta:get_int("cartographer:size") or SIZE_SMALL; local detail = smeta:get_int("cartographer:detail") or 1; return get_material_cost(size, detail - 1); end return { paper = 0, pigment = 0, }; end -- Get the converted material value of the given itemstack -- stack: The itemstack to convert -- -- Returns a table with the material values function cartographer.get_material_value(stack) local item_name = stack:get_name(); local item_count = stack:get_count(); for name,mats in pairs(_cartographer.materials_by_name) do if name == item_name then return { paper = (mats.paper or 0) * item_count, pigment = (mats.pigment or 0) * item_count, } end end for group,mats in pairs(_cartographer.materials_by_group) do if minetest.get_item_group(item_name, group) ~= 0 then return { paper = (mats.paper or 0) * item_count, pigment = (mats.pigment or 0) * item_count, } end end return { paper = 0, pigment = 0, }; end local fs = {}; -- Draw a button, with support for enabled/disabled states -- x: The x position of the button -- y: The y position of the button -- w: The width of the button -- h: The height of the button -- id: The element id -- text: The text to display in the button -- enabled: Whether or not the button is enabled -- -- Returns a formspec string function fs.button(x, y, w, h, id, text, enabled) if enabled then return string.format("button[%f,%f;%f,%f;%s;%s]", x, y, w, h, id, text); end return string.format("button[%f,%f;%f,%f;disabled_button;%s]", x, y, w, h, text); end -- Draw a 1px thick horizontal separator formspec element -- y: The y position of the separator -- skin: A 9-slice background skin table -- -- Returns a formspec string function fs.separator(y, skin) return string.format("background9[0.1,%f;10.05,0.01;%s.png;false;%s]", y, skin.texture, tostring(skin.radius)) end -- Draw all the essential formspec data (size, background, styles, tabs) -- w: The width of the formspec -- h: The height of the formspec -- rank: An into defining the 'rank' of the table being displayed -- tab: An int defining the index of the selected tab -- skin: A formspec skin table -- -- Returns a formspec string function fs.header(w, h, rank, tab, skin) local data = "formspec_version[3]" .. string.format("size[%f,%f]", w, h) .. string.format("background9[-0.1,0;1,1;%s.png;true;%s]", skin.background.texture, tostring(skin.background.radius)) .. string.format("background9[0.0625,0.125;%f,%f;%s.png;false;%s]", w - 0.125, h - 0.25, skin.inner_background.texture, tostring(skin.inner_background.radius)) .. string.format("style_type[button;noclip=true;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", skin.tab.texture, skin.tab.hovered_texture, skin.tab.pressed_texture, tostring(skin.tab.radius), skin.tab.font_color) .. string.format("style[tab%d;noclip=true;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", tab, skin.tab.selected_texture, skin.tab.selected_texture, skin.tab.selected_texture, tostring(skin.tab.radius), skin.tab.font_color) .. string.format("button[0.25,-0.425;1.5,0.55;tab1;Materials]", tab) .. string.format("button[1.75,-0.425;1.5,0.55;tab2;Create Map]", tab); if rank >= 2 then data = data .. string.format("button[3.25,-0.425;1.5,0.55;tab3;Copy Map]", tab); end return data .. string.format("style_type[button;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", skin.button.texture, skin.button.hovered_texture, skin.button.pressed_texture, tostring(skin.button.radius), skin.button.font_color) .. string.format("style[disabled_button;bgimg=;bgimg_hovered=;bgimg_pressed=;textcolor=%s]", skin.button.disabled_font_color); end -- Draw material counters from a table's metadata -- x: The x position of the labels -- y: The y position of the labels -- meta: A metadata object containing the material quantities -- skin: A formspec skin table -- -- Returns a formspec string function fs.materials(x, y, meta, skin) return string.format("container[%f,%f]", x, y) .. "formspec_version[3]" .. string.format("background9[0,0.125;1,0.25;%s.png;false;%s]", skin.label.texture, tostring(skin.label.radius)) .. string.format("image[0.125,0.125;0.25,0.25;%s.png]", skin.paper_texture) .. string.format("label[0.375,0.25;%sx %d]", minetest.get_color_escape_sequence(skin.label.font_color), meta:get_int("paper")) .. string.format("background9[1.25,0.125;1,0.25;%s.png;false;%s]", skin.label.texture, tostring(skin.label.radius)) .. string.format("image[1.375,0.125;0.25,0.25;%s.png]", skin.pigment_texture) .. string.format("label[1.625,0.25;%sx %d]", minetest.get_color_escape_sequence(skin.label.font_color), meta:get_int("pigment")) .. "container_end[]"; end -- Draw a label with material costs from a table -- x: The x position of the interface -- y: The y position of the interface -- materials: A table of material costs, with string keys for the material -- names and iteger values -- skin: A formspec skin table -- -- Returns a formspec string function fs.cost(x, y, materials, skin) local data = ""; local i = 0; for name,value in pairs(materials) do data = data .. string.format("label[%f,%f;%s%s x %d]", x, y + (i * 0.25), minetest.get_color_escape_sequence(skin.label.font_color), name, value); i = i + 1; end return data; end -- Draw the material conversion tab UI -- x: The x position of the interface -- y: The y position of the interface -- pos: The table position (for displaying the inventory) -- skin: A formspec skin table -- -- Returns a formspec string function fs.convert(x, y, pos, skin) local meta = minetest.get_meta(pos); local value = cartographer.get_material_value(meta:get_inventory():get_stack("input", 1)); return string.format("container[%f,%f]", x, y) .. "formspec_version[3]" .. inventory_bg(0, 0, 1, 1, skin.slot) .. string.format("list[nodemeta:%d,%d,%d;input;0,0;1,1;]", pos.x, pos.y, pos.z) .. fs.button(1.25, 0.5, 2, 0.5, "convert", "Convert Materials", value.paper + value.pigment > 0) .. fs.cost(1.25, 0.125, value, skin) .. "container_end[]"; end -- Draw the map crafting tab UI -- x: The x position of the interface -- y: The y position of the interface -- pos: The table position (for displaying the inventory) -- meta: A metadata object containing the table settings and material -- quantities -- skin: A formspec skin table -- -- Returns a formspec string function fs.craft(x, y, pos, rank, meta, skin) local cost, is_positive = get_craft_material_cost(meta); local data = string.format("container[%f,%f]", x, y) .. "formspec_version[3]" .. inventory_bg(0, 0.75, 1, 1, skin.slot) .. string.format("list[nodemeta:%d,%d,%d;output;0,0.75;1,1;]", pos.x, pos.y, pos.z) .. fs.button(1.25, 1.25, 2, 0.5, "craft", "Craft Map", is_positive and can_afford(cost, meta)) .. fs.cost(1.25, 0.875, cost, skin); if rank > 1 then local size = "s"; if meta:get_int("size") == SIZE_LARGE then size = "l"; end data = data .. string.format("style[%s;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png]", size, skin.button.selected_texture, skin.button.selected_texture, skin.button.selected_texture) .. "button[0,0;0.5,0.5;s;S]" .. "button[0.5,0;0.5,0.5;l;L]"; end if rank > 2 then local scale = "1x"; if meta:get_int("scale") == SCALE_LARGE then scale = "4x"; end data = data .. string.format("style[%s;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png]", scale, skin.button.selected_texture, skin.button.selected_texture, skin.button.selected_texture) .. "button[3.5,0;0.5,0.5;1x;1x]" .. "button[4.0,0;0.5,0.5;4x;4x]"; end data = data .. string.format("style[%d;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png]", meta:get_int("detail") + 1, skin.button.selected_texture, skin.button.selected_texture, skin.button.selected_texture) .. "button[1.25,0;0.5,0.5;1;1]" .. "button[1.75,0;0.5,0.5;2;2]"; if rank > 1 then data = data .. "button[2.25,0;0.5,0.5;3;3]"; if rank > 2 then data = data .. "button[2.75,0;0.5,0.5;4;4]"; end end return data .. "container_end[]"; end -- Draw the map copying tab UI -- x: The x position of the interface -- y: The y position of the interface -- pos: The table position (for displaying the inventory) -- skin: A formspec skin table -- -- Returns a formspec string function fs.copy(x, y, pos, skin) local meta = minetest.get_meta(pos); local costs = get_copy_material_cost(meta); return string.format("container[%f,%f]", x, y) .. "formspec_version[3]" .. inventory_bg(0, 0, 1, 1, skin.slot) .. string.format("list[nodemeta:%d,%d,%d;copy_input;0,0;1,1;]", pos.x, pos.y, pos.z) .. fs.button(1.25, 0.5, 2, 0.5, "copy", "Copy Map", can_afford(costs, meta)) .. fs.cost(1.25, 0.125, costs, skin) .. inventory_bg(8.75, 0, 1, 1, skin.slot) .. string.format("list[nodemeta:%d,%d,%d;copy_output;8.75,0;1,1;]", pos.x, pos.y, pos.z) .. "container_end[]"; end -- Draw the player's inventory -- x: The x position of the inventory -- y: The y position of the inventory -- skin: A formspec skin table -- -- Returns a formspec string function fs.inv(x, y, skin) return string.format("container[%f,%f]", x, y) .. "formspec_version[3]" .. inventory_bg(0, 0, 8, 4, skin.slot) .. "list[current_player;main;0,0;8,4;]" .. "container_end[]"; end local player_tables = {}; -- Show the table formspec to the specified player -- The player must be recorded in player_tables in order to receive -- a formspec. -- -- player: The player's name local function table_formspec(player) local data = player_tables[player]; local pos = data.pos; if not pos then return; end local meta = minetest.get_meta(pos); local rank = 1; local skin = cartographer.skin.table_skins.simple_table; local name = minetest.get_node(pos).name; if name == "cartographer:standard_table" then rank = 2; skin = cartographer.skin.table_skins.standard_table; elseif name == "cartographer:advanced_table" then rank = 3; skin = cartographer.skin.table_skins.advanced_table; end if data.tab == 1 then minetest.show_formspec(player, "simple_table", fs.header(10.25, 7.375, rank, data.tab, skin) .. fs.materials(0.25, 0.1875, meta, skin) .. fs.separator(0.6875, skin.separator) .. fs.convert(0.25, 0.875, pos, skin) .. fs.separator(2.125, skin.separator) .. fs.inv(0.25, 2.375, skin) ); elseif data.tab == 2 then minetest.show_formspec(player, "simple_table", fs.header(10.25, 8.0, rank, data.tab, skin) .. fs.materials(0.25, 0.1875, meta, skin) .. fs.separator(0.6875, skin.separator) .. fs.craft(0.25, 0.875, pos, rank, meta, skin) .. fs.separator(2.75, skin.separator) .. fs.inv(0.25, 3, skin) ); elseif data.tab == 3 then minetest.show_formspec(player, "simple_table", fs.header(10.25, 7.375, rank, data.tab, skin) .. fs.materials(0.25, 0.1875, meta, skin) .. fs.separator(0.6875, skin.separator) .. fs.copy(0.25, 0.875, pos, skin) .. fs.separator(2.125, skin.separator) .. fs.inv(0.25, 2.375, skin) ); end end -- Called when a player sends input to the server from a formspec -- This callback handles player input in the table formspec -- player: The player who sent the input -- name: The formspec name -- fields: A table containing the input minetest.register_on_player_receive_fields(function(player, name, fields) if name == "simple_table" then local meta = minetest.get_meta(player_tables[player:get_player_name()].pos); if fields["convert"] then local inv = meta:get_inventory(); local stack = inv:get_stack("input", 1); local value = cartographer.get_material_value(stack); if value.paper + value.pigment > 0 then meta:set_int("paper", meta:get_int("paper") + value.paper); meta:set_int("pigment", meta:get_int("pigment") + value.pigment); inv:set_stack("input", 1, ItemStack(nil)); end elseif fields["craft"] then local size = meta:get_int("size"); local detail = meta:get_int("detail"); local scale = meta:get_int("scale"); local cost, is_positive = get_craft_material_cost(meta); if is_positive and can_afford(cost, meta) then meta:set_int("paper", meta:get_int("paper") - cost.paper); meta:set_int("pigment", meta:get_int("pigment") - cost.pigment); local inv = meta:get_inventory(); local stack = inv:get_stack("output", 1); if stack:is_empty() then inv:set_stack("output", 1, cartographer.create_map_item(size, 1 + detail, scale)); else local smeta = stack:get_meta(); smeta:set_int("cartographer:detail", 1 + detail); cartographer.resize_map_item(smeta, size); local map = cartographer.get_map(smeta:get_int("cartographer:map_id")); if map then map.detail = 1 + detail; end inv:set_stack("output", 1, stack); end cartographer.map_sound("cartographer_write", player); end elseif fields["copy"] then local cost = get_copy_material_cost(meta); if can_afford(cost, meta) then meta:set_int("paper", meta:get_int("paper") - cost.paper); meta:set_int("pigment", meta:get_int("pigment") - cost.pigment); cartographer.map_sound("cartographer_write", player); local inv = meta:get_inventory(); inv:set_stack("copy_output", 1, cartographer.copy_map_item(inv:get_stack("copy_input", 1))); end elseif fields["s"] then meta:set_int("size", SIZE_SMALL); elseif fields["l"] then meta:set_int("size", SIZE_LARGE); elseif fields["1"] then meta:set_int("detail", 0); elseif fields["2"] then meta:set_int("detail", 1); elseif fields["3"] then meta:set_int("detail", 2); elseif fields["4"] then meta:set_int("detail", 3); elseif fields["1x"] then meta:set_int("scale", SCALE_SMALL); elseif fields["4x"] then meta:set_int("scale", SCALE_LARGE); elseif fields["tab1"] then player_tables[player:get_player_name()].tab = 1; cartographer.map_sound("cartographer_turn_page", player); elseif fields["tab2"] then player_tables[player:get_player_name()].tab = 2; cartographer.map_sound("cartographer_turn_page", player); elseif fields["tab3"] then player_tables[player:get_player_name()].tab = 3; cartographer.map_sound("cartographer_turn_page", player); end if not fields["quit"] then table_formspec(player:get_player_name()); end end end); -- Called after a table is placed. Sets up the table's inventory and metadata. -- pos: The node's position local function setup_table_node(pos) local meta = minetest.get_meta(pos); meta:get_inventory():set_size("input", 1); meta:get_inventory():set_size("output", 1); meta:get_inventory():set_size("copy_input", 1); meta:get_inventory():set_size("copy_output", 1); meta:set_int("size", SIZE_SMALL); meta:set_int("scale", SCALE_SMALL); meta:set_int("detail", 0); end -- Called when the player tries to put an item into one of the table's -- inventories. -- listname: The name of the inventory the item is being placed in. -- stack: The itemstack -- -- Returns 0 if the place is invalid; otherwise, returns the number of items -- that can be placed. local function table_can_put(_, listname, _, stack, _) if listname == "copy_output" then return 0; end if stack:get_name() ~= "cartographer:map" and (listname == "output" or listname == "copy_input") then return 0; end return stack:get_count(); end -- Called when the player tries to move an item between two of the table's -- inventories. -- to_list: The name of the inventory the item is being placed in. -- count: The number of items being moved -- -- Returns 0 if the move is invalid; otherwise, returns the number of items -- that can be moved. local function table_can_move(_, _, _, to_list, _, count, _) if to_list == "copy_output" then return 0; end return count; end -- Called when a change occurs in a table's inventory -- pos: The node's position -- listname: The name of the changed inventory list local function table_on_items_changed(pos, listname, _, _, _) for player, data in pairs(player_tables) do if vector.equals(pos, data.pos) and ( (data.tab == 1 and listname == "input") or (data.tab == 2 and listname == "output") or (data.tab == 3 and listname == "copy_input")) then table_formspec(player); end end end minetest.register_node("cartographer:simple_table", { description = "Shabby Cartographer's Table", drawtype = "mesh", mesh = cartographer.skin.table_skins.simple_table.node_mesh, tiles = { cartographer.skin.table_skins.simple_table.node_texture }, paramtype2 = "facedir", groups = { choppy = 2, oddly_breakable_by_hand = 2, }, selection_box = { type = "fixed", fixed = { {-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375}, }, }, collision_box = { type = "fixed", fixed = { {-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375}, }, }, on_rightclick = function(pos, node, player, itemstack, pointed_thing) player_tables[player:get_player_name()] = { pos = minetest.get_pointed_thing_position(pointed_thing), tab = 1, }; cartographer.map_sound("cartographer_open_map", player); table_formspec(player:get_player_name()) end, after_place_node = setup_table_node, allow_metadata_inventory_move = table_can_move, allow_metadata_inventory_put = table_can_put, on_metadata_inventory_put = table_on_items_changed, on_metadata_inventory_take = table_on_items_changed, }); minetest.register_node("cartographer:standard_table", { description = "Simple Cartographer's Table", drawtype = "mesh", mesh = cartographer.skin.table_skins.standard_table.node_mesh, tiles = { cartographer.skin.table_skins.standard_table.node_texture }, paramtype2 = "facedir", groups = { choppy = 2, oddly_breakable_by_hand = 2, }, selection_box = { type = "fixed", fixed = { {-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375}, }, }, collision_box = { type = "fixed", fixed = { {-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375}, }, }, on_rightclick = function(pos, node, player, itemstack, pointed_thing) player_tables[player:get_player_name()] = { pos = minetest.get_pointed_thing_position(pointed_thing), tab = 1, }; cartographer.map_sound("cartographer_open_map", player); table_formspec(player:get_player_name()) end, after_place_node = setup_table_node, allow_metadata_inventory_move = table_can_move, allow_metadata_inventory_put = table_can_put, on_metadata_inventory_put = table_on_items_changed, on_metadata_inventory_take = table_on_items_changed, }); minetest.register_node("cartographer:advanced_table", { description = "Advanced Cartographer's Table", drawtype = "mesh", mesh = cartographer.skin.table_skins.advanced_table.node_mesh, tiles = { cartographer.skin.table_skins.advanced_table.node_texture }, paramtype2 = "facedir", groups = { choppy = 2, oddly_breakable_by_hand = 2, }, selection_box = { type = "fixed", fixed = { {-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375}, }, }, collision_box = { type = "fixed", fixed = { {-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375}, }, }, on_rightclick = function(pos, node, player, itemstack, pointed_thing) player_tables[player:get_player_name()] = { pos = minetest.get_pointed_thing_position(pointed_thing), tab = 1, }; cartographer.map_sound("cartographer_open_map", player); table_formspec(player:get_player_name()) end, after_place_node = setup_table_node, allow_metadata_inventory_move = table_can_move, allow_metadata_inventory_put = table_can_put, on_metadata_inventory_put = table_on_items_changed, on_metadata_inventory_take = table_on_items_changed, }); function cartographer.register_map_material_name(name, material, value) if _cartographer.materials_by_name[name] == nil then _cartographer.materials_by_name[name] = { [material] = value or 1, }; else _cartographer.materials_by_name[name][material] = value; end end function cartographer.register_map_material_group(name, material, value) if _cartographer.materials_by_group[name] == nil then _cartographer.materials_by_group[name] = { [material] = value or 1, }; else _cartographer.materials_by_group[name][material] = value; end end