1 local S
= minetest
.get_translator("mcl_portals")
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
= {
44 local node_particles_allowed_level
= node_particles_levels
[node_particles_allowed
]
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)
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"))
64 meta
:set_string("portal_frame1", "")
65 meta
:set_string("portal_frame2", "")
66 meta
:set_string("portal_target", "")
67 meta
:set_string("portal_time", "")
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
)
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
})
85 if not has_meta
then -- no meta means repeated call: function calls on every node destruction
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)
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)
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
})
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."),
110 name
= "mcl_portals_portal.png",
112 type = "vertical_frames",
119 name
= "mcl_portals_portal.png",
121 type = "vertical_frames",
128 drawtype
= "nodebox",
130 paramtype2
= "facedir",
131 sunlight_propagates
= true,
132 use_texture_alpha
= true,
136 buildable_to
= false,
137 is_ground_content
= false,
140 post_effect_color
= {a
= 180, r
= 51, g
= 7, b
= 89},
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
,
152 _mcl_blast_resistance
= 0,
155 local function find_target_y(x
, y
, z
, y_min
, y_max
)
157 local node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
161 while node
.name
~= "air" and y
< y_max
do
163 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
169 if node
.name
~= "air" then
173 while node
== nil and y
> y_min
do
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
180 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
189 while node
.name
== "air" and y
> y_min
do
191 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
192 while node
== nil and y
> y_min
do
194 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
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
}))
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
})
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
}))
227 local function update_target(pos
, target
, time_str
)
228 local stack
= {{x
= pos
.x
, y
= pos
.y
, z
= pos
.z
}}
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
235 local node
= minetest
.get_node(stack
[i
])
236 local portal
= node
.name
== "mcl_portals:portal"
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
)
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
}
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
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
)
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"))
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:
320 portal_pos
= {x
= p3
.x
+ 1, y
= p3
.y
+ 1, z
= p3
.z
}
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
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)
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
)
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
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
)
404 if minetest
.is_protected({x
= x
, y
= y
, z
= z
}, "") then
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
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
)
417 if dimension
== "nether" then
418 y1
= find_nether_target_y(math
.min(x1
, x2
), y1
, math
.min(z1
, z2
))
420 y1
= find_overworld_target_y(math
.min(x1
, x2
), y1
, math
.min(z1
, z2
))
426 minetest
.set_node({x
= x
, y
= y
, z
= z
}, {name
= "mcl_core:obsidian"})
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))
454 if build_frame
== false or pass
== 2 then
457 if build_frame
and not protection
and pass
== 1 then
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})
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
).."!")
507 local function check_and_light_shape(pos
, orientation
)
508 local stack
= {{x
= pos
.x
, y
= pos
.y
, z
= pos
.z
}}
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", "")
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
530 local good
, obsidian
= available_for_nether_portal(stack
[i
])
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
}
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
}
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", "")
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
593 local orientation
= math
.random(0, 1)
594 for orientation_iteration
= 1, 2 do
595 if check_and_light_shape(pos
, orientation
) then
598 orientation
= 1 - orientation
603 local function update_portal_time(pos
, time_str
)
604 local stack
= {{x
= pos
.x
, y
= pos
.y
, z
= pos
.z
}}
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
611 local node
= minetest
.get_node(stack
[i
])
612 local portal
= node
.name
== "mcl_portals:portal"
616 local x
, y
, z
= stack
[i
].x
, stack
[i
].y
, stack
[i
].z
617 meta
:set_string("portal_time", time_str
)
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
}
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
)
655 minetest
.after(MOB_TELEPORT_COOLOFF
, stop_teleport_cooloff
, obj
)
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
666 local objpos
= obj
:get_pos()
667 if objpos
== nil then
671 if mcl_portals
.nether_portal_cooloff
[obj
] then
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
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
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
716 if minetest
.get_us_time() - touch_chatter_prevention
[o
] >= TOUCH_CHATTER_TIME_US
then
717 touch_chatter_prevention
[o
] = nil
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({
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},
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
)
749 if obj
:is_player() then
750 name
= obj
:get_player_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"},
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
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}
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))
785 distance
.x
= -distance
.x
786 velocity
.x
= -velocity
.x
787 acceleration
.x
= -acceleration
.x
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,
803 minacc
= acceleration
,
804 maxacc
= acceleration
,
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
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")