Harden a number of minetest.after player checks
[MineClone/MineClone2.git] / mods / ITEMS / mcl_portals / portal_nether.lua
blob1c1f67742155ba641fe679d8293a09619a2e5718
1 -- Parameters
3 local TCAVE = 0.6
4 local nobj_cave = nil
6 -- Portal frame sizes
7 local FRAME_SIZE_X_MIN = 4
8 local FRAME_SIZE_Y_MIN = 5
9 local FRAME_SIZE_X_MAX = 23
10 local FRAME_SIZE_Y_MAX = 23
12 local mg_name = minetest.get_mapgen_setting("mg_name")
14 -- 3D noise
15 local np_cave = {
16 offset = 0,
17 scale = 1,
18 spread = {x = 384, y = 128, z = 384},
19 seed = 59033,
20 octaves = 5,
21 persist = 0.7
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 local portal_cooloff = {}
29 -- Destroy portal if pos (portal frame or portal node) got destroyed
30 local destroy_portal = function(pos)
31 -- Deactivate Nether portal
32 local meta = minetest.get_meta(pos)
33 local p1 = minetest.string_to_pos(meta:get_string("portal_frame1"))
34 local p2 = minetest.string_to_pos(meta:get_string("portal_frame2"))
35 if not p1 or not p2 then
36 return
37 end
39 local counter = 1
41 local mp1
42 for x = p1.x, p2.x do
43 for y = p1.y, p2.y do
44 for z = p1.z, p2.z do
45 local p = vector.new(x, y, z)
46 local m = minetest.get_meta(p)
47 if counter == 2 then
48 --[[ Only proceed if the second node still has metadata.
49 (first node is a corner and not needed for the portal)
50 If it doesn't have metadata, another node propably triggred the delection
51 routine earlier, so we bail out earlier to avoid an infinite cascade
52 of on_destroy events. ]]
53 mp1 = minetest.string_to_pos(m:get_string("portal_frame1"))
54 if not mp1 then
55 return
56 end
57 end
58 local nn = minetest.get_node(p).name
59 if nn == "mcl_core:obsidian" or nn == "mcl_portals:portal" then
60 -- Remove portal nodes, but not myself
61 if nn == "mcl_portals:portal" and not vector.equals(p, pos) then
62 minetest.remove_node(p)
63 end
64 -- Clear metadata of portal nodes and the frame
65 m:set_string("portal_frame1", "")
66 m:set_string("portal_frame2", "")
67 m:set_string("portal_target", "")
68 end
69 counter = counter + 1
70 end
71 end
72 end
73 end
75 minetest.register_node("mcl_portals:portal", {
76 description = "Nether Portal",
77 _doc_items_longdesc = "A Nether portal teleports creatures and objects to the hot and dangerous Nether dimension (and back!). Enter at your own risk!",
78 _doc_items_usagehelp = "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.",
80 tiles = {
81 "blank.png",
82 "blank.png",
83 "blank.png",
84 "blank.png",
86 name = "mcl_portals_portal.png",
87 animation = {
88 type = "vertical_frames",
89 aspect_w = 16,
90 aspect_h = 16,
91 length = 0.5,
95 name = "mcl_portals_portal.png",
96 animation = {
97 type = "vertical_frames",
98 aspect_w = 16,
99 aspect_h = 16,
100 length = 0.5,
104 drawtype = "nodebox",
105 paramtype = "light",
106 paramtype2 = "facedir",
107 sunlight_propagates = true,
108 use_texture_alpha = true,
109 walkable = false,
110 diggable = false,
111 pointable = false,
112 buildable_to = false,
113 is_ground_content = false,
114 drop = "",
115 light_source = 11,
116 post_effect_color = {a = 180, r = 51, g = 7, b = 89},
117 alpha = 192,
118 node_box = {
119 type = "fixed",
120 fixed = {
121 {-0.5, -0.5, -0.1, 0.5, 0.5, 0.1},
124 groups = {not_in_creative_inventory = 1},
125 on_destruct = destroy_portal,
127 _mcl_hardness = -1,
128 _mcl_blast_resistance = 0,
131 -- Functions
132 --Build arrival portal
133 local function build_portal(pos, target)
134 local p = {x = pos.x - 1, y = pos.y - 1, z = pos.z}
135 local p1 = {x = pos.x - 1, y = pos.y - 1, z = pos.z}
136 local p2 = {x = p1.x + 3, y = p1.y + 4, z = p1.z}
138 for i = 1, FRAME_SIZE_Y_MIN - 1 do
139 minetest.set_node(p, {name = "mcl_core:obsidian"})
140 p.y = p.y + 1
142 for i = 1, FRAME_SIZE_X_MIN - 1 do
143 minetest.set_node(p, {name = "mcl_core:obsidian"})
144 p.x = p.x + 1
146 for i = 1, FRAME_SIZE_Y_MIN - 1 do
147 minetest.set_node(p, {name = "mcl_core:obsidian"})
148 p.y = p.y - 1
150 for i = 1, FRAME_SIZE_X_MIN - 1 do
151 minetest.set_node(p, {name = "mcl_core:obsidian"})
152 p.x = p.x - 1
155 for x = p1.x, p2.x do
156 for y = p1.y, p2.y do
157 p = {x = x, y = y, z = p1.z}
158 if not ((x == p1.x or x == p2.x) and (y == p1.y or y == p2.y)) then
159 if not (x == p1.x or x == p2.x or y == p1.y or y == p2.y) then
160 minetest.set_node(p, {name = "mcl_portals:portal", param2 = 0})
162 local meta = minetest.get_meta(p)
163 meta:set_string("portal_frame1", minetest.pos_to_string(p1))
164 meta:set_string("portal_frame2", minetest.pos_to_string(p2))
165 meta:set_string("portal_target", minetest.pos_to_string(target))
168 if y ~= p1.y then
169 for z = -2, 2 do
170 if z ~= 0 then
171 p.z = p.z + z
172 if minetest.registered_nodes[
173 minetest.get_node(p).name].is_ground_content then
174 minetest.remove_node(p)
176 p.z = p.z - z
184 local function find_nether_target_y(target_x, target_z)
185 if mg_name == "flat" then
186 return mcl_vars.mg_bedrock_nether_bottom_max + 5
188 local start_y = math.random(mcl_vars.mg_lava_nether_max + 1, mcl_vars.mg_bedrock_nether_top_min - 5) -- Search start
189 if not nobj_cave then
190 nobj_cave = minetest.get_perlin(np_cave)
192 local air = 4
194 for y = start_y, math.max(mcl_vars.mg_lava_nether_max + 1), -1 do
195 local nval_cave = nobj_cave:get3d({x = target_x, y = y, z = target_z})
197 if nval_cave > TCAVE then -- Cavern
198 air = air + 1
199 else -- Not cavern, check if 4 nodes of space above
200 if air >= 4 then
201 return y + 2
202 else -- Not enough space, reset air to zero
203 air = 0
208 return start_y -- Fallback
211 local function move_check(p1, max, dir)
212 local p = {x = p1.x, y = p1.y, z = p1.z}
213 local d = math.sign(max - p1[dir])
214 local min = p[dir]
216 for k = min, max, d do
217 p[dir] = k
218 local node = minetest.get_node(p)
219 -- Check for obsidian (except at corners)
220 if k ~= min and k ~= max and node.name ~= "mcl_core:obsidian" then
221 return false
223 -- Abort if any of the portal frame blocks already has metadata.
224 -- This mod does not yet portals which neighbor each other directly.
225 -- TODO: Reorganize the way how portal frame coordinates are stored.
226 if node.name == "mcl_core:obsidian" then
227 local meta = minetest.get_meta(p)
228 local pframe1 = meta:get_string("portal_frame1")
229 if minetest.string_to_pos(pframe1) ~= nil then
230 return false
235 return true
238 local function check_portal(p1, p2)
239 if p1.x ~= p2.x then
240 if not move_check(p1, p2.x, "x") then
241 return false
243 if not move_check(p2, p1.x, "x") then
244 return false
246 elseif p1.z ~= p2.z then
247 if not move_check(p1, p2.z, "z") then
248 return false
250 if not move_check(p2, p1.z, "z") then
251 return false
253 else
254 return false
257 if not move_check(p1, p2.y, "y") then
258 return false
260 if not move_check(p2, p1.y, "y") then
261 return false
264 return true
267 local function is_portal(pos)
268 local xsize, ysize = FRAME_SIZE_X_MIN-1, FRAME_SIZE_Y_MIN-1
269 for d = -xsize, xsize do
270 for y = -ysize, ysize do
271 local px = {x = pos.x + d, y = pos.y + y, z = pos.z}
272 local pz = {x = pos.x, y = pos.y + y, z = pos.z + d}
274 if check_portal(px, {x = px.x + xsize, y = px.y + ysize, z = px.z}) then
275 return px, {x = px.x + xsize, y = px.y + ysize, z = px.z}
277 if check_portal(pz, {x = pz.x, y = pz.y + ysize, z = pz.z + xsize}) then
278 return pz, {x = pz.x, y = pz.y + ysize, z = pz.z + xsize}
284 -- Attempts to light a Nether portal at pos and
285 -- select target position.
286 -- Pos can be any of the obsidian frame blocks or the inner part.
287 -- The frame MUST be filled only with air or any fire, which will be replaced with Nether portal blocks.
288 -- If no Nether portal can be lit, nothing happens.
289 -- Returns true on success and false on failure.
290 function mcl_portals.light_nether_portal(pos)
291 -- Only allow to make portals in Overworld and Nether
292 local dim = mcl_worlds.pos_to_dimension(pos)
293 if dim ~= "overworld" and dim ~= "nether" then
294 return false
296 -- Create Nether portal nodes
297 local p1, p2 = is_portal(pos)
298 if not p1 or not p2 then
299 return false
302 for d = 1, 2 do
303 for y = p1.y + 1, p2.y - 1 do
304 local p
305 if p1.z == p2.z then
306 p = {x = p1.x + d, y = y, z = p1.z}
307 else
308 p = {x = p1.x, y = y, z = p1.z + d}
310 local nn = minetest.get_node(p).name
311 if nn ~= "air" and minetest.get_item_group(nn, "fire") ~= 1 then
312 return false
317 local param2
318 if p1.z == p2.z then
319 param2 = 0
320 else
321 param2 = 1
324 -- Find target
326 local target = {x = p1.x, y = p1.y, z = p1.z}
327 target.x = target.x + 1
328 if target.y < mcl_vars.mg_nether_max and target.y > mcl_vars.mg_nether_min then
329 if mg_name == "flat" then
330 target.y = mcl_vars.mg_bedrock_overworld_max + 5
331 else
332 target.y = math.random(mcl_vars.mg_overworld_min + 40, mcl_vars.mg_overworld_min + 96)
334 else
335 target.y = find_nether_target_y(target.x, target.z)
338 local dmin, dmax, ymin, ymax = 0, FRAME_SIZE_X_MIN - 1, p1.y, p2.y
339 for d = dmin, dmax do
340 for y = ymin, ymax do
341 if not ((d == dmin or d == dmax) and (y == ymin or y == ymax)) then
342 local p
343 if param2 == 0 then
344 p = {x = p1.x + d, y = y, z = p1.z}
345 else
346 p = {x = p1.x, y = y, z = p1.z + d}
348 if d ~= dmin and d ~= dmax and y ~= ymin and y ~= ymax then
349 minetest.set_node(p, {name = "mcl_portals:portal", param2 = param2})
351 local meta = minetest.get_meta(p)
353 -- Portal frame corners
354 meta:set_string("portal_frame1", minetest.pos_to_string(p1))
355 meta:set_string("portal_frame2", minetest.pos_to_string(p2))
357 -- Portal target coordinates
358 meta:set_string("portal_target", minetest.pos_to_string(target))
363 return true
367 minetest.register_abm({
368 label = "Nether portal teleportation and particles",
369 nodenames = {"mcl_portals:portal"},
370 interval = 1,
371 chance = 2,
372 action = function(pos, node)
373 minetest.add_particlespawner(
374 32, --amount
375 4, --time
376 {x = pos.x - 0.25, y = pos.y - 0.25, z = pos.z - 0.25}, --minpos
377 {x = pos.x + 0.25, y = pos.y + 0.25, z = pos.z + 0.25}, --maxpos
378 {x = -0.8, y = -0.8, z = -0.8}, --minvel
379 {x = 0.8, y = 0.8, z = 0.8}, --maxvel
380 {x = 0, y = 0, z = 0}, --minacc
381 {x = 0, y = 0, z = 0}, --maxacc
382 0.5, --minexptime
383 1, --maxexptime
384 1, --minsize
385 2, --maxsize
386 false, --collisiondetection
387 "mcl_particles_teleport.png" --texture
389 for _,obj in ipairs(minetest.get_objects_inside_radius(pos,1)) do --maikerumine added for objects to travel
390 local lua_entity = obj:get_luaentity() --maikerumine added for objects to travel
391 if obj:is_player() or lua_entity then
392 -- Prevent quick back-and-forth teleportation
393 if portal_cooloff[obj] then
394 return
396 local meta = minetest.get_meta(pos)
397 local target = minetest.string_to_pos(meta:get_string("portal_target"))
398 if target then
399 -- force emerge of target area
400 minetest.get_voxel_manip():read_from_map(target, target)
401 if not minetest.get_node_or_nil(target) then
402 minetest.emerge_area(
403 vector.subtract(target, 4), vector.add(target, 4))
405 -- teleport the object
406 minetest.after(3, function(obj, pos, target)
407 if not obj:get_luaentity() then
408 return
410 -- Prevent quick back-and-forth teleportation
411 if portal_cooloff[obj] then
412 return
414 local objpos = obj:getpos()
415 if objpos == nil then
416 return
418 -- If player stands, player is at ca. something+0.5
419 -- which might cause precision problems, so we used ceil.
420 objpos.y = math.ceil(objpos.y)
422 if minetest.get_node(objpos).name ~= "mcl_portals:portal" then
423 return
426 -- Build target portal (if there isn't already one)
428 local n = minetest.get_node_or_nil(target)
429 if n and n.name ~= "mcl_portals:portal" then
430 local emerge_callback = function(blockpos, action, calls_remaining, param)
431 if calls_remaining <= 0 then
432 build_portal(param.target, param.pos, false)
435 minetest.emerge_area(vector.subtract(target, 7), vector.add(target, 7), emerge_callback, { pos = pos, target = target })
438 -- Teleport
439 obj:set_pos(target)
440 if obj:is_player() then
441 mcl_worlds.dimension_change(obj, mcl_worlds.pos_to_dimension(target))
442 minetest.sound_play("mcl_portals_teleport", {pos=target, gain=0.5, max_hear_distance = 16})
445 -- Enable teleportation cooloff for 4 seconds, to prevent back-and-forth teleportation
446 portal_cooloff[obj] = true
447 minetest.after(4, function(o)
448 portal_cooloff[o] = false
449 end, obj)
451 end, obj, pos, target)
455 end,
459 --[[ ITEM OVERRIDES ]]
461 local longdesc = minetest.registered_nodes["mcl_core:obsidian"]._doc_items_longdesc
462 longdesc = longdesc .. "\n" .. "Obsidian is also used as the frame of Nether portals."
463 local usagehelp = "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."
465 minetest.override_item("mcl_core:obsidian", {
466 _doc_items_longdesc = longdesc,
467 _doc_items_usagehelp = usagehelp,
468 on_destruct = destroy_portal,
469 _on_ignite = function(user, pointed_thing)
470 local pos = pointed_thing.under
471 local portal_placed = mcl_portals.light_nether_portal(pos)
472 if portal_placed and minetest.get_modpath("doc") then
473 doc.mark_entry_as_revealed(user:get_player_name(), "nodes", "mcl_portals:portal")
475 -- Achievement for finishing a Nether portal TO the Nether
476 local dim = mcl_worlds.pos_to_dimension(pos)
477 if minetest.get_modpath("awards") and dim ~= "nether" and user:is_player() then
478 awards.unlock(user:get_player_name(), "mcl:buildNetherPortal")
480 return true
481 else
482 return false
484 end,