Merge updated Nether portals, https://git.minetest.land/Wuzzy/MineClone2/issues/804
[MineClone/MineClone2.git] / mods / ITEMS / mcl_portals / portal_nether.lua
blob08cb654b8ca9c3ce4fcf84425b252dbb8b7ab71e
1 local S = minetest.get_translator("mcl_portals")
3 -- Parameters
5 -- Portal frame sizes
6 local FRAME_SIZE_X_MIN = 4
7 local FRAME_SIZE_Y_MIN = 5
8 local FRAME_SIZE_X_MAX = 23
9 local FRAME_SIZE_Y_MAX = 23
11 local PORTAL_NODES_MIN = 5
12 local PORTAL_NODES_MAX = (FRAME_SIZE_X_MAX - 2) * (FRAME_SIZE_Y_MAX - 2)
14 local TELEPORT_COOLOFF = 3 -- after player was teleported, for this many seconds they won't teleported again
15 local MOB_TELEPORT_COOLOFF = 14 -- after mob was teleported, for this many seconds they won't teleported again
16 local TOUCH_CHATTER_TIME = 1 -- prevent multiple teleportation attempts caused by multiple portal touches, for this number of seconds
17 local TOUCH_CHATTER_TIME_US = TOUCH_CHATTER_TIME * 1000000
18 local TELEPORT_DELAY = 3 -- seconds before teleporting in Nether portal (4 minus ABM interval time)
19 local DESTINATION_EXPIRES = 60 * 1000000 -- cached destination expires after this number of microseconds have passed without using the same origin portal
21 local PORTAL_SEARCH_HALF_CHUNK = 40 -- greater values may slow down the teleportation
22 local PORTAL_SEARCH_ALTITUDE = 128
24 -- Table of objects (including players) which recently teleported by a
25 -- Nether portal. Those objects have a brief cooloff period before they
26 -- can teleport again. This prevents annoying back-and-forth teleportation.
27 mcl_portals.nether_portal_cooloff = {}
28 local touch_chatter_prevention = {}
30 local overworld_ymin = math.max(mcl_vars.mg_overworld_min, -31)
31 local overworld_ymax = math.min(mcl_vars.mg_overworld_max_official, 63)
32 local nether_ymin = mcl_vars.mg_bedrock_nether_bottom_min
33 local nether_ymax = mcl_vars.mg_bedrock_nether_top_max
34 local overworld_dy = overworld_ymax - overworld_ymin + 1
35 local nether_dy = nether_ymax - nether_ymin + 1
37 local node_particles_allowed = minetest.settings:get("mcl_node_particles") or "none"
38 local node_particles_levels = {
39 high = 3,
40 medium = 2,
41 low = 1,
42 none = 0,
44 local node_particles_allowed_level = node_particles_levels[node_particles_allowed]
47 -- Functions
49 -- https://git.minetest.land/Wuzzy/MineClone2/issues/795#issuecomment-11058
50 -- A bit simplified Nether fast travel ping-pong formula and function by ryvnf:
51 local function nether_to_overworld(x)
52 return 30912 - math.abs(((x * 8 + 30912) % 123648) - 61824)
53 end
55 -- Destroy portal if pos (portal frame or portal node) got destroyed
56 local function destroy_nether_portal(pos)
57 local meta = minetest.get_meta(pos)
58 local node = minetest.get_node(pos)
59 local nn, orientation = node.name, node.param2
60 local obsidian = nn == "mcl_core:obsidian"
62 local has_meta = minetest.string_to_pos(meta:get_string("portal_frame1"))
63 if has_meta then
64 meta:set_string("portal_frame1", "")
65 meta:set_string("portal_frame2", "")
66 meta:set_string("portal_target", "")
67 meta:set_string("portal_time", "")
68 end
69 local check_remove = function(pos, orientation)
70 local node = minetest.get_node(pos)
71 if node and (node.name == "mcl_portals:portal" and (orientation == nil or (node.param2 == orientation))) then
72 minetest.log("action", "[mcl_portal] Destroying Nether portal at " .. minetest.pos_to_string(pos))
73 return minetest.remove_node(pos)
74 end
75 end
76 if obsidian then -- check each of 6 sides of it and destroy every portal:
77 check_remove({x = pos.x - 1, y = pos.y, z = pos.z}, 0)
78 check_remove({x = pos.x + 1, y = pos.y, z = pos.z}, 0)
79 check_remove({x = pos.x, y = pos.y, z = pos.z - 1}, 1)
80 check_remove({x = pos.x, y = pos.y, z = pos.z + 1}, 1)
81 check_remove({x = pos.x, y = pos.y - 1, z = pos.z})
82 check_remove({x = pos.x, y = pos.y + 1, z = pos.z})
83 return
84 end
85 if not has_meta then -- no meta means repeated call: function calls on every node destruction
86 return
87 end
88 if orientation == 0 then
89 check_remove({x = pos.x - 1, y = pos.y, z = pos.z}, 0)
90 check_remove({x = pos.x + 1, y = pos.y, z = pos.z}, 0)
91 else
92 check_remove({x = pos.x, y = pos.y, z = pos.z - 1}, 1)
93 check_remove({x = pos.x, y = pos.y, z = pos.z + 1}, 1)
94 end
95 check_remove({x = pos.x, y = pos.y - 1, z = pos.z})
96 check_remove({x = pos.x, y = pos.y + 1, z = pos.z})
97 end
99 minetest.register_node("mcl_portals:portal", {
100 description = S("Nether Portal"),
101 _doc_items_longdesc = S("A Nether portal teleports creatures and objects to the hot and dangerous Nether dimension (and back!). Enter at your own risk!"),
102 _doc_items_usagehelp = S("Stand in the portal for a moment to activate the teleportation. Entering a Nether portal for the first time will also create a new portal in the other dimension. If a Nether portal has been built in the Nether, it will lead to the Overworld. A Nether portal is destroyed if the any of the obsidian which surrounds it is destroyed, or if it was caught in an explosion."),
104 tiles = {
105 "blank.png",
106 "blank.png",
107 "blank.png",
108 "blank.png",
110 name = "mcl_portals_portal.png",
111 animation = {
112 type = "vertical_frames",
113 aspect_w = 16,
114 aspect_h = 16,
115 length = 0.5,
119 name = "mcl_portals_portal.png",
120 animation = {
121 type = "vertical_frames",
122 aspect_w = 16,
123 aspect_h = 16,
124 length = 0.5,
128 drawtype = "nodebox",
129 paramtype = "light",
130 paramtype2 = "facedir",
131 sunlight_propagates = true,
132 use_texture_alpha = true,
133 walkable = false,
134 diggable = false,
135 pointable = false,
136 buildable_to = false,
137 is_ground_content = false,
138 drop = "",
139 light_source = 11,
140 post_effect_color = {a = 180, r = 51, g = 7, b = 89},
141 alpha = 192,
142 node_box = {
143 type = "fixed",
144 fixed = {
145 {-0.5, -0.5, -0.1, 0.5, 0.5, 0.1},
148 groups = {portal=1, not_in_creative_inventory = 1},
149 on_destruct = destroy_nether_portal,
151 _mcl_hardness = -1,
152 _mcl_blast_resistance = 0,
155 local function find_target_y(x, y, z, y_min, y_max)
156 local y_org = y
157 local node = minetest.get_node_or_nil({x = x, y = y, z = z})
158 if node == nil then
159 return y
161 while node.name ~= "air" and y < y_max do
162 y = y + 1
163 node = minetest.get_node_or_nil({x = x, y = y, z = z})
164 if node == nil then
165 break
168 if node then
169 if node.name ~= "air" then
170 y = y_org
173 while node == nil and y > y_min do
174 y = y - 1
175 node = minetest.get_node_or_nil({x = x, y = y, z = z})
177 if y == y_max and node ~= nil then -- try reverse direction who knows what they built there...
178 while node.name ~= "air" and y > y_min do
179 y = y - 1
180 node = minetest.get_node_or_nil({x = x, y = y, z = z})
181 if node == nil then
182 break
186 if node == nil then
187 return y_org
189 while node.name == "air" and y > y_min do
190 y = y - 1
191 node = minetest.get_node_or_nil({x = x, y = y, z = z})
192 while node == nil and y > y_min do
193 y = y - 1
194 node = minetest.get_node_or_nil({x = x, y = y, z = z})
196 if node == nil then
197 return y_org
200 if y == y_min then
201 return y_org
203 return y
206 local function find_nether_target_y(x, y, z)
207 local target_y = find_target_y(x, y, z, nether_ymin + 4, nether_ymax - 25) + 1
208 minetest.log("verbose", "[mcl_portal] Found Nether target altitude: " .. tostring(target_y) .. " for pos. " .. minetest.pos_to_string({x = x, y = y, z = z}))
209 return target_y
212 local function find_overworld_target_y(x, y, z)
213 local target_y = find_target_y(x, y, z, overworld_ymin + 4, overworld_ymax - 25) + 1
214 local node = minetest.get_node({x = x, y = target_y - 1, z = z})
215 if not node then
216 return target_y
218 nn = node.name
219 if nn ~= "air" and minetest.get_item_group(nn, "water") == 0 then
220 target_y = target_y + 1
222 minetest.log("verbose", "[mcl_portal] Found Overworld target altitude: " .. tostring(target_y) .. " for pos. " .. minetest.pos_to_string({x = x, y = y, z = z}))
223 return target_y
227 local function update_target(pos, target, time_str)
228 local stack = {{x = pos.x, y = pos.y, z = pos.z}}
229 while #stack > 0 do
230 local i = #stack
231 local meta = minetest.get_meta(stack[i])
232 if meta:get_string("portal_time") == time_str then
233 stack[i] = nil -- Already updated, skip it
234 else
235 local node = minetest.get_node(stack[i])
236 local portal = node.name == "mcl_portals:portal"
237 if not portal then
238 stack[i] = nil
239 else
240 local x, y, z = stack[i].x, stack[i].y, stack[i].z
241 meta:set_string("portal_time", time_str)
242 meta:set_string("portal_target", target)
243 stack[i].y = y - 1
244 stack[i + 1] = {x = x, y = y + 1, z = z}
245 if node.param2 == 0 then
246 stack[i + 2] = {x = x - 1, y = y, z = z}
247 stack[i + 3] = {x = x + 1, y = y, z = z}
248 else
249 stack[i + 2] = {x = x, y = y, z = z - 1}
250 stack[i + 3] = {x = x, y = y, z = z + 1}
257 local function ecb_setup_target_portal(blockpos, action, calls_remaining, param)
258 -- param.: srcx, srcy, srcz, dstx, dsty, dstz, srcdim, ax1, ay1, az1, ax2, ay2, az2
260 local portal_search = function(target, p1, p2)
261 local portal_nodes = minetest.find_nodes_in_area(p1, p2, "mcl_portals:portal")
262 local portal_pos = false
263 if portal_nodes and #portal_nodes > 0 then
264 -- Found some portal(s), use nearest:
265 portal_pos = {x = portal_nodes[1].x, y = portal_nodes[1].y, z = portal_nodes[1].z}
266 local nearest_distance = vector.distance(target, portal_pos)
267 for n = 2, #portal_nodes do
268 local distance = vector.distance(target, portal_nodes[n])
269 if distance < nearest_distance then
270 portal_pos = {x = portal_nodes[n].x, y = portal_nodes[n].y, z = portal_nodes[n].z}
271 nearest_distance = distance
274 end -- here we have the best portal_pos
275 return portal_pos
278 if calls_remaining <= 0 then
279 minetest.log("action", "[mcl_portal] Area for destination Nether portal emerged!")
280 local src_pos = {x = param.srcx, y = param.srcy, z = param.srcz}
281 local dst_pos = {x = param.dstx, y = param.dsty, z = param.dstz}
282 local meta = minetest.get_meta(src_pos)
283 local portal_pos = portal_search(dst_pos, {x = param.ax1, y = param.ay1, z = param.az1}, {x = param.ax2, y = param.ay2, z = param.az2})
285 if portal_pos == false then
286 minetest.log("verbose", "[mcl_portal] No portal in area " .. minetest.pos_to_string({x = param.ax1, y = param.ay1, z = param.az1}) .. "-" .. minetest.pos_to_string({x = param.ax2, y = param.ay2, z = param.az2}))
287 -- Need to build arrival portal:
288 local org_dst_y = dst_pos.y
289 if param.srcdim == "overworld" then
290 dst_pos.y = find_nether_target_y(dst_pos.x, dst_pos.y, dst_pos.z)
291 else
292 dst_pos.y = find_overworld_target_y(dst_pos.x, dst_pos.y, dst_pos.z)
294 if math.abs(org_dst_y - dst_pos.y) >= PORTAL_SEARCH_ALTITUDE / 2 then
295 portal_pos = portal_search(dst_pos,
296 {x = dst_pos.x - PORTAL_SEARCH_HALF_CHUNK, y = math.floor(dst_pos.y - PORTAL_SEARCH_ALTITUDE / 2), z = dst_pos.z - PORTAL_SEARCH_HALF_CHUNK},
297 {x = dst_pos.x + PORTAL_SEARCH_HALF_CHUNK, y = math.ceil(dst_pos.y + PORTAL_SEARCH_ALTITUDE / 2), z = dst_pos.z + PORTAL_SEARCH_HALF_CHUNK}
300 if portal_pos == false then
301 minetest.log("verbose", "[mcl_portal] 2nd attempt: No portal in area " .. minetest.pos_to_string({x = dst_pos.x - PORTAL_SEARCH_HALF_CHUNK, y = math.floor(dst_pos.y - PORTAL_SEARCH_ALTITUDE / 2), z = dst_pos.z - PORTAL_SEARCH_HALF_CHUNK}) .. "-" .. minetest.pos_to_string({x = dst_pos.x + PORTAL_SEARCH_HALF_CHUNK, y = math.ceil(dst_pos.y + PORTAL_SEARCH_ALTITUDE / 2), z = dst_pos.z + PORTAL_SEARCH_HALF_CHUNK}))
302 local width, height = 2, 3
303 portal_pos = mcl_portals.build_nether_portal(dst_pos, width, height)
307 local target_meta = minetest.get_meta(portal_pos)
308 local p3 = minetest.string_to_pos(target_meta:get_string("portal_frame1"))
309 local p4 = minetest.string_to_pos(target_meta:get_string("portal_frame2"))
310 if p3 and p4 then
311 portal_pos = vector.divide(vector.add(p3, p4), 2.0)
312 portal_pos.y = math.min(p3.y, p4.y)
313 portal_pos = vector.round(portal_pos)
314 local node = minetest.get_node(portal_pos)
315 if node and node.name ~= "mcl_portals:portal" then
316 portal_pos = {x = p3.x, y = p3.y, z = p3.z}
317 if minetest.get_node(portal_pos).name == "mcl_core:obsidian" then
318 -- Old-version portal:
319 if p4.z == p3.z then
320 portal_pos = {x = p3.x + 1, y = p3.y + 1, z = p3.z}
321 else
322 portal_pos = {x = p3.x, y = p3.y + 1, z = p3.z + 1}
327 local time_str = tostring(minetest.get_us_time())
328 local target = minetest.pos_to_string(portal_pos)
330 update_target(src_pos, target, time_str)
334 local function nether_portal_get_target_position(src_pos)
335 local _, current_dimension = mcl_worlds.y_to_layer(src_pos.y)
336 local x, y, z, y_min, y_max = 0, 0, 0, 0, 0
337 if current_dimension == "nether" then
338 x = math.floor(nether_to_overworld(src_pos.x) + 0.5)
339 z = math.floor(nether_to_overworld(src_pos.z) + 0.5)
340 y = math.floor((math.min(math.max(src_pos.y, nether_ymin), nether_ymax) - nether_ymin) / nether_dy * overworld_dy + overworld_ymin + 0.5)
341 y_min = overworld_ymin
342 y_max = overworld_ymax
343 else -- overworld:
344 x = math.floor(src_pos.x / 8 + 0.5)
345 z = math.floor(src_pos.z / 8 + 0.5)
346 y = math.floor((math.min(math.max(src_pos.y, overworld_ymin), overworld_ymax) - overworld_ymin) / overworld_dy * nether_dy + nether_ymin + 0.5)
347 y_min = nether_ymin
348 y_max = nether_ymax
350 return x, y, z, current_dimension, y_min, y_max
353 local function find_or_create_portal(src_pos)
354 local x, y, z, cdim, y_min, y_max = nether_portal_get_target_position(src_pos)
355 local pos1 = {x = x - PORTAL_SEARCH_HALF_CHUNK, y = math.max(y_min, math.floor(y - PORTAL_SEARCH_ALTITUDE / 2)), z = z - PORTAL_SEARCH_HALF_CHUNK}
356 local pos2 = {x = x + PORTAL_SEARCH_HALF_CHUNK, y = math.min(y_max, math.ceil(y + PORTAL_SEARCH_ALTITUDE / 2)), z = z + PORTAL_SEARCH_HALF_CHUNK}
357 if pos1.y == y_min then
358 pos2.y = math.min(y_max, pos1.y + PORTAL_SEARCH_ALTITUDE)
359 else
360 if pos2.y == y_max then
361 pos1.y = math.max(y_min, pos2.y - PORTAL_SEARCH_ALTITUDE)
364 minetest.emerge_area(pos1, pos2, ecb_setup_target_portal, {srcx=src_pos.x, srcy=src_pos.y, srcz=src_pos.z, dstx=x, dsty=y, dstz=z, srcdim=cdim, ax1=pos1.x, ay1=pos1.y, az1=pos1.z, ax2=pos2.x, ay2=pos2.y, az2=pos2.z})
367 local function emerge_target_area(src_pos)
368 local x, y, z, cdim, y_min, y_max = nether_portal_get_target_position(src_pos)
369 local pos1 = {x = x - PORTAL_SEARCH_HALF_CHUNK, y = math.max(y_min + 2, math.floor(y - PORTAL_SEARCH_ALTITUDE / 2)), z = z - PORTAL_SEARCH_HALF_CHUNK}
370 local pos2 = {x = x + PORTAL_SEARCH_HALF_CHUNK, y = math.min(y_max - 2, math.ceil(y + PORTAL_SEARCH_ALTITUDE / 2)), z = z + PORTAL_SEARCH_HALF_CHUNK}
371 minetest.emerge_area(pos1, pos2)
372 pos1 = {x = x - 1, y = y_min, z = z - 1}
373 pos2 = {x = x + 1, y = y_max, z = z + 1}
374 minetest.emerge_area(pos1, pos2)
377 local function available_for_nether_portal(p)
378 local nn = minetest.get_node(p).name
379 local obsidian = nn == "mcl_core:obsidian"
380 if nn ~= "air" and minetest.get_item_group(nn, "fire") ~= 1 then
381 return false, obsidian
383 return true, obsidian
386 local function light_frame(x1, y1, z1, x2, y2, z2, build_frame)
387 local build_frame = build_frame or false
388 local orientation = 0
389 if x1 == x2 then
390 orientation = 1
392 local disperse = 50
393 local pass = 1
394 while true do
395 local protection = false
397 for x = x1 - 1 + orientation, x2 + 1 - orientation do
398 for z = z1 - orientation, z2 + orientation do
399 for y = y1 - 1, y2 + 1 do
400 local frame = (x < x1) or (x > x2) or (y < y1) or (y > y2) or (z < z1) or (z > z2)
401 if frame then
402 if build_frame then
403 if pass == 1 then
404 if minetest.is_protected({x = x, y = y, z = z}, "") then
405 protection = true
406 local offset_x = math.random(-disperse, disperse)
407 local offset_z = math.random(-disperse, disperse)
408 disperse = disperse + math.random(25, 177)
409 if disperse > 5000 then
410 return nil
412 x1, z1 = x1 + offset_x, z1 + offset_z
413 x2, z2 = x2 + offset_x, z2 + offset_z
414 local _, dimension = mcl_worlds.y_to_layer(y1)
415 local height = math.abs(y2 - y1)
416 y1 = (y1 + y2) / 2
417 if dimension == "nether" then
418 y1 = find_nether_target_y(math.min(x1, x2), y1, math.min(z1, z2))
419 else
420 y1 = find_overworld_target_y(math.min(x1, x2), y1, math.min(z1, z2))
422 y2 = y1 + height
423 break
425 else
426 minetest.set_node({x = x, y = y, z = z}, {name = "mcl_core:obsidian"})
429 else
430 if not build_frame or pass == 2 then
431 local node = minetest.get_node({x = x, y = y, z = z})
432 minetest.set_node({x = x, y = y, z = z}, {name = "mcl_portals:portal", param2 = orientation})
435 if not frame and pass == 2 then
436 local meta = minetest.get_meta({x = x, y = y, z = z})
437 -- Portal frame corners
438 meta:set_string("portal_frame1", minetest.pos_to_string({x = x1, y = y1, z = z1}))
439 meta:set_string("portal_frame2", minetest.pos_to_string({x = x2, y = y2, z = z2}))
440 -- Portal target coordinates
441 meta:set_string("portal_target", "")
442 -- Portal last teleportation time
443 meta:set_string("portal_time", tostring(0))
446 if protection then
447 break
450 if protection then
451 break
454 if build_frame == false or pass == 2 then
455 break
457 if build_frame and not protection and pass == 1 then
458 pass = 2
461 emerge_target_area({x = x1, y = y1, z = z1})
462 return {x = x1, y = y1, z = z1}
465 --Build arrival portal
466 function mcl_portals.build_nether_portal(pos, width, height, orientation)
467 local height = height or FRAME_SIZE_Y_MIN - 2
468 local width = width or FRAME_SIZE_X_MIN - 2
469 local orientation = orientation or math.random(0, 1)
471 if orientation == 0 then
472 minetest.load_area({x = pos.x - 3, y = pos.y - 1, z = pos.z - width * 2}, {x = pos.x + width + 2, y = pos.y + height + 2, z = pos.z + width * 2})
473 else
474 minetest.load_area({x = pos.x - width * 2, y = pos.y - 1, z = pos.z - 3}, {x = pos.x + width * 2, y = pos.y + height + 2, z = pos.z + width + 2})
477 pos = light_frame(pos.x, pos.y, pos.z, pos.x + (1 - orientation) * (width - 1), pos.y + height - 1, pos.z + orientation * (width - 1), true)
479 -- Clear some space around:
480 for x = pos.x - math.random(2 + (width-2)*( orientation), 5 + (2*width-5)*( orientation)), pos.x + width*(1-orientation) + math.random(2+(width-2)*( orientation), 4 + (2*width-4)*( orientation)) do
481 for z = pos.z - math.random(2 + (width-2)*(1-orientation), 5 + (2*width-5)*(1-orientation)), pos.z + width*( orientation) + math.random(2+(width-2)*(1-orientation), 4 + (2*width-4)*(1-orientation)) do
482 for y = pos.y - 1, pos.y + height + math.random(1,6) do
483 local nn = minetest.get_node({x = x, y = y, z = z}).name
484 if nn ~= "mcl_core:obsidian" and nn ~= "mcl_portals:portal" and minetest.registered_nodes[nn].is_ground_content and not minetest.is_protected({x = x, y = y, z = z}, "") then
485 minetest.remove_node({x = x, y = y, z = z})
491 -- Build obsidian platform:
492 for x = pos.x - orientation, pos.x + orientation + (width - 1) * (1 - orientation), 1 + orientation do
493 for z = pos.z - 1 + orientation, pos.z + 1 - orientation + (width - 1) * orientation, 2 - orientation do
494 local pp = {x = x, y = pos.y - 1, z = z}
495 local nn = minetest.get_node(pp).name
496 if not minetest.registered_nodes[nn].is_ground_content and not minetest.is_protected(pp, "") then
497 minetest.set_node(pp, {name = "mcl_core:obsidian"})
502 minetest.log("action", "[mcl_portal] Destination Nether portal generated at "..minetest.pos_to_string(pos).."!")
504 return pos
507 local function check_and_light_shape(pos, orientation)
508 local stack = {{x = pos.x, y = pos.y, z = pos.z}}
509 local node_list = {}
510 local node_counter = 0
511 -- Search most low node from the left (pos1) and most right node from the top (pos2)
512 local pos1 = {x = pos.x, y = pos.y, z = pos.z}
513 local pos2 = {x = pos.x, y = pos.y, z = pos.z}
515 local wrong_portal_nodes_clean_up = function(node_list)
516 for i = 1, #node_list do
517 local meta = minetest.get_meta(node_list[i])
518 meta:set_string("portal_time", "")
520 return false
523 while #stack > 0 do
524 local i = #stack
525 local meta = minetest.get_meta(stack[i])
526 local target = meta:get_string("portal_time")
527 if target and target == "-2" then
528 stack[i] = nil -- Already checked, skip it
529 else
530 local good, obsidian = available_for_nether_portal(stack[i])
531 if obsidian then
532 stack[i] = nil
533 else
534 if (not good) or (node_counter >= PORTAL_NODES_MAX) then
535 return wrong_portal_nodes_clean_up(node_list)
537 local x, y, z = stack[i].x, stack[i].y, stack[i].z
538 meta:set_string("portal_time", "-2")
539 node_counter = node_counter + 1
540 node_list[node_counter] = {x = x, y = y, z = z}
541 stack[i].y = y - 1
542 stack[i + 1] = {x = x, y = y + 1, z = z}
543 if orientation == 0 then
544 stack[i + 2] = {x = x - 1, y = y, z = z}
545 stack[i + 3] = {x = x + 1, y = y, z = z}
546 else
547 stack[i + 2] = {x = x, y = y, z = z - 1}
548 stack[i + 3] = {x = x, y = y, z = z + 1}
550 if (y < pos1.y) or (y == pos1.y and (x < pos1.x or z < pos1.z)) then
551 pos1 = {x = x, y = y, z = z}
553 if (x > pos2.x or z > pos2.z) or (x == pos2.x and z == pos2.z and y > pos2.y) then
554 pos2 = {x = x, y = y, z = z}
560 if node_counter < PORTAL_NODES_MIN then
561 return wrong_portal_nodes_clean_up(node_list)
564 -- Limit rectangles width and height
565 if math.abs(pos2.x - pos1.x + pos2.z - pos1.z) + 3 > FRAME_SIZE_X_MAX or math.abs(pos2.y - pos1.y) + 3 > FRAME_SIZE_Y_MAX then
566 return wrong_portal_nodes_clean_up(node_list)
569 for i = 1, node_counter do
570 local node_pos = node_list[i]
571 local node = minetest.get_node(node_pos)
572 minetest.set_node(node_pos, {name = "mcl_portals:portal", param2 = orientation})
573 local meta = minetest.get_meta(node_pos)
574 meta:set_string("portal_frame1", minetest.pos_to_string(pos1))
575 meta:set_string("portal_frame2", minetest.pos_to_string(pos2))
576 meta:set_string("portal_time", tostring(0))
577 meta:set_string("portal_target", "")
579 return true
582 -- Attempts to light a Nether portal at pos
583 -- Pos can be any of the inner part.
584 -- The frame MUST be filled only with air or any fire, which will be replaced with Nether portal blocks.
585 -- If no Nether portal can be lit, nothing happens.
586 -- Returns number of portals created (0, 1 or 2)
587 function mcl_portals.light_nether_portal(pos)
588 -- Only allow to make portals in Overworld and Nether
589 local dim = mcl_worlds.pos_to_dimension(pos)
590 if dim ~= "overworld" and dim ~= "nether" then
591 return 0
593 local orientation = math.random(0, 1)
594 for orientation_iteration = 1, 2 do
595 if check_and_light_shape(pos, orientation) then
596 return true
598 orientation = 1 - orientation
600 return false
603 local function update_portal_time(pos, time_str)
604 local stack = {{x = pos.x, y = pos.y, z = pos.z}}
605 while #stack > 0 do
606 local i = #stack
607 local meta = minetest.get_meta(stack[i])
608 if meta:get_string("portal_time") == time_str then
609 stack[i] = nil -- Already updated, skip it
610 else
611 local node = minetest.get_node(stack[i])
612 local portal = node.name == "mcl_portals:portal"
613 if not portal then
614 stack[i] = nil
615 else
616 local x, y, z = stack[i].x, stack[i].y, stack[i].z
617 meta:set_string("portal_time", time_str)
618 stack[i].y = y - 1
619 stack[i + 1] = {x = x, y = y + 1, z = z}
620 if node.param2 == 0 then
621 stack[i + 2] = {x = x - 1, y = y, z = z}
622 stack[i + 3] = {x = x + 1, y = y, z = z}
623 else
624 stack[i + 2] = {x = x, y = y, z = z - 1}
625 stack[i + 3] = {x = x, y = y, z = z + 1}
632 local function prepare_target(pos)
633 local meta, us_time = minetest.get_meta(pos), minetest.get_us_time()
634 local portal_time = tonumber(meta:get_string("portal_time")) or 0
635 local delta_time_us = us_time - portal_time
636 local pos1, pos2 = minetest.string_to_pos(meta:get_string("portal_frame1")), minetest.string_to_pos(meta:get_string("portal_frame2"))
637 if delta_time_us <= DESTINATION_EXPIRES then
638 -- Destination point must be still cached according to https://minecraft.gamepedia.com/Nether_portal
639 return update_portal_time(pos, tostring(us_time))
641 -- No cached destination point
642 find_or_create_portal(pos)
645 -- Teleportation cooloff for some seconds, to prevent back-and-forth teleportation
646 local function stop_teleport_cooloff(o)
647 mcl_portals.nether_portal_cooloff[o] = false
648 touch_chatter_prevention[o] = nil
651 local function teleport_cooloff(obj)
652 if obj:is_player() then
653 minetest.after(TELEPORT_COOLOFF, stop_teleport_cooloff, obj)
654 else
655 minetest.after(MOB_TELEPORT_COOLOFF, stop_teleport_cooloff, obj)
659 -- Teleport function
660 local function teleport_no_delay(obj, pos)
661 local is_player = obj:is_player()
662 if (not obj:get_luaentity()) and (not is_player) then
663 return
666 local objpos = obj:get_pos()
667 if objpos == nil then
668 return
671 if mcl_portals.nether_portal_cooloff[obj] then
672 return
674 -- If player stands, player is at ca. something+0.5
675 -- which might cause precision problems, so we used ceil.
676 objpos.y = math.ceil(objpos.y)
678 if minetest.get_node(objpos).name ~= "mcl_portals:portal" then
679 return
682 local meta = minetest.get_meta(pos)
683 local delta_time = minetest.get_us_time() - (tonumber(meta:get_string("portal_time")) or 0)
684 local target = minetest.string_to_pos(meta:get_string("portal_target"))
685 if delta_time > DESTINATION_EXPIRES or target == nil then
686 -- Area not ready yet - retry after a second
687 if obj:is_player() then
688 minetest.chat_send_player(obj:get_player_name(), S("Loading terrain..."))
690 return minetest.after(1, teleport_no_delay, obj, pos)
693 -- Enable teleportation cooloff for some seconds, to prevent back-and-forth teleportation
694 teleport_cooloff(obj)
695 mcl_portals.nether_portal_cooloff[obj] = true
697 -- Teleport
698 obj:set_pos(target)
700 if is_player then
701 mcl_worlds.dimension_change(obj, mcl_worlds.pos_to_dimension(target))
702 minetest.sound_play("mcl_portals_teleport", {pos=target, gain=0.5, max_hear_distance = 16}, true)
703 local name = obj:get_player_name()
704 minetest.log("action", "[mcl_portal] "..name.." teleported to Nether portal at "..minetest.pos_to_string(target)..".")
708 local function prevent_portal_chatter(obj)
709 local time_us = minetest.get_us_time()
710 local chatter = touch_chatter_prevention[obj] or 0
711 touch_chatter_prevention[obj] = time_us
712 minetest.after(TOUCH_CHATTER_TIME, function(o)
713 if not o or not touch_chatter_prevention[o] then
714 return
716 if minetest.get_us_time() - touch_chatter_prevention[o] >= TOUCH_CHATTER_TIME_US then
717 touch_chatter_prevention[o] = nil
719 end, obj)
720 return time_us - chatter > TOUCH_CHATTER_TIME_US
723 local function animation(player, playername)
724 local chatter = touch_chatter_prevention[player] or 0
725 if mcl_portals.nether_portal_cooloff[player] or minetest.get_us_time() - chatter < TOUCH_CHATTER_TIME_US then
726 local pos = player:get_pos()
727 minetest.add_particlespawner({
728 amount = 1,
729 minpos = {x = pos.x - 0.1, y = pos.y + 1.4, z = pos.z - 0.1},
730 maxpos = {x = pos.x + 0.1, y = pos.y + 1.6, z = pos.z + 0.1},
731 minvel = 0,
732 maxvel = 0,
733 minacc = 0,
734 maxacc = 0,
735 minexptime = 0.1,
736 maxexptime = 0.2,
737 minsize = 5,
738 maxsize = 15,
739 collisiondetection = false,
740 texture = "mcl_particles_nether_portal_t.png",
741 playername = playername,
743 minetest.after(0.3, animation, player, playername)
747 local function teleport(obj, portal_pos)
748 local name = ""
749 if obj:is_player() then
750 name = obj:get_player_name()
751 animation(obj, name)
753 -- Call prepare_target() first because it might take a long
754 prepare_target(portal_pos)
755 -- Prevent quick back-and-forth teleportation
756 if not mcl_portals.nether_portal_cooloff[obj] then
757 local creative_enabled = minetest.is_creative_enabled(name)
758 if creative_enabled then
759 return teleport_no_delay(obj, portal_pos)
761 minetest.after(TELEPORT_DELAY, teleport_no_delay, obj, portal_pos)
765 minetest.register_abm({
766 label = "Nether portal teleportation and particles",
767 nodenames = {"mcl_portals:portal"},
768 interval = 1,
769 chance = 1,
770 action = function(pos, node)
771 local o = node.param2 -- orientation
772 local d = math.random(0, 1) -- direction
773 local time = math.random() * 1.9 + 0.5
774 local velocity, acceleration
775 if o == 1 then
776 velocity = {x = math.random() * 0.7 + 0.3, y = math.random() - 0.5, z = math.random() - 0.5}
777 acceleration = {x = math.random() * 1.1 + 0.3, y = math.random() - 0.5, z = math.random() - 0.5}
778 else
779 velocity = {x = math.random() - 0.5, y = math.random() - 0.5, z = math.random() * 0.7 + 0.3}
780 acceleration = {x = math.random() - 0.5, y = math.random() - 0.5, z = math.random() * 1.1 + 0.3}
782 local distance = vector.add(vector.multiply(velocity, time), vector.multiply(acceleration, time * time / 2))
783 if d == 1 then
784 if o == 1 then
785 distance.x = -distance.x
786 velocity.x = -velocity.x
787 acceleration.x = -acceleration.x
788 else
789 distance.z = -distance.z
790 velocity.z = -velocity.z
791 acceleration.z = -acceleration.z
794 distance = vector.subtract(pos, distance)
795 for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 15)) do
796 if obj:is_player() then
797 minetest.add_particlespawner({
798 amount = node_particles_allowed_level + 1,
799 minpos = distance,
800 maxpos = distance,
801 minvel = velocity,
802 maxvel = velocity,
803 minacc = acceleration,
804 maxacc = acceleration,
805 minexptime = time,
806 maxexptime = time,
807 minsize = 0.3,
808 maxsize = 1.8,
809 collisiondetection = false,
810 texture = "mcl_particles_nether_portal.png",
811 playername = obj:get_player_name(),
815 for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 1)) do --maikerumine added for objects to travel
816 local lua_entity = obj:get_luaentity() --maikerumine added for objects to travel
817 if (obj:is_player() or lua_entity) and prevent_portal_chatter(obj) then
818 teleport(obj, pos)
821 end,
825 --[[ ITEM OVERRIDES ]]
827 local longdesc = minetest.registered_nodes["mcl_core:obsidian"]._doc_items_longdesc
828 longdesc = longdesc .. "\n" .. S("Obsidian is also used as the frame of Nether portals.")
829 local usagehelp = S("To open a Nether portal, place an upright frame of obsidian with a width of 4 blocks and a height of 5 blocks, leaving only air in the center. After placing this frame, light a fire in the obsidian frame. Nether portals only work in the Overworld and the Nether.")
831 minetest.override_item("mcl_core:obsidian", {
832 _doc_items_longdesc = longdesc,
833 _doc_items_usagehelp = usagehelp,
834 on_destruct = destroy_nether_portal,
835 _on_ignite = function(user, pointed_thing)
836 local x, y, z = pointed_thing.under.x, pointed_thing.under.y, pointed_thing.under.z
837 -- Check empty spaces around obsidian and light all frames found:
838 local portals_placed =
839 mcl_portals.light_nether_portal({x = x - 1, y = y, z = z}) or mcl_portals.light_nether_portal({x = x + 1, y = y, z = z}) or
840 mcl_portals.light_nether_portal({x = x, y = y - 1, z = z}) or mcl_portals.light_nether_portal({x = x, y = y + 1, z = z}) or
841 mcl_portals.light_nether_portal({x = x, y = y, z = z - 1}) or mcl_portals.light_nether_portal({x = x, y = y, z = z + 1})
842 if portals_placed then
843 minetest.log("action", "[mcl_portal] Nether portal activated at "..minetest.pos_to_string({x=x,y=y,z=z})..".")
844 if minetest.get_modpath("doc") then
845 doc.mark_entry_as_revealed(user:get_player_name(), "nodes", "mcl_portals:portal")
847 -- Achievement for finishing a Nether portal TO the Nether
848 local dim = mcl_worlds.pos_to_dimension({x=x, y=y, z=z})
849 if minetest.get_modpath("awards") and dim ~= "nether" and user:is_player() then
850 awards.unlock(user:get_player_name(), "mcl:buildNetherPortal")
853 return true
854 else
855 return false
857 end,