1 local S
= minetest
.get_translator("mcl_portals")
5 local OVERWORLD_TO_NETHER_SCALE
= 8
6 local LIMIT
= math
.min(math
.abs(mcl_vars
.mapgen_edge_min
), math
.abs(mcl_vars
.mapgen_edge_max
))
9 local FRAME_SIZE_X_MIN
= 4
10 local FRAME_SIZE_Y_MIN
= 5
11 local FRAME_SIZE_X_MAX
= 23
12 local FRAME_SIZE_Y_MAX
= 23
14 local PORTAL_NODES_MIN
= 5
15 local PORTAL_NODES_MAX
= (FRAME_SIZE_X_MAX
- 2) * (FRAME_SIZE_Y_MAX
- 2)
17 local TELEPORT_COOLOFF
= 3 -- after player was teleported, for this many seconds they won't teleported again
18 local MOB_TELEPORT_COOLOFF
= 14 -- after mob was teleported, for this many seconds they won't teleported again
19 local TOUCH_CHATTER_TIME
= 1 -- prevent multiple teleportation attempts caused by multiple portal touches, for this number of seconds
20 local TOUCH_CHATTER_TIME_US
= TOUCH_CHATTER_TIME
* 1000000
21 local TELEPORT_DELAY
= 3 -- seconds before teleporting in Nether portal (4 minus ABM interval time)
22 local DESTINATION_EXPIRES
= 60 * 1000000 -- cached destination expires after this number of microseconds have passed without using the same origin portal
24 local PORTAL_SEARCH_HALF_CHUNK
= 40 -- greater values may slow down the teleportation
25 local PORTAL_SEARCH_ALTITUDE
= 128
27 -- Table of objects (including players) which recently teleported by a
28 -- Nether portal. Those objects have a brief cooloff period before they
29 -- can teleport again. This prevents annoying back-and-forth teleportation.
30 mcl_portals
.nether_portal_cooloff
= {}
31 local touch_chatter_prevention
= {}
33 local overworld_ymin
= math
.max(mcl_vars
.mg_overworld_min
, -31)
34 local overworld_ymax
= math
.min(mcl_vars
.mg_overworld_max_official
, 63)
35 local nether_ymin
= mcl_vars
.mg_bedrock_nether_bottom_min
36 local nether_ymax
= mcl_vars
.mg_bedrock_nether_top_max
37 local overworld_dy
= overworld_ymax
- overworld_ymin
+ 1
38 local nether_dy
= nether_ymax
- nether_ymin
+ 1
40 local node_particles_allowed
= minetest
.settings
:get("mcl_node_particles") or "none"
41 local node_particles_levels
= {
47 local node_particles_allowed_level
= node_particles_levels
[node_particles_allowed
]
52 -- Ping-Pong fast travel, https://git.minetest.land/Wuzzy/MineClone2/issues/795#issuecomment-11058
53 local function nether_to_overworld(x
)
54 return LIMIT
- math
.abs(((x
* OVERWORLD_TO_NETHER_SCALE
+ LIMIT
) % (LIMIT
*4)) - (LIMIT
*2))
57 -- Destroy portal if pos (portal frame or portal node) got destroyed
58 local function destroy_nether_portal(pos
)
59 local meta
= minetest
.get_meta(pos
)
60 local node
= minetest
.get_node(pos
)
61 local nn
, orientation
= node
.name
, node
.param2
62 local obsidian
= nn
== "mcl_core:obsidian"
64 local has_meta
= minetest
.string_to_pos(meta
:get_string("portal_frame1"))
66 meta
:set_string("portal_frame1", "")
67 meta
:set_string("portal_frame2", "")
68 meta
:set_string("portal_target", "")
69 meta
:set_string("portal_time", "")
71 local check_remove
= function(pos
, orientation
)
72 local node
= minetest
.get_node(pos
)
73 if node
and (node
.name
== "mcl_portals:portal" and (orientation
== nil or (node
.param2
== orientation
))) then
74 minetest
.log("action", "[mcl_portal] Destroying Nether portal at " .. minetest
.pos_to_string(pos
))
75 return minetest
.remove_node(pos
)
78 if obsidian
then -- check each of 6 sides of it and destroy every portal:
79 check_remove({x
= pos
.x
- 1, y
= pos
.y
, z
= pos
.z
}, 0)
80 check_remove({x
= pos
.x
+ 1, y
= pos
.y
, z
= pos
.z
}, 0)
81 check_remove({x
= pos
.x
, y
= pos
.y
, z
= pos
.z
- 1}, 1)
82 check_remove({x
= pos
.x
, y
= pos
.y
, z
= pos
.z
+ 1}, 1)
83 check_remove({x
= pos
.x
, y
= pos
.y
- 1, z
= pos
.z
})
84 check_remove({x
= pos
.x
, y
= pos
.y
+ 1, z
= pos
.z
})
87 if not has_meta
then -- no meta means repeated call: function calls on every node destruction
90 if orientation
== 0 then
91 check_remove({x
= pos
.x
- 1, y
= pos
.y
, z
= pos
.z
}, 0)
92 check_remove({x
= pos
.x
+ 1, y
= pos
.y
, z
= pos
.z
}, 0)
94 check_remove({x
= pos
.x
, y
= pos
.y
, z
= pos
.z
- 1}, 1)
95 check_remove({x
= pos
.x
, y
= pos
.y
, z
= pos
.z
+ 1}, 1)
97 check_remove({x
= pos
.x
, y
= pos
.y
- 1, z
= pos
.z
})
98 check_remove({x
= pos
.x
, y
= pos
.y
+ 1, z
= pos
.z
})
101 minetest
.register_node("mcl_portals:portal", {
102 description
= S("Nether Portal"),
103 _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!"),
104 _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."),
112 name
= "mcl_portals_portal.png",
114 type = "vertical_frames",
121 name
= "mcl_portals_portal.png",
123 type = "vertical_frames",
130 drawtype
= "nodebox",
132 paramtype2
= "facedir",
133 sunlight_propagates
= true,
134 use_texture_alpha
= true,
138 buildable_to
= false,
139 is_ground_content
= false,
142 post_effect_color
= {a
= 180, r
= 51, g
= 7, b
= 89},
147 {-0.5, -0.5, -0.1, 0.5, 0.5, 0.1},
150 groups
= {portal
=1, not_in_creative_inventory
= 1},
151 on_destruct
= destroy_nether_portal
,
154 _mcl_blast_resistance
= 0,
157 local function find_target_y(x
, y
, z
, y_min
, y_max
)
158 local y_org
= math
.max(math
.min(y
, y_max
), y_min
)
159 local node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
163 while node
.name
~= "air" and y
< y_max
do
165 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
171 if node
.name
~= "air" then
175 while node
== nil and y
> y_min
do
177 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
179 if y
== y_max
and node
~= nil then -- try reverse direction who knows what they built there...
180 while node
.name
~= "air" and y
> y_min
do
182 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
191 while node
.name
== "air" and y
> y_min
do
193 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
194 while node
== nil and y
> y_min
do
196 node
= minetest
.get_node_or_nil({x
= x
, y
= y
, z
= z
})
205 return math
.max(math
.min(y
, y_max
), y_min
)
208 local function find_nether_target_y(x
, y
, z
)
209 local target_y
= find_target_y(x
, y
, z
, nether_ymin
+ 4, nether_ymax
- 25) + 1
210 minetest
.log("verbose", "[mcl_portal] Found Nether target altitude: " .. tostring(target_y
) .. " for pos. " .. minetest
.pos_to_string({x
= x
, y
= y
, z
= z
}))
214 local function find_overworld_target_y(x
, y
, z
)
215 local target_y
= find_target_y(x
, y
, z
, overworld_ymin
+ 4, overworld_ymax
- 25) + 1
216 local node
= minetest
.get_node({x
= x
, y
= target_y
- 1, z
= z
})
221 if nn
~= "air" and minetest
.get_item_group(nn
, "water") == 0 then
222 target_y
= target_y
+ 1
224 minetest
.log("verbose", "[mcl_portal] Found Overworld target altitude: " .. tostring(target_y
) .. " for pos. " .. minetest
.pos_to_string({x
= x
, y
= y
, z
= z
}))
229 local function update_target(pos
, target
, time_str
)
230 local stack
= {{x
= pos
.x
, y
= pos
.y
, z
= pos
.z
}}
233 local meta
= minetest
.get_meta(stack
[i
])
234 if meta
:get_string("portal_time") == time_str
then
235 stack
[i
] = nil -- Already updated, skip it
237 local node
= minetest
.get_node(stack
[i
])
238 local portal
= node
.name
== "mcl_portals:portal"
242 local x
, y
, z
= stack
[i
].x
, stack
[i
].y
, stack
[i
].z
243 meta
:set_string("portal_time", time_str
)
244 meta
:set_string("portal_target", target
)
246 stack
[i
+ 1] = {x
= x
, y
= y
+ 1, z
= z
}
247 if node
.param2
== 0 then
248 stack
[i
+ 2] = {x
= x
- 1, y
= y
, z
= z
}
249 stack
[i
+ 3] = {x
= x
+ 1, y
= y
, z
= z
}
251 stack
[i
+ 2] = {x
= x
, y
= y
, z
= z
- 1}
252 stack
[i
+ 3] = {x
= x
, y
= y
, z
= z
+ 1}
259 local function ecb_setup_target_portal(blockpos
, action
, calls_remaining
, param
)
260 -- param.: srcx, srcy, srcz, dstx, dsty, dstz, srcdim, ax1, ay1, az1, ax2, ay2, az2
262 local portal_search
= function(target
, p1
, p2
)
263 local portal_nodes
= minetest
.find_nodes_in_area(p1
, p2
, "mcl_portals:portal")
264 local portal_pos
= false
265 if portal_nodes
and #portal_nodes
> 0 then
266 -- Found some portal(s), use nearest:
267 portal_pos
= {x
= portal_nodes
[1].x
, y
= portal_nodes
[1].y
, z
= portal_nodes
[1].z
}
268 local nearest_distance
= vector
.distance(target
, portal_pos
)
269 for n
= 2, #portal_nodes
do
270 local distance
= vector
.distance(target
, portal_nodes
[n
])
271 if distance
< nearest_distance
then
272 portal_pos
= {x
= portal_nodes
[n
].x
, y
= portal_nodes
[n
].y
, z
= portal_nodes
[n
].z
}
273 nearest_distance
= distance
276 end -- here we have the best portal_pos
280 if calls_remaining
<= 0 then
281 minetest
.log("action", "[mcl_portal] Area for destination Nether portal emerged!")
282 local src_pos
= {x
= param
.srcx
, y
= param
.srcy
, z
= param
.srcz
}
283 local dst_pos
= {x
= param
.dstx
, y
= param
.dsty
, z
= param
.dstz
}
284 local meta
= minetest
.get_meta(src_pos
)
285 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
})
287 if portal_pos
== false then
288 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
}))
289 -- Need to build arrival portal:
290 local org_dst_y
= dst_pos
.y
291 if param
.srcdim
== "overworld" then
292 dst_pos
.y
= find_nether_target_y(dst_pos
.x
, dst_pos
.y
, dst_pos
.z
)
294 dst_pos
.y
= find_overworld_target_y(dst_pos
.x
, dst_pos
.y
, dst_pos
.z
)
296 if math
.abs(org_dst_y
- dst_pos
.y
) >= PORTAL_SEARCH_ALTITUDE
/ 2 then
297 portal_pos
= portal_search(dst_pos
,
298 {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
},
299 {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 if portal_pos
== false then
303 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
}))
304 local width
, height
= 2, 3
305 portal_pos
= mcl_portals
.build_nether_portal(dst_pos
, width
, height
)
309 local target_meta
= minetest
.get_meta(portal_pos
)
310 local p3
= minetest
.string_to_pos(target_meta
:get_string("portal_frame1"))
311 local p4
= minetest
.string_to_pos(target_meta
:get_string("portal_frame2"))
313 portal_pos
= vector
.divide(vector
.add(p3
, p4
), 2.0)
314 portal_pos
.y
= math
.min(p3
.y
, p4
.y
)
315 portal_pos
= vector
.round(portal_pos
)
316 local node
= minetest
.get_node(portal_pos
)
317 if node
and node
.name
~= "mcl_portals:portal" then
318 portal_pos
= {x
= p3
.x
, y
= p3
.y
, z
= p3
.z
}
319 if minetest
.get_node(portal_pos
).name
== "mcl_core:obsidian" then
320 -- Old-version portal:
322 portal_pos
= {x
= p3
.x
+ 1, y
= p3
.y
+ 1, z
= p3
.z
}
324 portal_pos
= {x
= p3
.x
, y
= p3
.y
+ 1, z
= p3
.z
+ 1}
329 local time_str
= tostring(minetest
.get_us_time())
330 local target
= minetest
.pos_to_string(portal_pos
)
332 update_target(src_pos
, target
, time_str
)
336 local function nether_portal_get_target_position(src_pos
)
337 local _
, current_dimension
= mcl_worlds
.y_to_layer(src_pos
.y
)
338 local x
, y
, z
, y_min
, y_max
= 0, 0, 0, 0, 0
339 if current_dimension
== "nether" then
340 x
= math
.floor(nether_to_overworld(src_pos
.x
) + 0.5)
341 z
= math
.floor(nether_to_overworld(src_pos
.z
) + 0.5)
342 y
= math
.floor((math
.min(math
.max(src_pos
.y
, nether_ymin
), nether_ymax
) - nether_ymin
) / nether_dy
* overworld_dy
+ overworld_ymin
+ 0.5)
343 y_min
= overworld_ymin
344 y_max
= overworld_ymax
346 x
= math
.floor(src_pos
.x
/ OVERWORLD_TO_NETHER_SCALE
+ 0.5)
347 z
= math
.floor(src_pos
.z
/ OVERWORLD_TO_NETHER_SCALE
+ 0.5)
348 y
= math
.floor((math
.min(math
.max(src_pos
.y
, overworld_ymin
), overworld_ymax
) - overworld_ymin
) / overworld_dy
* nether_dy
+ nether_ymin
+ 0.5)
352 return x
, y
, z
, current_dimension
, y_min
, y_max
355 local function find_or_create_portal(src_pos
)
356 local x
, y
, z
, cdim
, y_min
, y_max
= nether_portal_get_target_position(src_pos
)
357 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
}
358 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
}
359 if pos1
.y
== y_min
then
360 pos2
.y
= math
.min(y_max
, pos1
.y
+ PORTAL_SEARCH_ALTITUDE
)
362 if pos2
.y
== y_max
then
363 pos1
.y
= math
.max(y_min
, pos2
.y
- PORTAL_SEARCH_ALTITUDE
)
366 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
})
369 local function emerge_target_area(src_pos
)
370 local x
, y
, z
, cdim
, y_min
, y_max
= nether_portal_get_target_position(src_pos
)
371 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
}
372 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
}
373 minetest
.emerge_area(pos1
, pos2
)
374 pos1
= {x
= x
- 1, y
= y_min
, z
= z
- 1}
375 pos2
= {x
= x
+ 1, y
= y_max
, z
= z
+ 1}
376 minetest
.emerge_area(pos1
, pos2
)
379 local function available_for_nether_portal(p
)
380 local nn
= minetest
.get_node(p
).name
381 local obsidian
= nn
== "mcl_core:obsidian"
382 if nn
~= "air" and minetest
.get_item_group(nn
, "fire") ~= 1 then
383 return false, obsidian
385 return true, obsidian
388 local function light_frame(x1
, y1
, z1
, x2
, y2
, z2
, build_frame
)
389 local build_frame
= build_frame
or false
390 local orientation
= 0
397 local protection
= false
399 for x
= x1
- 1 + orientation
, x2
+ 1 - orientation
do
400 for z
= z1
- orientation
, z2
+ orientation
do
401 for y
= y1
- 1, y2
+ 1 do
402 local frame
= (x
< x1
) or (x
> x2
) or (y
< y1
) or (y
> y2
) or (z
< z1
) or (z
> z2
)
406 if minetest
.is_protected({x
= x
, y
= y
, z
= z
}, "") then
408 local offset_x
= math
.random(-disperse
, disperse
)
409 local offset_z
= math
.random(-disperse
, disperse
)
410 disperse
= disperse
+ math
.random(25, 177)
411 if disperse
> 5000 then
414 x1
, z1
= x1
+ offset_x
, z1
+ offset_z
415 x2
, z2
= x2
+ offset_x
, z2
+ offset_z
416 local _
, dimension
= mcl_worlds
.y_to_layer(y1
)
417 local height
= math
.abs(y2
- y1
)
419 if dimension
== "nether" then
420 y1
= find_nether_target_y(math
.min(x1
, x2
), y1
, math
.min(z1
, z2
))
422 y1
= find_overworld_target_y(math
.min(x1
, x2
), y1
, math
.min(z1
, z2
))
428 minetest
.set_node({x
= x
, y
= y
, z
= z
}, {name
= "mcl_core:obsidian"})
432 if not build_frame
or pass
== 2 then
433 local node
= minetest
.get_node({x
= x
, y
= y
, z
= z
})
434 minetest
.set_node({x
= x
, y
= y
, z
= z
}, {name
= "mcl_portals:portal", param2
= orientation
})
437 if not frame
and pass
== 2 then
438 local meta
= minetest
.get_meta({x
= x
, y
= y
, z
= z
})
439 -- Portal frame corners
440 meta
:set_string("portal_frame1", minetest
.pos_to_string({x
= x1
, y
= y1
, z
= z1
}))
441 meta
:set_string("portal_frame2", minetest
.pos_to_string({x
= x2
, y
= y2
, z
= z2
}))
442 -- Portal target coordinates
443 meta
:set_string("portal_target", "")
444 -- Portal last teleportation time
445 meta
:set_string("portal_time", tostring(0))
456 if build_frame
== false or pass
== 2 then
459 if build_frame
and not protection
and pass
== 1 then
463 emerge_target_area({x
= x1
, y
= y1
, z
= z1
})
464 return {x
= x1
, y
= y1
, z
= z1
}
467 --Build arrival portal
468 function mcl_portals
.build_nether_portal(pos
, width
, height
, orientation
)
469 local height
= height
or FRAME_SIZE_Y_MIN
- 2
470 local width
= width
or FRAME_SIZE_X_MIN
- 2
471 local orientation
= orientation
or math
.random(0, 1)
473 if orientation
== 0 then
474 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})
476 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})
479 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)
481 -- Clear some space around:
482 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
483 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
484 for y
= pos
.y
- 1, pos
.y
+ height
+ math
.random(1,6) do
485 local nn
= minetest
.get_node({x
= x
, y
= y
, z
= z
}).name
486 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
487 minetest
.remove_node({x
= x
, y
= y
, z
= z
})
493 -- Build obsidian platform:
494 for x
= pos
.x
- orientation
, pos
.x
+ orientation
+ (width
- 1) * (1 - orientation
), 1 + orientation
do
495 for z
= pos
.z
- 1 + orientation
, pos
.z
+ 1 - orientation
+ (width
- 1) * orientation
, 2 - orientation
do
496 local pp
= {x
= x
, y
= pos
.y
- 1, z
= z
}
497 local nn
= minetest
.get_node(pp
).name
498 if not minetest
.registered_nodes
[nn
].is_ground_content
and not minetest
.is_protected(pp
, "") then
499 minetest
.set_node(pp
, {name
= "mcl_core:obsidian"})
504 minetest
.log("action", "[mcl_portal] Destination Nether portal generated at "..minetest
.pos_to_string(pos
).."!")
509 local function check_and_light_shape(pos
, orientation
)
510 local stack
= {{x
= pos
.x
, y
= pos
.y
, z
= pos
.z
}}
512 local node_counter
= 0
513 -- Search most low node from the left (pos1) and most right node from the top (pos2)
514 local pos1
= {x
= pos
.x
, y
= pos
.y
, z
= pos
.z
}
515 local pos2
= {x
= pos
.x
, y
= pos
.y
, z
= pos
.z
}
517 local wrong_portal_nodes_clean_up
= function(node_list
)
518 for i
= 1, #node_list
do
519 local meta
= minetest
.get_meta(node_list
[i
])
520 meta
:set_string("portal_time", "")
527 local meta
= minetest
.get_meta(stack
[i
])
528 local target
= meta
:get_string("portal_time")
529 if target
and target
== "-2" then
530 stack
[i
] = nil -- Already checked, skip it
532 local good
, obsidian
= available_for_nether_portal(stack
[i
])
536 if (not good
) or (node_counter
>= PORTAL_NODES_MAX
) then
537 return wrong_portal_nodes_clean_up(node_list
)
539 local x
, y
, z
= stack
[i
].x
, stack
[i
].y
, stack
[i
].z
540 meta
:set_string("portal_time", "-2")
541 node_counter
= node_counter
+ 1
542 node_list
[node_counter
] = {x
= x
, y
= y
, z
= z
}
544 stack
[i
+ 1] = {x
= x
, y
= y
+ 1, z
= z
}
545 if orientation
== 0 then
546 stack
[i
+ 2] = {x
= x
- 1, y
= y
, z
= z
}
547 stack
[i
+ 3] = {x
= x
+ 1, y
= y
, z
= z
}
549 stack
[i
+ 2] = {x
= x
, y
= y
, z
= z
- 1}
550 stack
[i
+ 3] = {x
= x
, y
= y
, z
= z
+ 1}
552 if (y
< pos1
.y
) or (y
== pos1
.y
and (x
< pos1
.x
or z
< pos1
.z
)) then
553 pos1
= {x
= x
, y
= y
, z
= z
}
555 if (x
> pos2
.x
or z
> pos2
.z
) or (x
== pos2
.x
and z
== pos2
.z
and y
> pos2
.y
) then
556 pos2
= {x
= x
, y
= y
, z
= z
}
562 if node_counter
< PORTAL_NODES_MIN
then
563 return wrong_portal_nodes_clean_up(node_list
)
566 -- Limit rectangles width and height
567 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
568 return wrong_portal_nodes_clean_up(node_list
)
571 for i
= 1, node_counter
do
572 local node_pos
= node_list
[i
]
573 local node
= minetest
.get_node(node_pos
)
574 minetest
.set_node(node_pos
, {name
= "mcl_portals:portal", param2
= orientation
})
575 local meta
= minetest
.get_meta(node_pos
)
576 meta
:set_string("portal_frame1", minetest
.pos_to_string(pos1
))
577 meta
:set_string("portal_frame2", minetest
.pos_to_string(pos2
))
578 meta
:set_string("portal_time", tostring(0))
579 meta
:set_string("portal_target", "")
584 -- Attempts to light a Nether portal at pos
585 -- Pos can be any of the inner part.
586 -- The frame MUST be filled only with air or any fire, which will be replaced with Nether portal blocks.
587 -- If no Nether portal can be lit, nothing happens.
588 -- Returns number of portals created (0, 1 or 2)
589 function mcl_portals
.light_nether_portal(pos
)
590 -- Only allow to make portals in Overworld and Nether
591 local dim
= mcl_worlds
.pos_to_dimension(pos
)
592 if dim
~= "overworld" and dim
~= "nether" then
595 local orientation
= math
.random(0, 1)
596 for orientation_iteration
= 1, 2 do
597 if check_and_light_shape(pos
, orientation
) then
600 orientation
= 1 - orientation
605 local function update_portal_time(pos
, time_str
)
606 local stack
= {{x
= pos
.x
, y
= pos
.y
, z
= pos
.z
}}
609 local meta
= minetest
.get_meta(stack
[i
])
610 if meta
:get_string("portal_time") == time_str
then
611 stack
[i
] = nil -- Already updated, skip it
613 local node
= minetest
.get_node(stack
[i
])
614 local portal
= node
.name
== "mcl_portals:portal"
618 local x
, y
, z
= stack
[i
].x
, stack
[i
].y
, stack
[i
].z
619 meta
:set_string("portal_time", time_str
)
621 stack
[i
+ 1] = {x
= x
, y
= y
+ 1, z
= z
}
622 if node
.param2
== 0 then
623 stack
[i
+ 2] = {x
= x
- 1, y
= y
, z
= z
}
624 stack
[i
+ 3] = {x
= x
+ 1, y
= y
, z
= z
}
626 stack
[i
+ 2] = {x
= x
, y
= y
, z
= z
- 1}
627 stack
[i
+ 3] = {x
= x
, y
= y
, z
= z
+ 1}
634 local function prepare_target(pos
)
635 local meta
, us_time
= minetest
.get_meta(pos
), minetest
.get_us_time()
636 local portal_time
= tonumber(meta
:get_string("portal_time")) or 0
637 local delta_time_us
= us_time
- portal_time
638 local pos1
, pos2
= minetest
.string_to_pos(meta
:get_string("portal_frame1")), minetest
.string_to_pos(meta
:get_string("portal_frame2"))
639 if delta_time_us
<= DESTINATION_EXPIRES
then
640 -- Destination point must be still cached according to https://minecraft.gamepedia.com/Nether_portal
641 return update_portal_time(pos
, tostring(us_time
))
643 -- No cached destination point
644 find_or_create_portal(pos
)
647 -- Teleportation cooloff for some seconds, to prevent back-and-forth teleportation
648 local function stop_teleport_cooloff(o
)
649 mcl_portals
.nether_portal_cooloff
[o
] = false
650 touch_chatter_prevention
[o
] = nil
653 local function teleport_cooloff(obj
)
654 if obj
:is_player() then
655 minetest
.after(TELEPORT_COOLOFF
, stop_teleport_cooloff
, obj
)
657 minetest
.after(MOB_TELEPORT_COOLOFF
, stop_teleport_cooloff
, obj
)
662 local function teleport_no_delay(obj
, pos
)
663 local is_player
= obj
:is_player()
664 if (not obj
:get_luaentity()) and (not is_player
) then
668 local objpos
= obj
:get_pos()
669 if objpos
== nil then
673 if mcl_portals
.nether_portal_cooloff
[obj
] then
676 -- If player stands, player is at ca. something+0.5
677 -- which might cause precision problems, so we used ceil.
678 objpos
.y
= math
.ceil(objpos
.y
)
680 if minetest
.get_node(objpos
).name
~= "mcl_portals:portal" then
684 local meta
= minetest
.get_meta(pos
)
685 local delta_time
= minetest
.get_us_time() - (tonumber(meta
:get_string("portal_time")) or 0)
686 local target
= minetest
.string_to_pos(meta
:get_string("portal_target"))
687 if delta_time
> DESTINATION_EXPIRES
or target
== nil then
688 -- Area not ready yet - retry after a second
689 return minetest
.after(1, teleport_no_delay
, obj
, pos
)
692 -- Enable teleportation cooloff for some seconds, to prevent back-and-forth teleportation
693 teleport_cooloff(obj
)
694 mcl_portals
.nether_portal_cooloff
[obj
] = true
700 mcl_worlds
.dimension_change(obj
, mcl_worlds
.pos_to_dimension(target
))
701 minetest
.sound_play("mcl_portals_teleport", {pos
=target
, gain
=0.5, max_hear_distance
= 16}, true)
702 local name
= obj
:get_player_name()
703 minetest
.log("action", "[mcl_portal] "..name
.." teleported to Nether portal at "..minetest
.pos_to_string(target
)..".")
707 local function prevent_portal_chatter(obj
)
708 local time_us
= minetest
.get_us_time()
709 local chatter
= touch_chatter_prevention
[obj
] or 0
710 touch_chatter_prevention
[obj
] = time_us
711 minetest
.after(TOUCH_CHATTER_TIME
, function(o
)
712 if not o
or not touch_chatter_prevention
[o
] then
715 if minetest
.get_us_time() - touch_chatter_prevention
[o
] >= TOUCH_CHATTER_TIME_US
then
716 touch_chatter_prevention
[o
] = nil
719 return time_us
- chatter
> TOUCH_CHATTER_TIME_US
722 local function animation(player
, playername
)
723 local chatter
= touch_chatter_prevention
[player
] or 0
724 if mcl_portals
.nether_portal_cooloff
[player
] or minetest
.get_us_time() - chatter
< TOUCH_CHATTER_TIME_US
then
725 local pos
= player
:get_pos()
726 minetest
.add_particlespawner({
728 minpos
= {x
= pos
.x
- 0.1, y
= pos
.y
+ 1.4, z
= pos
.z
- 0.1},
729 maxpos
= {x
= pos
.x
+ 0.1, y
= pos
.y
+ 1.6, z
= pos
.z
+ 0.1},
738 collisiondetection
= false,
739 texture
= "mcl_particles_nether_portal_t.png",
740 playername
= playername
,
742 minetest
.after(0.3, animation
, player
, playername
)
746 local function teleport(obj
, portal_pos
)
748 if obj
:is_player() then
749 name
= obj
:get_player_name()
752 -- Call prepare_target() first because it might take a long
753 prepare_target(portal_pos
)
754 -- Prevent quick back-and-forth teleportation
755 if not mcl_portals
.nether_portal_cooloff
[obj
] then
756 local creative_enabled
= minetest
.is_creative_enabled(name
)
757 if creative_enabled
then
758 return teleport_no_delay(obj
, portal_pos
)
760 minetest
.after(TELEPORT_DELAY
, teleport_no_delay
, obj
, portal_pos
)
764 minetest
.register_abm({
765 label
= "Nether portal teleportation and particles",
766 nodenames
= {"mcl_portals:portal"},
769 action
= function(pos
, node
)
770 local o
= node
.param2
-- orientation
771 local d
= math
.random(0, 1) -- direction
772 local time
= math
.random() * 1.9 + 0.5
773 local velocity
, acceleration
775 velocity
= {x
= math
.random() * 0.7 + 0.3, y
= math
.random() - 0.5, z
= math
.random() - 0.5}
776 acceleration
= {x
= math
.random() * 1.1 + 0.3, y
= math
.random() - 0.5, z
= math
.random() - 0.5}
778 velocity
= {x
= math
.random() - 0.5, y
= math
.random() - 0.5, z
= math
.random() * 0.7 + 0.3}
779 acceleration
= {x
= math
.random() - 0.5, y
= math
.random() - 0.5, z
= math
.random() * 1.1 + 0.3}
781 local distance
= vector
.add(vector
.multiply(velocity
, time
), vector
.multiply(acceleration
, time
* time
/ 2))
784 distance
.x
= -distance
.x
785 velocity
.x
= -velocity
.x
786 acceleration
.x
= -acceleration
.x
788 distance
.z
= -distance
.z
789 velocity
.z
= -velocity
.z
790 acceleration
.z
= -acceleration
.z
793 distance
= vector
.subtract(pos
, distance
)
794 for _
, obj
in ipairs(minetest
.get_objects_inside_radius(pos
, 15)) do
795 if obj
:is_player() then
796 minetest
.add_particlespawner({
797 amount
= node_particles_allowed_level
+ 1,
802 minacc
= acceleration
,
803 maxacc
= acceleration
,
808 collisiondetection
= false,
809 texture
= "mcl_particles_nether_portal.png",
810 playername
= obj
:get_player_name(),
814 for _
, obj
in ipairs(minetest
.get_objects_inside_radius(pos
, 1)) do --maikerumine added for objects to travel
815 local lua_entity
= obj
:get_luaentity() --maikerumine added for objects to travel
816 if (obj
:is_player() or lua_entity
) and prevent_portal_chatter(obj
) then
824 --[[ ITEM OVERRIDES ]]
826 local longdesc
= minetest
.registered_nodes
["mcl_core:obsidian"]._doc_items_longdesc
827 longdesc
= longdesc
.. "\n" .. S("Obsidian is also used as the frame of Nether portals.")
828 local usagehelp
= S("To open a Nether portal, place an upright frame of obsidian with a width of at least 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.")
830 minetest
.override_item("mcl_core:obsidian", {
831 _doc_items_longdesc
= longdesc
,
832 _doc_items_usagehelp
= usagehelp
,
833 on_destruct
= destroy_nether_portal
,
834 _on_ignite
= function(user
, pointed_thing
)
835 local x
, y
, z
= pointed_thing
.under
.x
, pointed_thing
.under
.y
, pointed_thing
.under
.z
836 -- Check empty spaces around obsidian and light all frames found:
837 local portals_placed
=
838 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
839 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
840 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})
841 if portals_placed
then
842 minetest
.log("action", "[mcl_portal] Nether portal activated at "..minetest
.pos_to_string({x
=x
,y
=y
,z
=z
})..".")
843 if minetest
.get_modpath("doc") then
844 doc
.mark_entry_as_revealed(user
:get_player_name(), "nodes", "mcl_portals:portal")
846 -- Achievement for finishing a Nether portal TO the Nether
847 local dim
= mcl_worlds
.pos_to_dimension({x
=x
, y
=y
, z
=z
})
848 if minetest
.get_modpath("awards") and dim
~= "nether" and user
:is_player() then
849 awards
.unlock(user
:get_player_name(), "mcl:buildNetherPortal")