From e2666146ca085993c88808c899ee1b98406ecb49 Mon Sep 17 00:00:00 2001 From: Treer Date: Sun, 21 Jul 2019 13:06:04 +1000 Subject: [PATCH] Implement sound and events in API --- init.lua | 8 ++--- portal_api.lua | 89 +++++++++++++++++++++++++++++++++++++++++++------- portal_api.txt | 47 ++++++++++++++++---------- 3 files changed, 109 insertions(+), 35 deletions(-) diff --git a/init.lua b/init.lua index 1f48126..cb388aa 100644 --- a/init.lua +++ b/init.lua @@ -47,8 +47,8 @@ nether.register_portal_ignition_item("default:mese_crystal_fragment") nether.register_portal("nether_portal", { shape = nether.PortalShape_Traditional, frame_node_name = "default:obsidian", - wormhole_node_name = "nether:portal", wormhole_node_color = 0, -- 0 is magenta + -- Warning: "Four per Em" spaces have been used to align the diagram in this text, rather -- than ASCII spaces. If Minetest changes font this may need to be updated. book_of_portals_pagetext = S([[ ──══♦♦♦◊ The Nether ◊♦♦♦══── @@ -66,10 +66,6 @@ This opens to a truly hellish place, though for small mercies the air there is s The expedition parties have found no diamonds or gold, and after an experienced search party failed to return from the trail of a missing expedition party, I must conclude this is a dangerous place. ]], 10 * nether.FASTTRAVEL_FACTOR), - sound_ambient = "nether_portal_hum", - sound_ignite = "", - sound_extinguish = "", - sound_teleport = "", within_realm = function(pos) -- return true if pos is inside the Nether return pos.y < nether.DEPTH @@ -113,4 +109,4 @@ The expedition parties have found no diamonds or gold, and after an experienced return destination_pos end end -}) +}) \ No newline at end of file diff --git a/portal_api.lua b/portal_api.lua index 47956ff..08d7a27 100644 --- a/portal_api.lua +++ b/portal_api.lua @@ -528,6 +528,10 @@ local function build_portal(portal_definition, anchorPos, orientation, destinati if DEBUG then minetest.chat_send_all("Placed portal schematic at " .. minetest.pos_to_string(portal_definition.shape.get_schematicPos_from_anchorPos(anchorPos, orientation)) .. ", orientation " .. orientation) end set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos) + + if portal_definition.on_created ~= nil then + portal_definition.on_created(portal_definition, anchorPos, orientation) + end end @@ -770,11 +774,48 @@ local function ignite_portal(ignition_pos, ignition_node_name) -- ignition/BURN_BABY_BURN set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos) + if portal_definition.sounds.ignite ~= nil then + local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) + minetest.sound_play(portal_definition.sounds.ignite, {pos = local_wormholePos, max_hear_distance = 20}) + end + + if portal_definition.on_ignite ~= nil then + portal_definition.on_ignite(portal_definition, anchorPos, orientation) + end + return true end end end +-- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run +-- see also ambient_sound_stop() +function ambient_sound_play(portal_definition, soundPos, timerNodeMeta) + if portal_definition.sounds.ambient ~= nil then + local soundLength = portal_definition.sounds.ambient.length + if soundLength == nil then soundLength = 3 end + local lastPlayed = timerNodeMeta:get_int("ambient_sound_last_played") + + -- Using "os.time() % soundLength == 0" is lightweight but means delayed starts, so trying a stored lastPlayed + if os.time() >= lastPlayed + soundLength then + local soundHandle = minetest.sound_play(portal_definition.sounds.ambient, {pos = soundPos, max_hear_distance = 6}) + if timerNodeMeta ~= nil then + timerNodeMeta:set_int("ambient_sound_handle", soundHandle) + timerNodeMeta:set_int("ambient_sound_last_played", os.time()) + end + end + end +end + +-- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run +-- see also ambient_sound_play() +function ambient_sound_stop(timerNodeMeta) + if timerNodeMeta ~= nil then + local soundHandle = timerNodeMeta:get_int("ambient_sound_handle") + minetest.sound_stop(soundHandle) + end +end + -- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos extinguish_portal = function(pos, node_name, frame_was_destroyed) -- assigned rather than declared because extinguish_portal is already declared, for use by earlier functions in the file. @@ -796,6 +837,15 @@ extinguish_portal = function(pos, node_name, frame_was_destroyed) -- assigned ra minetest.log("error", "extinguish_portal() invoked on " .. node_name .. " but no registered portal is constructed from " .. node_name) return -- no portal frames are made from this type of node end + + if portal_definition.sounds.extinguish ~= nil then + minetest.sound_play(portal_definition.sounds.extinguish, {pos = p1}) + end + + -- stop timer and ambient sound + local timerPos = get_timerPos_from_p1_and_p2(p1, p2) + minetest.get_node_timer(timerPos):stop() + ambient_sound_stop(minetest.get_meta(timerPos)) -- update the ignition state in the portal location info local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2) @@ -808,7 +858,6 @@ extinguish_portal = function(pos, node_name, frame_was_destroyed) -- assigned ra local frame_node_name = portal_definition.frame_node_name local wormhole_node_name = portal_definition.wormhole_node_name - minetest.get_node_timer(get_timerPos_from_p1_and_p2(p1, p2)):stop() for x = p1.x, p2.x do for y = p1.y, p2.y do @@ -891,12 +940,22 @@ local function ensure_remote_portal_then_teleport(player, portal_definition, loc if DEBUG then minetest.chat_send_all("Teleporting player from wormholePos" .. minetest.pos_to_string(local_wormholePos) .. " to wormholePos" .. minetest.pos_to_string(destination_wormholePos)) end + -- play the teleport sound + if portal_definition.sounds.teleport ~= nil then + minetest.sound_play(portal_definition.sounds.teleport, {to_player = player.name}) + end + -- rotate the player if the destination portal is a different orientation local rotation_angle = math.rad(destination_orientation - local_orientation) local offset = vector.subtract(playerPos, local_wormholePos) -- preserve player's position in the portal local rotated_offset = {x = math.cos(rotation_angle) * offset.x - math.sin(rotation_angle) * offset.z, y = offset.y, z = math.sin(rotation_angle) * offset.x + math.cos(rotation_angle) * offset.z} - player:setpos(vector.add(destination_wormholePos, rotated_offset)) + local new_playerPos = vector.add(destination_wormholePos, rotated_offset) + player:setpos(new_playerPos) player:set_look_horizontal(player:get_look_horizontal() + rotation_angle) + + if portal_definition.on_player_teleported ~= nil then + portal_definition.on_player_teleported(portal_definition, player, playerPos, new_playerPos) + end else -- no wormhole node at destination - destination portal either needs to be built or ignited -- A very rare edge-case that takes time to set up: @@ -930,8 +989,8 @@ end -- run_wormhole() is invoked once per second per portal, handling teleportation and particle effects. --- See get_timerPos_from_p1_and_p2() for an explanation of where pos will be -function run_wormhole(pos, time_elapsed) +-- See get_timerPos_from_p1_and_p2() for an explanation of the timerPos location +function run_wormhole(timerPos, time_elapsed) local portal_definition -- will be used inside run_wormhole_node_func() @@ -989,7 +1048,7 @@ function run_wormhole(pos, time_elapsed) end local p1, p2, frame_node_name - local meta = minetest.get_meta(pos) + local meta = minetest.get_meta(timerPos) if meta ~= nil then p1 = minetest.string_to_pos(meta:get_string("p1")) p2 = minetest.string_to_pos(meta:get_string("p2")) @@ -997,10 +1056,10 @@ function run_wormhole(pos, time_elapsed) end if p1 ~= nil and p2 ~= nil then -- look up the portal shape by what it's built from, so we know where the wormhole nodes will be located - if frame_node_name == nil then frame_node_name = minetest.get_node(pos).name end -- pos should be a frame node + if frame_node_name == nil then frame_node_name = minetest.get_node(timerPos).name end -- timerPos should be a frame node if the shape is traditionalPortalShape portal_definition = get_portal_definition(frame_node_name, p1, p2) if portal_definition == nil then - minetest.log("error", "No portal with a \"" .. frame_node_name .. "\" frame is registered. run_wormhole" .. minetest.pos_to_string(pos) .. " was invoked but that location contains \"" .. frame_node_name .. "\"") + minetest.log("error", "No portal with a \"" .. frame_node_name .. "\" frame is registered. run_wormhole" .. minetest.pos_to_string(timerPos) .. " was invoked but that location contains \"" .. frame_node_name .. "\"") else local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2) portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, run_wormhole_node_func) @@ -1008,6 +1067,9 @@ function run_wormhole(pos, time_elapsed) if portal_definition.on_run_wormhole ~= nil then portal_definition.on_run_wormhole(portal_definition, anchorPos, orientation) end + + local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) + ambient_sound_play(portal_definition, wormholePos, meta) end end end @@ -1250,10 +1312,12 @@ local portaldef_default = { wormhole_node_color = 0, frame_node_name = "default:obsidian", particle_texture = "nether_particle.png", - sound_ambient = "nether_portal_hum", - sound_ignite = "", - sound_extinguish = "", - sound_teleport = "", + sounds = { + ambient = {name = "nether_portal_ambient", gain = 0.6, length = 3}, + ignite = {name = "nether_portal_ignite", gain = 0.3}, + extinguish = {name = "nether_portal_extinguish", gain = 0.3}, + teleport = {name = "nether_portal_teleport", gain = 0.3} + } } @@ -1268,7 +1332,8 @@ function nether.register_portal(name, portaldef) portaldef.name = name - -- use portaldef_default for any values missing from portaldef + -- use portaldef_default for any values missing from portaldef or portaldef.sounds + if portaldef.sounds ~= nil then setmetatable(portaldef.sounds, {__index = portaldef_default.sounds}) end setmetatable(portaldef, {__index = portaldef_default}) if portaldef.particle_color == nil then diff --git a/portal_api.txt b/portal_api.txt index 06a8164..fd2c557 100644 --- a/portal_api.txt +++ b/portal_api.txt @@ -4,6 +4,11 @@ Portal API Reference The portal system used to get to the Nether can be used to create portals to other realms. +Portal code is more efficient when each type of portal uses a different type +of node to build its frame out of, however it is possible to register more than +one kind of portal with the same frame material — such as obsidian — provided +the size of the PortalShape is distinct from any other type of portal that is +using the same node as its frame. Helper functions @@ -16,8 +21,8 @@ Helper functions * `nether.find_surface_target_y(target_x, target_z, portal_name)`: returns a suitable anchorPos - * Can be used to implement custom find_surface_anchorPos() functions - * portal_name is optional, and providing it allows existing portals on the + * Can be used when implementing custom find_surface_anchorPos() functions + * portal_name is optional, providing it allows existing portals on the surface to be reused. * `nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)`: returns @@ -33,9 +38,9 @@ Helper functions API functions ------------- -Call these functions only at load time! +Call these functions only at load time: -* `nether.register_portal(name, portal definition)` +* `nether.register_portal(name, portal_definition)` * Returns true on success. Can return false if the portal definition clashes with a portal already registered by another mod, e.g. if the size and frame node is not unique. @@ -73,10 +78,15 @@ Used by `nether.register_portal`. -- Required. For best results, have your portal constructed of a -- material nobody else is using. - sound_ambient = "nether_portal_hum", - sound_ignite = "", - sound_extinguish = "", - sound_teleport = "", + sounds = { + ambient = , + -- if the ambient SimpleSoundSpec is a table it can also contain a + -- "length" int, which is the number of seconds to wait before + -- repeating the ambient sound. Default is 3. + ignite = , + extinguish = , + teleport = , + } within_realm = function(pos), -- Required. Return true if a portal at pos is in the realm, rather @@ -90,19 +100,22 @@ Used by `nether.register_portal`. -- portal will be used. find_surface_anchorPos = function(realm_anchorPos), - -- Optional. If you don't use this then a position near the surface - -- will be picked. - -- Return an anchorPos or anchorPos, orientation + -- Optional. If you don't implement this then a position near the + -- surface will be picked. + -- Return an anchorPos or (anchorPos, orientation) -- If orientation is not specified then the orientation of the realm -- portal will be used. - on_run_wormhole = function(portalDef, anochorPos, orientation) + on_run_wormhole = function(portalDef, anochorPos, orientation), -- invoked once per second per portal - on_extinguish = function(portalDef, anochorPos, orientation) + on_extinguish = function(portalDef, anochorPos, orientation), -- invoked when a portal is extinguished, including when the portal -- it connected to was extinguished. - - todo: on_ignite = function(portalDef, anochorPos, orientation) - todo: on_player_teleported = function(portalDef, oldPos, newPos) - todo: on_created = function(portalDef, anochorPos, orientation) + on_player_teleported = function(portalDef, player, oldPos, newPos), + -- invoked immediately after a player is teleported + on_ignite = function(portalDef, anochorPos, orientation) + -- invoked when a player or mesecon ignites a portal + on_created = function(portalDef, anochorPos, orientation) + -- invoked when a portal's remote twin is created, usually when a + -- player travels through a portal for the first time. }