From 36d1f6f573e748163dcb17b08651c106bced4051 Mon Sep 17 00:00:00 2001 From: Treer Date: Sat, 1 Feb 2020 16:51:58 +1100 Subject: [PATCH] Images instead of ASCII diagrams in Book of Portals --- README.md | 2 +- init.lua | 20 +--- portal_api.lua | 113 ++++++++++++++----- portal_api.txt | 69 ++++++----- portal_examples.lua | 36 ++---- textures/nether_book_diagram_circular.png | Bin 0 -> 7465 bytes textures/nether_book_diagram_platform.png | Bin 0 -> 5816 bytes textures/nether_book_diagram_traditional.png | Bin 0 -> 9232 bytes 8 files changed, 135 insertions(+), 105 deletions(-) create mode 100644 textures/nether_book_diagram_circular.png create mode 100644 textures/nether_book_diagram_platform.png create mode 100644 textures/nether_book_diagram_traditional.png diff --git a/README.md b/README.md index 7659f0b..46d7b5d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ SOFTWARE. * `nether_portal_ignite.ogg` is a derivative of "Flame Ignition" by hykenfreak (https://freesound.org/people/hykenfreak), used under CC BY 3.0. "Nether Portal ignite" is licensed under CC BY 3.0 by Treer. ### [Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/) - * `nether_book_`* (files starting with "nether_book"): Treer, 2019 + * `nether_book_`* (files starting with "nether_book"): Treer, 2019-2020 * `nether_portal_ignition_failure.ogg`: Treer, 2019 * `nether_particle_anim`* (files starting with "nether_particle_anim"): Treer, 2019 diff --git a/init.lua b/init.lua index a5255a3..aaec842 100644 --- a/init.lua +++ b/init.lua @@ -75,24 +75,12 @@ if nether.NETHER_REALM_ENABLED then shape = nether.PortalShape_Traditional, frame_node_name = "default:obsidian", wormhole_node_color = 0, -- 0 is magenta + title = S("Nether Portal"), + book_of_portals_pagetext = S([[Construction requires 14 blocks of obsidian, which we found deep underground where water had solidified molten rock. The finished frame is four blocks wide, five blocks high, and stands vertically, like a doorway. - -- Warning: "Four per Em" spaces have been used to align the diagram in this text, rather - -- than ASCII spaces. If Minetest changes font this may need to be updated. - book_of_portals_pagetext = S([[ ──══♦♦♦◊ The Nether ◊♦♦♦══── +This opens to a truly hellish place, though for small mercies the air there is still breathable. There is an intriguing dimensional mismatch happening between this realm and ours, as after opening the second portal into it we observed that 10 strides taken in the Nether appear to be an equivalent of @1 in the natural world. - Requiring 14 blocks of obsidian, which we found deep underground where water had solidified molten rock. The frame must be constructed in the following fashion: - - ┌═╤═╤═╤═╗ - ├─╥─┴─┼─╢ - ├─╢         ├─╢ - ├─╢         ├─╢ four blocks wide - ├─╚═╤═╡─╢ five blocks high - └─┴─┴─┴─┘ Standing vertically, like a doorway - - This opens to a truly hellish place, though for small mercies the air there is still breathable. There is an intriguing dimensional mismatch happening between this realm and ours, as after opening the second portal into it we observed that 10 strides taken in the Nether appear to be an equivalent of @1 in the natural world. - - The expedition parties have found no diamonds or gold, and after an experienced search party failed to return from the trail of a missing expedition party, I must conclude this is a dangerous place. - ]], 10 * nether.FASTTRAVEL_FACTOR), +The expedition parties have found no diamonds or gold, and after an experienced search party failed to return from the trail of a missing expedition party, I must conclude this is a dangerous place.]], 10 * nether.FASTTRAVEL_FACTOR), is_within_realm = function(pos) -- return true if pos is inside the Nether return pos.y < nether.DEPTH diff --git a/portal_api.lua b/portal_api.lua index 8769ca7..293cef7 100644 --- a/portal_api.lua +++ b/portal_api.lua @@ -123,8 +123,10 @@ nether.PortalShape_Traditional = { name = "Traditional", size = vector.new(4, 5, 1), -- size of the portal, and not necessarily the size of the schematic, -- which may clear area around the portal. - schematic_filename = nether.path .. "/schematics/nether_portal.mts", - is_horizontal = false, -- whether the wormhole is a vertical or horizontal surface + schematic_filename = nether.path .. "/schematics/nether_portal.mts", + is_horizontal = false, -- whether the wormhole is a vertical or horizontal surface + diagram_image_texture = "nether_book_diagram_traditional.png", -- The diagram to be shown in the Book of Portals + diagram_image_aspect = 2.11, -- gives the vertical size of the image when multiplied by the width -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos get_schematicPos_from_anchorPos = function(anchorPos, orientation) @@ -271,8 +273,10 @@ nether.PortalShape_Circular = { name = "Circular", size = vector.new(7, 7, 1), -- size of the portal, and not necessarily the size of the schematic, -- which may clear area around the portal. - schematic_filename = nether.path .. "/schematics/nether_portal_circular.mts", - is_horizontal = false, -- whether the wormhole is a vertical or horizontal surface + schematic_filename = nether.path .. "/schematics/nether_portal_circular.mts", + is_horizontal = false, -- whether the wormhole is a vertical or horizontal surface + diagram_image_texture = "nether_book_diagram_circular.png", -- The diagram to be shown in the Book of Portals + diagram_image_aspect = 1.62, -- gives the vertical size of the image when multiplied by the width -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos get_schematicPos_from_anchorPos = function(anchorPos, orientation) @@ -401,8 +405,10 @@ nether.PortalShape_Platform = { name = "Platform", size = vector.new(5, 2, 5), -- size of the portal, and not necessarily the size of the schematic, -- which may clear area around the portal. - schematic_filename = nether.path .. "/schematics/nether_portal_platform.mts", - is_horizontal = true, -- whether the wormhole is a vertical or horizontal surface + schematic_filename = nether.path .. "/schematics/nether_portal_platform.mts", + is_horizontal = true, -- whether the wormhole is a vertical or horizontal surface + diagram_image_texture = "nether_book_diagram_platform.png", -- The diagram to be shown in the Book of Portals + diagram_image_aspect = 0.65, -- gives the vertical size of the image when multiplied by the width -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos get_schematicPos_from_anchorPos = function(anchorPos, orientation) @@ -1379,7 +1385,7 @@ function run_wormhole(timerPos, time_elapsed) end -local function create_book(item_name, inventory_description, inventory_image, title, author, page1_text, page2_text) +local function create_book(item_name, inventory_description, inventory_image, title, author, chapters) local display_book = function(itemstack, user, pointed_thing) local player_name = user:get_player_name() @@ -1388,15 +1394,55 @@ local function create_book(item_name, inventory_description, inventory_image, ti local formspec = "size[18,12.122]" .. + "background[0,0;18,11;nether_book_background.png;true]".. + "image_button_exit[17.3,0;0.8,0.8;nether_book_close.png;;]".. "label[3.1,0.5;" .. minetest.formspec_escape(title) .. "]" .. - "label[3.6,0.9;" .. author .. "]" .. + "label[3.6,0.9;" .. author .. "]" - "textarea[ 0.9,1.7;7.9,12.0;;" .. minetest.formspec_escape(page1_text) .. ";]" .. - "textarea[10.1,0.8;7.9,12.9;;" .. minetest.formspec_escape(page2_text) .. ";]" .. + local image_x_adj = -0.4 + local image_width = 1.6 + local image_padding = 0.06 - "background[0,0;18,11;nether_book_background.png;true]".. - "image_button_exit[17.3,0;0.8,0.8;nether_book_close.png;;]" + for i, chapter in ipairs(chapters) do + local left = 0.9 + local top = 1.7 + local width = 7.9 + local height = 12.0 + local item_number = i + local items_on_page = math.floor(#chapters / 2) + if i > items_on_page then + -- page 2 + left = 10.1 + top = 0.8 + height = 12.9 + item_number = i - items_on_page + items_on_page = #chapters - items_on_page + end + + local available_height = (height - top) / items_on_page + local y = top + (item_number - 1) * available_height + + -- add chapter title + local title_height = 0 + if chapter.title ~= nil then + title_height = 0.6 + formspec = formspec .. "label[".. left + 1.5 .. "," + .. y .. "; ──══♦♦♦◊ " .. minetest.formspec_escape(chapter.title) .. " ◊♦♦♦══──]" + end + + -- add chapter image + local x_offset = 0 + if chapter.image ~= nil then + x_offset = image_width + image_x_adj + image_padding + formspec = formspec .. "image[" .. left + image_x_adj .. "," .. y + title_height .. ";" .. image_width .. "," + .. image_width * chapter.aspect .. ";" .. chapter.image .. "]" + end + + -- add chapter text + formspec = formspec .. "textarea[" .. left + x_offset .. "," .. y + title_height .. ";" .. width - x_offset .. "," + .. available_height - title_height .. ";;" .. minetest.formspec_escape(chapter.text) .. ";]" + end minetest.show_formspec(player_name, item_name, formspec) end @@ -1438,14 +1484,14 @@ end -- A book the player can read to lean how to build the different portals local function create_book_of_portals() - local page1_text - local page2_text = "" + local chapters = {} + local intro_text -- tell the player how many portal types there are if nether.registered_portals_count == 1 then - page1_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only one can I confirm as being more than merely a story.") + intro_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only one can I confirm as being more than merely a story.") else - page1_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only @1 can I confirm as being more than merely stories.", nether.registered_portals_count) + intro_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only @1 can I confirm as being more than merely stories.", nether.registered_portals_count) end -- tell the player how to ignite portals @@ -1453,27 +1499,31 @@ local function create_book_of_portals() if ignition_item_name ~= nil and minetest.registered_items[ignition_item_name] ~= nil then ignition_item_description = minetest.registered_items[ignition_item_name].description end - page1_text = page1_text .. - S("\n\nThe key to opening such a doorway is to strike the frame with a @1, at which point the very air inside begins to crackle and glow.\n\n\n", string.lower(ignition_item_description)) + intro_text = intro_text .. + S("\n\nThe key to opening such a doorway is to strike the frame with a @1, at which point the very air inside begins to crackle and glow.", string.lower(ignition_item_description)) + + chapters[#chapters + 1] = {text = intro_text} -- Describe how to create each type of portal, or perhaps just give clues or flavor text, -- but ensure the Nether is always listed first on the first page so other definitions can -- refer to it (pairs() returns order based on a random hash). - local i = 1 + local portalDefs_in_order = {} if nether.registered_portals["nether_portal"] then - page1_text = page1_text .. nether.registered_portals["nether_portal"].book_of_portals_pagetext .. "\n\n\n" - i = i + 1 + portalDefs_in_order[#portalDefs_in_order + 1] = nether.registered_portals["nether_portal"] end for portalName, portalDef in pairs(nether.registered_portals) do if portalName ~= "nether_portal" then - if i <= nether.registered_portals_count / 2 then - page1_text = page1_text .. portalDef.book_of_portals_pagetext .. "\n\n\n" - else - page2_text = page2_text .. portalDef.book_of_portals_pagetext .. "\n\n\n" - end - i = i + 1 + portalDefs_in_order[#portalDefs_in_order + 1] = portalDef end end + for _, portalDef in ipairs(portalDefs_in_order) do + chapters[#chapters + 1] = { + text = portalDef.book_of_portals_pagetext, + image = portalDef.shape.diagram_image_texture, + aspect = portalDef.shape.diagram_image_aspect, + title = portalDef.title + } + end local previouslyRegistered = minetest.registered_items["nether:book_of_portals"] ~= nil @@ -1481,10 +1531,9 @@ local function create_book_of_portals() "nether:book_of_portals", S("Book of Portals"), "nether_book_of_portals.png", - S("A treatise on Rifts and Portals"), + S("A definitive guide to Rifts and Portals"), "Riccard F. Burton", -- perhaps a Richard F. Burton of an alternate universe - page1_text, - page2_text + chapters ) if not previouslyRegistered and nether.PORTAL_BOOK_LOOT_WEIGHTING > 0 and nether.registered_portals_count > 0 then @@ -1563,7 +1612,7 @@ function register_frame_node(frame_node_name) extended_node_def.on_blast = function(pos, intensity) if DEBUG then minetest.chat_send_all("portal frame material: blast") end extinguish_portal(pos, frame_node_name, extended_node_def.replaced_by_portalapi.on_blast == nil) - if extended_node_def.replaced_by_portalapi.on_blast ~= nil then + if extended_node_def.replaced_by_portalapi.on_blast ~= nil then extended_node_def.replaced_by_portalapi.on_blast(pos, intensity) else minetest.remove_node(pos) @@ -1745,6 +1794,8 @@ end -- The fallback defaults for registered portaldef tables local portaldef_default = { + title = S("Untitled portal"), + book_of_portals_pagetext = S("We know almost nothing about this portal"), shape = nether.PortalShape_Traditional, wormhole_node_name = "nether:portal", wormhole_node_color = 0, diff --git a/portal_api.txt b/portal_api.txt index 4991281..dd00bf7 100644 --- a/portal_api.txt +++ b/portal_api.txt @@ -5,8 +5,8 @@ The portal system used to get to the Nether can be used to create portals to other realms. Pick a node type to have your portals built from, a shape in which the -portals must be built, and provide 3 functions for portals to find their -destination with: +portals must be built, and provide 3 functions for portals to find their +destination with: * `find_realm_anchorPos(surface_anchorPos)` * `find_surface_anchorPos(realm_anchorPos)` * `is_within_realm(pos)` @@ -16,7 +16,7 @@ Optionally decorate by choosing portal colors, particles, media etc. See `init.lua` and `portal_examples.lua` for examples of 3 different portals. Portal code is more efficient when each type of portal uses a different type -of node to build its frame out of - consider creating your own node for +of node to build its frame out of - consider creating your own node for players to build portals from. However it is possible to register more than one kind of portal with the same frame material — such as obsidian — provided the size of the PortalShape is distinct from any other type of portal that is @@ -24,15 +24,15 @@ using the same node for its frame, and portal sizes remain small. Stone is not a good choice for portal frame nodes as the Minetest engine may convert it into terrain nodes if the biome-pass occurs after the portal is -created. Similarly, avoid using nodes which may be replaced by ABMs or +created. Similarly, avoid using nodes which may be replaced by ABMs or docoration functions without triggering the node's `on_destruct` handler. Realms ------ -This API uses the concept of a "realm" for each type of portal. If a portal is -outside its realm then it links to a portal inside the realm, if a portal is +This API uses the concept of a "realm" for each type of portal. If a portal is +outside its realm then it links to a portal inside the realm, if a portal is inside its realm then it links to the outside. You get to decide what consitutes your realm by implementing the function @@ -47,7 +47,7 @@ any time a portal is opened it will use `find_surface_anchorPos()`. Note that the name "find_surface_anchorPos" is a Nether-centric misnomer, as different types of portals are free to use different definitions of a realm -such that leaving the realm might not be synonymous with travelling to the +such that leaving the realm might not be synonymous with travelling to the surface. @@ -55,18 +55,18 @@ Helper functions ---------------- * `nether.volume_is_natural(minp, maxp)`: returns a boolean - * use this when determining where to spawn a portal, to avoid overwriting - player builds. It checks the area for any nodes that aren't ground or + * use this when determining where to spawn a portal, to avoid overwriting + player builds. It checks the area for any nodes that aren't ground or trees. Water will fail this test, unless it is unemerged. -* `nether.find_surface_target_y(target_x, target_z, portal_name)`: returns a +* `nether.find_surface_target_y(target_x, target_z, portal_name)`: returns a suitable anchorPos * Can be used when implementing custom find_surface_anchorPos() functions * portal_name is optional, providing it allows existing portals on the surface to be reused. -* `nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)`: returns +* `nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)`: returns (anchorPos, orientation), or nil if no portal was found within the distance_limit. * A y_factor of 0 means y does not affect the distance_limit, a y_factor @@ -83,7 +83,7 @@ API functions Call these functions only at load time: * `nether.register_portal(name, portal_definition)` - * Returns true on success. Can return false if the portal definition + * Returns true on success. Can return false if the portal definition clashes with a portal already registered by another mod, e.g. if the size and frame node is not unique. A false return value should be handled, you could: @@ -95,18 +95,18 @@ Call these functions only at load time: * Unregisters the portal from the engine, and deletes the entry with key `name` from `nether.registered_portals` and associated internal tables. * Returns true on success - * You will probably never need to call this, it exists only for completeness. + * You will probably never need to call this, it exists only for completeness. * `nether.register_portal_ignition_item(name, ignition_failure_sound)` - * ignition_failure_sound is optional, it plays any time an attempt to use + * ignition_failure_sound is optional, it plays any time an attempt to use the item occurs if a portal is not ignited. * `nether.register_wormhole_node(name, nodedef_overrides)` * Can be used to register wormhole nodes with a different post_effect_color from the "nether:portal" node. "Post effect color" is the tint the world - takes on when you are standing inside a portal. `post_effect_color` is the + takes on when you are standing inside a portal. `post_effect_color` is the only key/value that is needed in the nodedef_overrides table to achieve that, but the function allows any nodedef key/value to be specified/overridden. - * After `register_wormhole_node()`, invoke `register_portal()` and include - `wormhole_node_name` in the portal_definition, assigning it the name of the + * After `register_wormhole_node()`, invoke `register_portal()` and include + `wormhole_node_name` in the portal_definition, assigning it the name of the new wormhole node. @@ -117,10 +117,10 @@ Used by `nether.register_portal`. { frame_node_name = "default:obsidian", - -- Required. For best results, have your portal constructed of a + -- Required. For best results, have your portal constructed of a -- material nobody else is using. - shape = nether.PortalShape_Traditional, + shape = nether.PortalShape_Traditional, -- Optional. -- Shapes available are: -- nether.PortalShape_Traditional (default) @@ -135,7 +135,7 @@ Used by `nether.register_portal`. wormhole_node_color = 0, -- Optional. Defaults to 0/magenta. - -- A value from 0 to 7 corresponding to the color of pixels in + -- A value from 0 to 7 corresponding to the color of pixels in -- nether_portals_palette.png: -- 0 traditional/magenta -- 1 black @@ -151,8 +151,8 @@ Used by `nether.register_portal`. -- if not specified. particle_texture = "image.png", - -- Optional. Hardware colouring (i.e. particle_color) is applied to - -- this texture, use particle_texture_colored instead if you want to + -- Optional. Hardware colouring (i.e. particle_color) is applied to + -- this texture, use particle_texture_colored instead if you want to -- use the colors of the image. -- Animation and particle scale may also be specified, e.g: -- particle_texture = { @@ -164,7 +164,7 @@ Used by `nether.register_portal`. -- length = 1, -- }, -- scale = 1.5 - -- }, + -- }, -- See lua_api.txt for Tile Animation definition -- Some animated and non-animated textures are provided by this mod: -- nether_particle.png (original) @@ -173,10 +173,23 @@ Used by `nether.register_portal`. -- nether_particle_anim3.png (sparks) -- nether_particle_anim4.png (particles) + title = "Gateway to Moria", + -- Optional. Provides a title for the portal. + -- Currently this title is only used by the Book of Portals. + + book_of_portals_pagetext = "Everything I need the player to know", + -- Optional. Provides the text for the portal in the Book of Portals. + -- The Book of Portals is a book that can be found in chests, and + -- provides players with instructions on how to build and use the + -- portal, so be sure to mention the node type the frame must be built + -- from. + -- This can also provide flavortext or details about where the portal + -- will take the player. + sounds = { - ambient = , - -- if the ambient SimpleSoundSpec is a table it can also contain a - -- "length" int, which is the number of seconds to wait before + ambient = , + -- if the ambient SimpleSoundSpec is a table it can also contain a + -- "length" int, which is the number of seconds to wait before -- repeating the ambient sound. Default is 3. ignite = , extinguish = , @@ -185,7 +198,7 @@ Used by `nether.register_portal`. -- sounds is optional within_realm = function(pos), - -- Required. Return true if a portal at pos is in the realm, rather + -- Required. Return true if a portal at pos is in the realm, rather than the surface world. find_realm_anchorPos = function(surface_anchorPos), @@ -207,7 +220,7 @@ Used by `nether.register_portal`. -- are free to use different definitions of a realm such that leaving -- the realm might not be synonymous with travelling to the surface. -- If orientation is not specified then the orientation of the realm - -- portal will be used. + -- portal will be used. -- If the location of an existing portal is returned then include the -- orientation, otherwise the existing portal could be overwritten by -- a new one with the orientation of the realm portal. diff --git a/portal_examples.lua b/portal_examples.lua index ea78389..0b402ad 100644 --- a/portal_examples.lua +++ b/portal_examples.lua @@ -75,22 +75,10 @@ if minetest.settings:get_bool("nether_enable_portal_example_floatlands", ENABLE_ }, scale = 1.5 }, - book_of_portals_pagetext = S([[ ──══♦♦♦◊ The Floatlands ◊♦♦♦══── + title = S("Floatlands Portal"), + book_of_portals_pagetext = S([[Requiring 21 blocks of ice, and constructed in the shape of a 3 × 3 platform with walls, or like a bowl. A finished platform is 2 blocks high, and 5 blocks wide at the widest in both directions. - Requiring 21 blocks of ice, and constructed in the shape of a 3 × 3 platform with walls, or like a bowl: - -       ┌─┬─┬─┐ - ┌─┼─┴─┴─┼─┐ Plan view (looking down from above) - ├─┤               ├─┤ - ├─┤               ├─┤ five blocks wide - └─┼─┬─┬─┼─┘ in both directions -       └─┴─┴─┘ - - ┌─┬─┬─┬─┬─┐ Side view (looking from either side) - └─┼─┼─┼─┼─┘ -       └─┴─┴─┘       two blocks deep - - This portal is different to the others, rather than acting akin to a doorway it appears to the eye more like a small pool of water which can be stepped into. Upon setting foot in the portal we found ourselves at a tremendous altitude.@1]], +This portal is different to the others, rather than acting akin to a doorway it appears to the eye more like a small pool of water which can be stepped into. Upon setting foot in the portal we found ourselves at a tremendous altitude.@1]], floatlands_flavortext), is_within_realm = function(pos) -- return true if pos is inside the Nether @@ -129,22 +117,12 @@ if minetest.settings:get_bool("nether_enable_portal_example_surfacetravel", ENAB shape = nether.PortalShape_Circular, frame_node_name = "default:tinblock", wormhole_node_color = 4, -- 4 is cyan - book_of_portals_pagetext = S([[ ──══♦♦♦◊ Surface portal ◊♦♦♦══── + title = S("Surface Portal"), + book_of_portals_pagetext = S([[Requiring 16 blocks of tin and constructed in a circular fashion, a finished frame is seven blocks wide, seven blocks high, and stands vertically like a doorway. - Requiring 16 blocks of tin, the frame must be constructed in the following fashion: +These travel a distance along the ground, and even when constructed deep underground will link back up to the surface. They appear to favor a strange direction, with the exit portal linking back only for as long as the portal stays open — attempting to reopen a portal from the exit doorway leads to a new destination along this favored direction. It has stymied our ability to study the behavior of these portals because without constructing dual portals and keeping both open it's hard to step through more than one and still be able to return home. -             ┌═╤═╤═╗ -       ┌═┼─┴─┴─┼═╗ - ┌═┼─┘               └─┼═╗ - ├─╢                           ├─╢ - ├─╢                           ├─╢ seven blocks wide - └─╚═╗               ┌═╡─┘ seven blocks high -       └─╚═╤═╤═┼─┘       in a circular shape -             └─┴─┴─┘             standing vertically, like a doorway - - These travel a distance along the ground, and even when constructed deep underground will link back up to the surface. They appear to favor a strange direction, with the exit portal linking back only for as long as the portal stays open — attempting to reopen a portal from the exit doorway leads to a new destination along this favored direction. It has stymied our ability to study the behavior of these portals because without constructing dual portals and keeping both open it's hard to step through more than one and still be able to return home. - - Due to such difficulties, we never learned what determines the direction and distance a matching twin portal will appear, and I have lost my friend and protégé. In cavalier youth and with little more than a rucksack, Coudreau has decided to follow the chain as far as it goes, and has not been seen since. Coudreau believes it works in epicycles, but I am not convinced. Still, I cling to the hope that one day the portal will open and Coudreau will step out from whichever place leads to this one, perhaps with an epic tale to tell.]]), +Due to such difficulties, we never learned what determines the direction and distance where the matching twin portal will appear, and I have lost my friend and protégé. In cavalier youth and with little more than a rucksack, Coudreau has decided to follow the chain as far as it goes, and has not been seen since. Coudreau believes it works in epicycles, but I am not convinced. Still, I cling to the hope that one day the portal will open and Coudreau will step out from whichever place leads to this one, perhaps with an epic tale to tell.]]), is_within_realm = function(pos) -- Always return true, because these portals always just take you around the surface diff --git a/textures/nether_book_diagram_circular.png b/textures/nether_book_diagram_circular.png new file mode 100644 index 0000000000000000000000000000000000000000..5eac5255e463ee815d237126963e5ddbef527c7a GIT binary patch literal 7465 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 T?bnBYIFb@jU9LgKBI5r5LMs2@ literal 0 HcmV?d00001 diff --git a/textures/nether_book_diagram_platform.png b/textures/nether_book_diagram_platform.png new file mode 100644 index 0000000000000000000000000000000000000000..810695e139e3d663c8002723bcde4333e0e4a11c GIT binary patch literal 5816 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%0DLG%Y<>ivASp*;?( zC91?-wYO*yPp%rL-zaK$d=wv$qwab?RRf3E#2Pcv^&9i-a1JjsRwAyZ<9H~^hevve zwcKIsY5N8RSi_?lm`W^0(q&8XUyA2*o`?-f6*z8OH%FP`(=O5YC5|Ke{D^*w)Q`s> z^EMI-NQ~M<)kf#6BIL8aD%s)p$Bv)Cg|l33m5(&v;jOBv&S-V!1k74i(t+TMBoshv zU`5^XN+!7EW4U0SISa|>q8l+dL44}Ov#;|sY~Uq74+=!7xt1rRiq}4-t1D64b05rv z*Z~kZExs7VP+_p(vnLsZ9)_OS#)r?dxO9l&`SrF%N#p#ZKD+q`(cHz#mm01RsVw9| zpFYB>D>obo!_6jfJMu7$XZ@Nty;1dATePrs-A%;mN#m^Rw%eQ?9U*ma;09B>x7;RJKj+c8W%@I>gZj9`8 zFwHG9*az}Fdhm=M`AXNl)crkEr|rAwJ0AR9 zs1^`p1SityrUgHVZnb{)PRp%&2#KAoYkhf7kM(7ShPu< z8Vc$CR!|9Qf|mS6X?_=Z;iuzYh{irrNOnr=8O+-=sBe0IY?8e}5+|)O0jdbxeh7PK z)n~LB>*vr*`I!mKQVmO{*BHgLua$F`;K^8OWoIQ`v1Q9}e$wt?pAhDFEQrJBiE9RO z;E{N~w4S8z+-U2j^4*(}3}lgWBA zUJ?G7cL9tT2QufqR>@h@Iu|76D&`9}^HuFXWnpN28H8PE*d-{_Enr^q(H*VZ$d z-fV~_+C04Y-cDN5Bu;T~Y@wO%uzj@x_9-5cq-7Q3R@llSm5Z9YW}qkr%`;Kgom! zljtKM( zZTP{F)Vy=oJX&0G&n}TJfQ)V=q~9(wChNz6)%w*3VT@1QBz!(N>N*U59gCZ!^J}1n zcpI2y5nIi*lZ*xNPejC*rjrCm^%o`$)S`D5i@v&u$p0Q4Q6>JO8CsgMc1E(rj@8ZDJ!Iz}Xkr-Z z5_&N)RnM4i$3K<1$Gt5GPO;a?qo?CpmQ2KYY82}0XOdJ_>NQn~2B24&ieH}x>0XFx zS{MX|(vaHxQXeKw|^t%fW zBA!H7sTh2cDP6!qxfMg0G3MT8*suGCYAeSNUjN8A&P^iOC7pP+T5)B-l?wmNr|$YO zPr>f${SRv} z%&!_&tcDtRCWk#Of2S(Hm>gIA6!I`YXnUCH=cu=%QOlshWNk0rd7+tnobfX)_Rn*{ zf*^Q#N~ucH&{{#W7{}wcPKzd+hcqBk)lI;P{8uWDO<+k`rZdyY{ar3v41^KI{-(7f z#1Kzj_y-&RUr z@OlSMh@55ezw=y)u+H6NJ-j{82W@Yd;|zgK|E7qmA-{y*O8nuyx^nT8R)JT43<=p) z1A?(`SUaBe>PvtPW3^=sV<+*#e;3TX0yR8!`Xkr!5~4dj#OJFl1af-wtLSh1r_TqA zxdrJw{XLBL#}{T6RzlewH4jW!7fUDptHGYViAmW{QAt$&AG%KoLRHCI=r~@_-E*T) z7u+g3z-%11W{WGzPeAz}fr6*k h8y{y!Cm29TKu7@iU*6zT&+mUc0IH;^SR-$R_&-A+oy!0K literal 0 HcmV?d00001 diff --git a/textures/nether_book_diagram_traditional.png b/textures/nether_book_diagram_traditional.png new file mode 100644 index 0000000000000000000000000000000000000000..6762d450865b40c59c1e220856493f94197991c3 GIT binary patch literal 9232 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 Q|KR|685QY9Nz<_Z2kGuWZ~y=R literal 0 HcmV?d00001