diff --git a/README.md b/README.md index d72df3f..0c2c509 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ right-click/used on the frame to activate it. See portal_api.txt for how to create custom portals to your own realms. +Nether Portals can allow surface fast-travel. + This mod provides Nether basalts (natural, hewn, and chiseled) as nodes which require a player to journey to the magma ocean to obtain, so these can be used for gating progression through a game. For example, a portal to another realm @@ -24,8 +26,7 @@ the nether first, or basalt might be a crafting ingredient required to reach a particular branch of the tech-tree. Netherbrick tools are provided (pick, shovel, axe, & sword), see tools.lua - -Nether Portals can allow surface fast-travel. +The Nether pickaxe has a 10x bonus again wear when mining netherrack. ## License of source code: @@ -50,6 +51,7 @@ SOFTWARE. ### [Public Domain Dedication (CC0 1.0)](https://creativecommons.org/publicdomain/zero/1.0/) * `nether_portal_teleport.ogg` is a timing adjusted version of "teleport" by [outroelison](https://freesound.org/people/outroelison), used under CC0 1.0 + * `nether_rack_destroy.ogg` is from "Rock destroy" by [Bertsz](https://freesound.org/people/Bertsz/), used under CC0 1.0 ### [Attribution 3.0 Unported (CC BY 3.0)](https://creativecommons.org/licenses/by/3.0/) @@ -64,6 +66,7 @@ SOFTWARE. * `nether_fumarole.ogg`: Treer, 2020 * `nether_lava_bubble`* (files starting with "nether_lava_bubble"): Treer, 2020 * `nether_lava_crust_animated.png`: Treer, 2019-2020 + * `nether_lightstaff.png`: Treer, 2021 * `nether_particle_anim`* (files starting with "nether_particle_anim"): Treer, 2019 * `nether_portal_ignition_failure.ogg`: Treer, 2019 * `nether_smoke_puff.png`: Treer, 2020 diff --git a/nodes.lua b/nodes.lua index f82559f..a484278 100644 --- a/nodes.lua +++ b/nodes.lua @@ -65,6 +65,110 @@ nether.register_wormhole_node("nether:portal_alt", { }) +--== Transmogrification functions ==-- +-- Functions enabling selected nodes to be temporarily transformed into other nodes. +-- (so the light staff can temporarily turn netherrack into glowstone) + +-- Swaps the node at `nodePos` with `newNode`, unless `newNode` is nil in which +-- case the node is swapped back to its original type. +-- `monoSimpleSoundSpec` is optional. +-- returns true if a node was transmogrified +nether.magicallyTransmogrify_node = function(nodePos, playerName, newNode, monoSimpleSoundSpec, isPermanent) + + local meta = minetest.get_meta(nodePos) + local playerEyePos = nodePos -- fallback value in case the player no longer exists + local player = minetest.get_player_by_name(playerName) + if player ~= nil then + local playerPos = player:get_pos() + playerEyePos = vector.add(playerPos, {x = 0, y = 1.5, z = 0}) -- not always the cameraPos, e.g. 3rd person mode. + end + + local oldNode = minetest.get_node(nodePos) + if oldNode.name == "air" then + -- the node has been mined or otherwise destroyed, abort the operation + return false + end + local oldNodeDef = minetest.registered_nodes[oldNode.name] or minetest.registered_nodes["air"] + + local specialFXSize = 1 -- a specialFXSize of 1 is for full SFX, 0.5 is half-sized + local returningToNormal = newNode == nil + if returningToNormal then + -- This is the transmogrified node returning back to normal - a more subdued animation + specialFXSize = 0.5 + -- read what the node used to be from the metadata + newNode = { + name = meta:get_string("transmogrified_name"), + param1 = meta:get_string("transmogrified_param1"), + param2 = meta:get_string("transmogrified_param2") + } + if newNode.name == "" then + minetest.log("warning", "nether.magicallyTransmogrify_node() invoked to restore node which wasn't transmogrified") + return false + end + end + + local soundSpec = monoSimpleSoundSpec + if soundSpec == nil and oldNodeDef.sounds ~= nil then + soundSpec = oldNodeDef.sounds.dug or oldNodeDef.sounds.dig + if soundSpec == "__group" then soundSpec = "default_dig_cracky" end + end + if soundSpec ~= nil then + minetest.sound_play(soundSpec, {pos = nodePos, max_hear_distance = 50}) + end + + -- Start the particlespawner nearer the player's side of the node to create + -- more initial occlusion for an illusion of the old node breaking apart / falling away. + local dirToPlayer = vector.normalize(vector.subtract(playerEyePos, nodePos)) + local impactPos = vector.add(nodePos, vector.multiply(dirToPlayer, 0.5)) + local velocity = 1 + specialFXSize + minetest.add_particlespawner({ + amount = 50 * specialFXSize, + time = 0.1, + minpos = vector.add(impactPos, -0.3), + maxpos = vector.add(impactPos, 0.3), + minvel = {x = -velocity, y = -velocity, z = -velocity}, + maxvel = {x = velocity, y = 3 * velocity, z = velocity}, -- biased upward to counter gravity in the initial stages + minacc = {x=0, y=-10, z=0}, + maxacc = {x=0, y=-10, z=0}, + minexptime = 1.5 * specialFXSize, + maxexptime = 3 * specialFXSize, + minsize = 0.5, + maxsize = 5, + node = {name = oldNodeDef.name}, + glow = oldNodeDef.light_source + }) + + if returningToNormal or isPermanent then + -- clear the metadata that indicates the node is transformed + meta:set_string("transmogrified_name", "") + meta:set_int("transmogrified_param1", 0) + meta:set_int("transmogrified_param2", 0) + else + -- save the original node so it can be restored + meta:set_string("transmogrified_name", oldNode.name) + meta:set_int("transmogrified_param1", oldNode.param1) + meta:set_int("transmogrified_param2", oldNode.param2) + end + + minetest.swap_node(nodePos, newNode) + return true +end + + +local function transmogrified_can_dig (pos, player) + if minetest.get_meta(pos):get_string("transmogrified_name") ~= "" then + -- This node was temporarily transformed into its current form + -- revert it back, rather than allow the player to mine transmogrified nodes. + local playerName = "" + if player ~= nil then playerName = player:get_player_name() end + nether.magicallyTransmogrify_node(pos, playerName) + return false + end + return true +end + + + -- Nether nodes minetest.register_node("nether:rack", { @@ -105,6 +209,7 @@ minetest.register_node("nether:glowstone", { paramtype = "light", groups = {cracky = 3, oddly_breakable_by_hand = 3}, sounds = default.node_sound_glass_defaults(), + can_dig = transmogrified_can_dig, -- to ensure glowstone temporarily created by the lightstaff can't be kept }) -- Deep glowstone, found in the mantle / central magma layers @@ -116,6 +221,7 @@ minetest.register_node("nether:glowstone_deep", { paramtype = "light", groups = {cracky = 3, oddly_breakable_by_hand = 3}, sounds = default.node_sound_glass_defaults(), + can_dig = transmogrified_can_dig, -- to ensure glowstone temporarily created by the lightstaff can't be kept }) minetest.register_node("nether:brick", { diff --git a/sounds/nether_rack_destroy.ogg b/sounds/nether_rack_destroy.ogg new file mode 100644 index 0000000..04fa2ed Binary files /dev/null and b/sounds/nether_rack_destroy.ogg differ diff --git a/textures/nether_lightstaff.png b/textures/nether_lightstaff.png new file mode 100644 index 0000000..915a706 Binary files /dev/null and b/textures/nether_lightstaff.png differ diff --git a/tools.lua b/tools.lua index b821f07..5679e75 100644 --- a/tools.lua +++ b/tools.lua @@ -151,3 +151,203 @@ minetest.register_craft({ {"group:stick"} } }) + + + + +--===========================-- +--== Nether Staff of Light ==-- +--===========================-- + +nether.lightstaff_recipes = { + ["nether:rack"] = "nether:glowstone", + ["nether:brick"] = "nether:glowstone", + ["nether:brick_cracked"] = "nether:glowstone", + ["nether:brick_compressed"] = "nether:glowstone", + ["stairs:slab_netherrack"] = "nether:glowstone", + ["nether:rack_deep"] = "nether:glowstone_deep", + ["nether:brick_deep"] = "nether:glowstone_deep", + ["stairs:slab_netherrack_deep"] = "nether:glowstone_deep" +} +nether.lightstaff_range = 100 +nether.lightstaff_velocity = 60 +nether.lightstaff_gravity = 0 -- using 0 instead of 10 because the particle physics doesn't seem accurate enough for curved trajectories +nether.lightstaff_uses = 60 -- number of times the Eternal Lightstaff can be used before wearing out +nether.lightstaff_duration = 40 -- lifespan of glowstone created by the termporay Lightstaff +local serverLag = 0.05 -- rough amount reduce particle effect synchronization with lightstaff node changes + +-- returns a pointed_thing, or nil if no solid node intersected the ray +local function raycastForSolidNode(rayStartPos, rayEndPos) + + local raycast = minetest.raycast( + rayStartPos, + rayEndPos, + false, -- objects - if false, only nodes will be returned. Default is `true` + true -- liquids - if false, liquid nodes won't be returned. Default is `false` + ) + local next_pointed = raycast:next() + while next_pointed do + local under_node = minetest.get_node(next_pointed.under) + local under_def = minetest.registered_nodes[under_node.name] + + if (under_def and not under_def.buildable_to) or not under_def then + return next_pointed + end + + next_pointed = raycast:next(next_pointed) + end + return nil +end + +-- Turns a node into a light source +-- `lightDuration` 0 is considered permanent, lightDuration is in seconds +-- returns true if a node is transmogrified into a glowstone +local function light_node(pos, playerName, lightDuration) + + local result = false + if minetest.is_protected(pos, playerName) then + minetest.record_protection_violation(pos, playerName) + return false + end + + local oldNode = minetest.get_node(pos) + local litNodeName = nether.lightstaff_recipes[oldNode.name] + + if litNodeName ~= nil then + result = nether.magicallyTransmogrify_node( + pos, + playerName, + {name=litNodeName}, + {name = "nether_rack_destroy", gain = 0.8}, + lightDuration == 0 -- isPermanent + ) + + if lightDuration > 0 then + minetest.after(lightDuration, + function() + -- Restore the node to its original type. + -- + -- If the server crashes or shuts down before this is invoked, the node + -- will remain in its transmogrified state. These could be cleaned up + -- with an LBM, but I don't think that's necessary: if this functionality + -- is only being used for the Nether Lightstaff then I don't think it + -- matters if there's occasionally an extra glowstone left in the + -- netherrack. + nether.magicallyTransmogrify_node(pos, playerName) + end + ) + end + end + return result +end + +-- a lightDuration of 0 is considered permanent, lightDuration is in seconds +-- returns true if a node is transmogrified into a glowstone +local function lightstaff_on_use(user, boltColorString, lightDuration) + + if not user then return false end + local playerName = user:get_player_name() + local playerlookDir = user:get_look_dir() + local playerPos = user:get_pos() + local playerEyePos = vector.add(playerPos, {x = 0, y = 1.5, z = 0}) -- not always the cameraPos, e.g. 3rd person mode. + local target = vector.add(playerEyePos, vector.multiply(playerlookDir, nether.lightstaff_range)) + + local targetHitPos = nil + local targetNodePos = nil + local target_pointed = raycastForSolidNode(playerEyePos, target) + if target_pointed then + targetNodePos = target_pointed.under + targetHitPos = vector.divide(vector.add(target_pointed.under, target_pointed.above), 2) + end + + local wieldOffset = {x= 0.5, y = -0.2, z= 0.8} + local lookRotation = ({x = -user:get_look_vertical(), y = user:get_look_horizontal(), z = 0}) + local wieldPos = vector.add(playerEyePos, vector.rotate(wieldOffset, lookRotation)) + local aimPos = targetHitPos or target + local distance = math.abs(vector.length(vector.subtract(aimPos, wieldPos))) + aimPos.y = aimPos.y + (distance / nether.lightstaff_velocity) * nether.lightstaff_gravity + local boltDir = vector.normalize(vector.subtract(aimPos, wieldPos)) + + -- needs a proper casting sound, instead of the portal ignition + minetest.sound_play("nether_portal_ignite", {to_player = playerName, gain = .3, pitch = 1.7}, true) + + -- animate a "magic bolt" from wieldPos to aimPos + local particleSpawnDef = { + amount = 20, + time = 0.4, + minpos = vector.add(wieldPos, -0.13), + maxpos = vector.add(wieldPos, 0.13), + minvel = vector.multiply(boltDir, nether.lightstaff_velocity - 0.3), + maxvel = vector.multiply(boltDir, nether.lightstaff_velocity + 0.3), + minacc = {x=0, y=-nether.lightstaff_gravity, z=0}, + maxacc = {x=0, y=-nether.lightstaff_gravity, z=0}, + minexptime = 1, + maxexptime = 2, + minsize = 4, + maxsize = 5, + collisiondetection = true, + collision_removal = true, + texture = "nether_particle_anim3.png", + animation = { type = "vertical_frames", aspect_w = 7, aspect_h = 7, length = 0.8 }, + glow = 15 + } + minetest.add_particlespawner(particleSpawnDef) + particleSpawnDef.texture = "nether_particle_anim3.png^[colorize:" .. boltColorString .. ":alpha" + particleSpawnDef.amount = 12 + particleSpawnDef.time = 0.2 + particleSpawnDef.minsize = 6 + particleSpawnDef.maxsize = 7 + particleSpawnDef.minpos = vector.add(wieldPos, -0.35) + particleSpawnDef.maxpos = vector.add(wieldPos, 0.35) + minetest.add_particlespawner(particleSpawnDef) + + local result = false + if targetNodePos then + -- delay the impact until roughly when the particle effects will have reached the target + minetest.after( + math.max(0, (distance / nether.lightstaff_velocity) - serverLag), + function() + light_node(targetNodePos, playerName, lightDuration) + end + ) + + if lightDuration ~= 0 then + -- we don't need to care whether the transmogrify will be successful + result = true + else + -- check whether the transmogrify will be successful + local targetNode = minetest.get_node(targetNodePos) + result = nether.lightstaff_recipes[targetNode.name] ~= nil + end + end + return result +end + +-- Inspired by FaceDeer's torch crossbow and Xanthin's Staff of Light +minetest.register_tool("nether:lightstaff", { + description = S("Nether staff of Light\nTemporarily transforms the netherrack into glowstone"), + inventory_image = "nether_lightstaff.png", + wield_image = "nether_lightstaff.png", + light_source = 11, -- used by wielded_light mod etc. + stack_max = 1, + on_use = function(itemstack, user, pointed_thing) + lightstaff_on_use(user, "#F70", nether.lightstaff_duration) + end +}) + +minetest.register_tool("nether:lightstaff_eternal", { + description = S("Nether staff of Eternal Light\nCreates glowstone from netherrack"), + inventory_image = "nether_lightstaff.png^[colorize:#55F:90", + wield_image = "nether_lightstaff.png^[colorize:#55F:90", + light_source = 11, -- used by wielded_light mod etc. + sound = {breaks = "default_tool_breaks"}, + stack_max = 1, + on_use = function(itemstack, user, pointed_thing) + if lightstaff_on_use(user, "#23F", 0) then -- was "#8088FF" or "#13F" + -- The staff of Eternal Light wears out, to limit how much + -- a player can alter the nether with it. + itemstack:add_wear(65535 / (nether.lightstaff_uses - 1)) + end + return itemstack + end +})