Rewrite emerge algorithm of Netherportal gen.
[MineClone/MineClone2.git] / mods / ITEMS / mcl_portals / portal_nether.lua
blobe9f5a8d9fb46685a0697597f4bd140da02692577
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 NETHER_PORTAL_TELEPORT_DELAY = 3 -- seconds before teleporting in Nether portal
14 local mg_name = minetest.get_mapgen_setting("mg_name")
16 -- 3D noise
17 local np_cave = {
18 offset = 0,
19 scale = 1,
20 spread = {x = 384, y = 128, z = 384},
21 seed = 59033,
22 octaves = 5,
23 persist = 0.7
26 -- Table of objects (including players) which recently teleported by a
27 -- Nether portal. Those objects have a brief cooloff period before they
28 -- can teleport again. This prevents annoying back-and-forth teleportation.
29 local portal_cooloff = {}
31 -- Destroy portal if pos (portal frame or portal node) got destroyed
32 local destroy_portal = function(pos)
33 -- Deactivate Nether portal
34 local meta = minetest.get_meta(pos)
35 local p1 = minetest.string_to_pos(meta:get_string("portal_frame1"))
36 local p2 = minetest.string_to_pos(meta:get_string("portal_frame2"))
37 if not p1 or not p2 then
38 return
39 end
41 local counter = 1
43 local mp1
44 for x = p1.x, p2.x do
45 for y = p1.y, p2.y do
46 for z = p1.z, p2.z do
47 local p = vector.new(x, y, z)
48 local m = minetest.get_meta(p)
49 if counter == 2 then
50 --[[ Only proceed if the second node still has metadata.
51 (first node is a corner and not needed for the portal)
52 If it doesn't have metadata, another node propably triggred the delection
53 routine earlier, so we bail out earlier to avoid an infinite cascade
54 of on_destroy events. ]]
55 mp1 = minetest.string_to_pos(m:get_string("portal_frame1"))
56 if not mp1 then
57 return
58 end
59 end
60 local nn = minetest.get_node(p).name
61 if nn == "mcl_core:obsidian" or nn == "mcl_portals:portal" then
62 -- Remove portal nodes, but not myself
63 if nn == "mcl_portals:portal" and not vector.equals(p, pos) then
64 minetest.remove_node(p)
65 end
66 -- Clear metadata of portal nodes and the frame
67 m:set_string("portal_frame1", "")
68 m:set_string("portal_frame2", "")
69 m:set_string("portal_target", "")
70 end
71 counter = counter + 1
72 end
73 end
74 end
75 end
77 minetest.register_node("mcl_portals:portal", {
78 description = "Nether Portal",
79 _doc_items_longdesc = "A Nether portal teleports creatures and objects to the hot and dangerous Nether dimension (and back!). Enter at your own risk!",
80 _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.",
82 tiles = {
83 "blank.png",
84 "blank.png",
85 "blank.png",
86 "blank.png",
88 name = "mcl_portals_portal.png",
89 animation = {
90 type = "vertical_frames",
91 aspect_w = 16,
92 aspect_h = 16,
93 length = 0.5,
97 name = "mcl_portals_portal.png",
98 animation = {
99 type = "vertical_frames",
100 aspect_w = 16,
101 aspect_h = 16,
102 length = 0.5,
106 drawtype = "nodebox",
107 paramtype = "light",
108 paramtype2 = "facedir",
109 sunlight_propagates = true,
110 use_texture_alpha = true,
111 walkable = false,
112 diggable = false,
113 pointable = false,
114 buildable_to = false,
115 is_ground_content = false,
116 drop = "",
117 light_source = 11,
118 post_effect_color = {a = 180, r = 51, g = 7, b = 89},
119 alpha = 192,
120 node_box = {
121 type = "fixed",
122 fixed = {
123 {-0.5, -0.5, -0.1, 0.5, 0.5, 0.1},
126 groups = {not_in_creative_inventory = 1},
127 on_destruct = destroy_portal,
129 _mcl_hardness = -1,
130 _mcl_blast_resistance = 0,
133 -- Functions
134 --Build arrival portal
135 local function build_portal(pos, target)
136 local p = {x = pos.x - 1, y = pos.y - 1, z = pos.z}
137 local p1 = {x = pos.x - 1, y = pos.y - 1, z = pos.z}
138 local p2 = {x = p1.x + 3, y = p1.y + 4, z = p1.z}
140 for i = 1, FRAME_SIZE_Y_MIN - 1 do
141 minetest.set_node(p, {name = "mcl_core:obsidian"})
142 p.y = p.y + 1
144 for i = 1, FRAME_SIZE_X_MIN - 1 do
145 minetest.set_node(p, {name = "mcl_core:obsidian"})
146 p.x = p.x + 1
148 for i = 1, FRAME_SIZE_Y_MIN - 1 do
149 minetest.set_node(p, {name = "mcl_core:obsidian"})
150 p.y = p.y - 1
152 for i = 1, FRAME_SIZE_X_MIN - 1 do
153 minetest.set_node(p, {name = "mcl_core:obsidian"})
154 p.x = p.x - 1
157 for x = p1.x, p2.x do
158 for y = p1.y, p2.y do
159 p = {x = x, y = y, z = p1.z}
160 if not ((x == p1.x or x == p2.x) and (y == p1.y or y == p2.y)) then
161 if not (x == p1.x or x == p2.x or y == p1.y or y == p2.y) then
162 minetest.set_node(p, {name = "mcl_portals:portal", param2 = 0})
164 local meta = minetest.get_meta(p)
165 meta:set_string("portal_frame1", minetest.pos_to_string(p1))
166 meta:set_string("portal_frame2", minetest.pos_to_string(p2))
167 meta:set_string("portal_target", minetest.pos_to_string(target))
170 if y ~= p1.y then
171 for z = -2, 2 do
172 if z ~= 0 then
173 p.z = p.z + z
174 if minetest.registered_nodes[
175 minetest.get_node(p).name].is_ground_content then
176 minetest.remove_node(p)
178 p.z = p.z - z
184 minetest.log("action", "[mcl_portal] Destination Nether portal generated at "..minetest.pos_to_string(p2).."!")
187 local function find_nether_target_y(target_x, target_z)
188 if mg_name == "flat" then
189 return mcl_vars.mg_bedrock_nether_bottom_max + 5
191 local start_y = math.random(mcl_vars.mg_lava_nether_max + 1, mcl_vars.mg_bedrock_nether_top_min - 5) -- Search start
192 if not nobj_cave then
193 nobj_cave = minetest.get_perlin(np_cave)
195 local air = 4
197 for y = start_y, math.max(mcl_vars.mg_lava_nether_max + 1), -1 do
198 local nval_cave = nobj_cave:get3d({x = target_x, y = y, z = target_z})
200 if nval_cave > TCAVE then -- Cavern
201 air = air + 1
202 else -- Not cavern, check if 4 nodes of space above
203 if air >= 4 then
204 return y + 2
205 else -- Not enough space, reset air to zero
206 air = 0
211 return start_y -- Fallback
214 local function move_check(p1, max, dir)
215 local p = {x = p1.x, y = p1.y, z = p1.z}
216 local d = math.sign(max - p1[dir])
217 local min = p[dir]
219 for k = min, max, d do
220 p[dir] = k
221 local node = minetest.get_node(p)
222 -- Check for obsidian (except at corners)
223 if k ~= min and k ~= max and node.name ~= "mcl_core:obsidian" then
224 return false
226 -- Abort if any of the portal frame blocks already has metadata.
227 -- This mod does not yet portals which neighbor each other directly.
228 -- TODO: Reorganize the way how portal frame coordinates are stored.
229 if node.name == "mcl_core:obsidian" then
230 local meta = minetest.get_meta(p)
231 local pframe1 = meta:get_string("portal_frame1")
232 if minetest.string_to_pos(pframe1) ~= nil then
233 return false
238 return true
241 local function check_portal(p1, p2)
242 if p1.x ~= p2.x then
243 if not move_check(p1, p2.x, "x") then
244 return false
246 if not move_check(p2, p1.x, "x") then
247 return false
249 elseif p1.z ~= p2.z then
250 if not move_check(p1, p2.z, "z") then
251 return false
253 if not move_check(p2, p1.z, "z") then
254 return false
256 else
257 return false
260 if not move_check(p1, p2.y, "y") then
261 return false
263 if not move_check(p2, p1.y, "y") then
264 return false
267 return true
270 local function is_portal(pos)
271 local xsize, ysize = FRAME_SIZE_X_MIN-1, FRAME_SIZE_Y_MIN-1
272 for d = -xsize, xsize do
273 for y = -ysize, ysize do
274 local px = {x = pos.x + d, y = pos.y + y, z = pos.z}
275 local pz = {x = pos.x, y = pos.y + y, z = pos.z + d}
277 if check_portal(px, {x = px.x + xsize, y = px.y + ysize, z = px.z}) then
278 return px, {x = px.x + xsize, y = px.y + ysize, z = px.z}
280 if check_portal(pz, {x = pz.x, y = pz.y + ysize, z = pz.z + xsize}) then
281 return pz, {x = pz.x, y = pz.y + ysize, z = pz.z + xsize}
287 -- Attempts to light a Nether portal at pos and
288 -- select target position.
289 -- Pos can be any of the obsidian frame blocks or the inner part.
290 -- The frame MUST be filled only with air or any fire, which will be replaced with Nether portal blocks.
291 -- If no Nether portal can be lit, nothing happens.
292 -- Returns true on success and false on failure.
293 function mcl_portals.light_nether_portal(pos)
294 -- Only allow to make portals in Overworld and Nether
295 local dim = mcl_worlds.pos_to_dimension(pos)
296 if dim ~= "overworld" and dim ~= "nether" then
297 return false
299 -- Create Nether portal nodes
300 local p1, p2 = is_portal(pos)
301 if not p1 or not p2 then
302 return false
305 for d = 1, 2 do
306 for y = p1.y + 1, p2.y - 1 do
307 local p
308 if p1.z == p2.z then
309 p = {x = p1.x + d, y = y, z = p1.z}
310 else
311 p = {x = p1.x, y = y, z = p1.z + d}
313 local nn = minetest.get_node(p).name
314 if nn ~= "air" and minetest.get_item_group(nn, "fire") ~= 1 then
315 return false
320 local param2
321 if p1.z == p2.z then
322 param2 = 0
323 else
324 param2 = 1
327 -- Find target
329 local target = {x = p1.x, y = p1.y, z = p1.z}
330 target.x = target.x + 1
331 if target.y < mcl_vars.mg_nether_max and target.y > mcl_vars.mg_nether_min then
332 if mg_name == "flat" then
333 target.y = mcl_vars.mg_bedrock_overworld_max + 5
334 else
335 target.y = math.random(mcl_vars.mg_overworld_min + 40, mcl_vars.mg_overworld_min + 96)
337 else
338 target.y = find_nether_target_y(target.x, target.z)
341 local dmin, dmax, ymin, ymax = 0, FRAME_SIZE_X_MIN - 1, p1.y, p2.y
342 for d = dmin, dmax do
343 for y = ymin, ymax do
344 if not ((d == dmin or d == dmax) and (y == ymin or y == ymax)) then
345 local p
346 if param2 == 0 then
347 p = {x = p1.x + d, y = y, z = p1.z}
348 else
349 p = {x = p1.x, y = y, z = p1.z + d}
351 if d ~= dmin and d ~= dmax and y ~= ymin and y ~= ymax then
352 minetest.set_node(p, {name = "mcl_portals:portal", param2 = param2})
354 local meta = minetest.get_meta(p)
356 -- Portal frame corners
357 meta:set_string("portal_frame1", minetest.pos_to_string(p1))
358 meta:set_string("portal_frame2", minetest.pos_to_string(p2))
360 -- Portal target coordinates
361 meta:set_string("portal_target", minetest.pos_to_string(target))
366 return true
370 minetest.register_abm({
371 label = "Nether portal teleportation and particles",
372 nodenames = {"mcl_portals:portal"},
373 interval = 1,
374 chance = 2,
375 action = function(pos, node)
376 minetest.add_particlespawner(
377 32, --amount
378 4, --time
379 {x = pos.x - 0.25, y = pos.y - 0.25, z = pos.z - 0.25}, --minpos
380 {x = pos.x + 0.25, y = pos.y + 0.25, z = pos.z + 0.25}, --maxpos
381 {x = -0.8, y = -0.8, z = -0.8}, --minvel
382 {x = 0.8, y = 0.8, z = 0.8}, --maxvel
383 {x = 0, y = 0, z = 0}, --minacc
384 {x = 0, y = 0, z = 0}, --maxacc
385 0.5, --minexptime
386 1, --maxexptime
387 1, --minsize
388 2, --maxsize
389 false, --collisiondetection
390 "mcl_particles_teleport.png" --texture
392 for _,obj in ipairs(minetest.get_objects_inside_radius(pos,1)) do --maikerumine added for objects to travel
393 local lua_entity = obj:get_luaentity() --maikerumine added for objects to travel
394 if obj:is_player() or lua_entity then
395 -- Prevent quick back-and-forth teleportation
396 if portal_cooloff[obj] then
397 return
399 local meta = minetest.get_meta(pos)
400 local target = minetest.string_to_pos(meta:get_string("portal_target"))
401 if target then
402 -- force emerge of target area
403 minetest.get_voxel_manip():read_from_map(target, target)
404 if not minetest.get_node_or_nil(target) then
405 minetest.emerge_area(vector.subtract(target, 4), vector.add(target, 4))
408 -- teleport function
409 local teleport = function(obj, pos, target)
410 if (not obj:get_luaentity()) and (not obj:is_player()) then
411 return
413 -- Prevent quick back-and-forth teleportation
414 if portal_cooloff[obj] then
415 return
417 local objpos = obj:getpos()
418 if objpos == nil then
419 return
421 -- If player stands, player is at ca. something+0.5
422 -- which might cause precision problems, so we used ceil.
423 objpos.y = math.ceil(objpos.y)
425 if minetest.get_node(objpos).name ~= "mcl_portals:portal" then
426 return
429 -- Teleport
430 obj:set_pos(target)
431 if obj:is_player() then
432 mcl_worlds.dimension_change(obj, mcl_worlds.pos_to_dimension(target))
433 minetest.sound_play("mcl_portals_teleport", {pos=target, gain=0.5, max_hear_distance = 16})
436 -- Enable teleportation cooloff for 4 seconds, to prevent back-and-forth teleportation
437 portal_cooloff[obj] = true
438 minetest.after(4, function(o)
439 portal_cooloff[o] = false
440 end, obj)
441 if obj:is_player() then
442 local name = obj:get_player_name()
443 minetest.log("action", "[mcl_portal] "..name.." teleported to Nether portal at "..minetest.pos_to_string(target)..".")
447 local n = minetest.get_node_or_nil(target)
448 if n and n.name ~= "mcl_portals:portal" then
449 -- Emerge target area, wait for emerging to be finished, build destination portal
450 -- (if there isn't already one, teleport object after a short delay.
451 local emerge_callback = function(blockpos, action, calls_remaining, param)
452 minetest.log("verbose", "[mcl_portal] emerge_callack called! action="..action)
453 if calls_remaining <= 0 then
454 minetest.log("verbose", "[mcl_portal] Area for destination Nether portal emerged!")
455 build_portal(param.target, param.pos, false)
456 minetest.after(NETHER_PORTAL_TELEPORT_DELAY, teleport, obj, pos, target)
459 minetest.log("verbose", "[mcl_portal] Emerging area for destination Nether portal ...")
460 minetest.emerge_area(vector.subtract(target, 7), vector.add(target, 7), emerge_callback, { pos = pos, target = target })
461 else
462 minetest.after(NETHER_PORTAL_TELEPORT_DELAY, teleport, obj, pos, target)
468 end,
472 --[[ ITEM OVERRIDES ]]
474 local longdesc = minetest.registered_nodes["mcl_core:obsidian"]._doc_items_longdesc
475 longdesc = longdesc .. "\n" .. "Obsidian is also used as the frame of Nether portals."
476 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."
478 minetest.override_item("mcl_core:obsidian", {
479 _doc_items_longdesc = longdesc,
480 _doc_items_usagehelp = usagehelp,
481 on_destruct = destroy_portal,
482 _on_ignite = function(user, pointed_thing)
483 local pos = pointed_thing.under
484 local portal_placed = mcl_portals.light_nether_portal(pos)
485 if portal_placed then
486 minetest.log("action", "[mcl_portal] Nether portal activated at "..minetest.pos_to_string(pos)..".")
488 if portal_placed and minetest.get_modpath("doc") then
489 doc.mark_entry_as_revealed(user:get_player_name(), "nodes", "mcl_portals:portal")
491 -- Achievement for finishing a Nether portal TO the Nether
492 local dim = mcl_worlds.pos_to_dimension(pos)
493 if minetest.get_modpath("awards") and dim ~= "nether" and user:is_player() then
494 awards.unlock(user:get_player_name(), "mcl:buildNetherPortal")
496 return true
497 else
498 return false
500 end,