diff --git a/README.md b/README.md index d72df3f..f938389 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,9 +51,11 @@ 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/) + * `nether_lightstaff.ogg` is "Fire Burst" by [SilverIllusionist](https://freesound.org/people/SilverIllusionist/), 2019 * `nether_portal_ambient.ogg` & `nether_portal_ambient.0.ogg` are extractions from "Deep Cinematic Rumble Stereo" by [Patrick Lieberkind](http://www.lieberkindvisuals.dk), used under CC BY 3.0 * `nether_portal_extinguish.ogg` is an extraction from "Tight Laser Weapon Hit Scifi" by [damjancd](https://freesound.org/people/damjancd), used under CC BY 3.0 * `nether_portal_ignite.ogg` is a derivative of "Flame Ignition" by [hykenfreak](https://freesound.org/people/hykenfreak), used under CC BY 3.0. "Nether Portal ignite" is licensed under CC BY 3.0 by Treer. @@ -64,6 +67,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/mod.conf b/mod.conf index 6a7e193..76467cb 100644 --- a/mod.conf +++ b/mod.conf @@ -1,4 +1,4 @@ name = nether description = Adds a deep underground realm with different mapgen that you can reach with obsidian portals. depends = stairs, default -optional_depends = moreblocks, mesecons, loot, dungeon_loot, doc_basics, fire, climate_api, ethereal +optional_depends = moreblocks, mesecons, loot, dungeon_loot, doc_basics, fire, climate_api, ethereal, walls diff --git a/nodes.lua b/nodes.lua index e07cce5..a484278 100644 --- a/nodes.lua +++ b/nodes.lua @@ -65,13 +65,118 @@ 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", { description = S("Netherrack"), tiles = {"nether_rack.png"}, is_ground_content = true, - groups = {cracky = 3, level = 2}, + -- setting workable_with_nether_tools reduces the wear on nether:pick_nether when mining this node + groups = {cracky = 3, level = 2, workable_with_nether_tools = 3}, sounds = default.node_sound_stone_defaults(), }) @@ -81,7 +186,8 @@ minetest.register_node("nether:rack_deep", { _doc_items_longdesc = S("Netherrack from deep in the mantle"), tiles = {"nether_rack_deep.png"}, is_ground_content = true, - groups = {cracky = 3, level = 2}, + -- setting workable_with_nether_tools reduces the wear on nether:pick_nether when mining this node + groups = {cracky = 3, level = 2, workable_with_nether_tools = 3}, sounds = default.node_sound_stone_defaults(), }) @@ -103,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 @@ -114,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", { @@ -175,37 +283,89 @@ minetest.register_node("nether:brick_deep", { -- Register stair and slab -stairs.register_stair_and_slab( - "nether_brick", - "nether:brick", - {cracky = 2, level = 2}, - {"nether_brick.png"}, - S("Nether Stair"), - S("Nether Slab"), - default.node_sound_stone_defaults(), - nil, - S("Inner Nether Stair"), - S("Outer Nether Stair") +-- Nether bricks can be made into stairs, slabs, inner stairs, and outer stairs + +stairs.register_stair_and_slab( -- this function also registers inner and outer stairs + "nether_brick", -- subname + "nether:brick", -- recipeitem + {cracky = 2, level = 2}, -- groups + {"nether_brick.png"}, -- images + S("Nether Stair"), -- desc_stair + S("Nether Slab"), -- desc_slab + minetest.registered_nodes["nether:brick"].sounds, -- sounds + false, -- worldaligntex + S("Inner Nether Stair"), -- desc_stair_inner + S("Outer Nether Stair") -- desc_stair_outer ) +stairs.register_stair_and_slab( -- this function also registers inner and outer stairs + "nether_brick_deep", -- subname + "nether:brick_deep", -- recipeitem + {cracky = 2, level = 2}, -- groups + {"nether_brick_deep.png"}, -- images + S("Deep Nether Stair"), -- desc_stair + S("Deep Nether Slab"), -- desc_slab + minetest.registered_nodes["nether:brick_deep"].sounds, -- sounds + false, -- worldaligntex + S("Inner Deep Nether Stair"), -- desc_stair_inner + S("Outer Deep Nether Stair") -- desc_stair_outer +) + +-- Netherrack can be shaped into stairs, slabs and walls + stairs.register_stair( "netherrack", "nether:rack", {cracky = 2, level = 2}, {"nether_rack.png"}, S("Netherrack stair"), - default.node_sound_stone_defaults() + minetest.registered_nodes["nether:rack"].sounds ) +stairs.register_slab( -- register a slab without adding inner and outer stairs + "netherrack", + "nether:rack", + {cracky = 2, level = 2}, + {"nether_rack.png"}, + S("Deep Netherrack slab"), + minetest.registered_nodes["nether:rack"].sounds +) + +stairs.register_stair( + "netherrack_deep", + "nether:rack_deep", + {cracky = 2, level = 2}, + {"nether_rack_deep.png"}, + S("Deep Netherrack stair"), + minetest.registered_nodes["nether:rack_deep"].sounds +) +stairs.register_slab( -- register a slab without adding inner and outer stairs + "netherrack_deep", + "nether:rack_deep", + {cracky = 2, level = 2}, + {"nether_rack_deep.png"}, + S("Deep Netherrack slab"), + minetest.registered_nodes["nether:rack_deep"].sounds +) + +-- Connecting walls +if minetest.get_modpath("walls") and minetest.global_exists("walls") and walls.register ~= nil then + walls.register("nether:rack_wall", "A Netherrack wall", "nether_rack.png", "nether:rack", minetest.registered_nodes["nether:rack"].sounds) + walls.register("nether:rack_deep_wall", "A Deep Netherrack wall", "nether_rack_deep.png", "nether:rack_deep", minetest.registered_nodes["nether:rack_deep"].sounds) +end -- StairsPlus if minetest.get_modpath("moreblocks") then + -- Registers about 49 different shapes of nether brick, replacing the stairs & slabs registered above. + -- (This could also be done for deep nether brick, but I've left that out to avoid a precedent of 49 new + -- nodes every time the nether gets a new material. Nether structures won't be able to use them because + -- they can't depend on moreblocks) stairsplus:register_all( "nether", "brick", "nether:brick", { description = S("Nether Brick"), groups = {cracky = 2, level = 2}, tiles = {"nether_brick.png"}, - sounds = default.node_sound_stone_defaults(), + sounds = minetest.registered_nodes["nether:brick"].sounds, }) end diff --git a/sounds/nether_lightstaff.ogg b/sounds/nether_lightstaff.ogg new file mode 100644 index 0000000..c1260e3 Binary files /dev/null and b/sounds/nether_lightstaff.ogg differ 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 26e1425..1591e47 100644 --- a/tools.lua +++ b/tools.lua @@ -20,18 +20,33 @@ local S = nether.get_translator minetest.register_tool("nether:pick_nether", { - description = S("Nether Pickaxe"), + description = S("Nether Pickaxe\nWell suited for mining netherrack"), + _doc_items_longdesc = S("Uniquely suited for mining netherrack, with minimal wear when doing so. Blunts quickly on other materials."), inventory_image = "nether_tool_netherpick.png", tool_capabilities = { full_punch_interval = 0.8, max_drop_level=3, groupcaps={ - cracky = {times={[1]=1.90, [2]=0.9, [3]=0.4}, uses=35, maxlevel=3}, + cracky = {times={[1]=1.90, [2]=0.9, [3]=0.3}, uses=35, maxlevel=2}, }, damage_groups = {fleshy=4}, }, sound = {breaks = "default_tool_breaks"}, - groups = {pickaxe = 1} + groups = {pickaxe = 1}, + + after_use = function(itemstack, user, node, digparams) + local wearDivisor = 1 + local nodeDef = minetest.registered_nodes[node.name] + if nodeDef ~= nil and nodeDef.groups ~= nil then + -- The nether pick hardly wears out when mining netherrack + local workable = nodeDef.groups.workable_with_nether_tools or 0 + wearDivisor = 1 + (3 * workable) -- 10 for netherrack, 1 otherwise. Making it able to mine 350 netherrack nodes, instead of 35. + end + + local wear = math.floor(digparams.wear / wearDivisor) + itemstack:add_wear(wear) -- apply the adjusted wear as usual + return itemstack + end }) minetest.register_tool("nether:shovel_nether", { @@ -136,3 +151,212 @@ 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 projectile arcs look less magical - magic isn't affected by gravity ;) (but set this to 10 if you're making a crossbow etc.) +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 + +-- 'serverLag' is a rough amount to reduce the projected impact-time the server must wait before initiating the +-- impact events (i.e. node changing to glowstone with explosion particle effect). +-- In tests using https://github.com/jagt/clumsy to simulate network lag I've found this value to not noticeably +-- matter. A large network lag is noticeable in the time between clicking fire and when the shooting-particleEffect +-- begins, as well as the time between when the impact sound/particleEffect start and when the netherrack turns +-- into glowstone. The synchronization that 'serverLag' adjusts seems to already tolerate network lag well enough (at +-- least when lag is consistent, as I have not simulated random lag) +local serverLag = 0.05 -- in seconds. Larger values makes impact events more premature/early. + +-- 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))) + local flightTime = distance / nether.lightstaff_velocity + local dropDistance = nether.lightstaff_gravity * 0.5 * (flightTime * flightTime) + aimPos.y = aimPos.y + dropDistance + local boltDir = vector.normalize(vector.subtract(aimPos, wieldPos)) + + minetest.sound_play("nether_lightstaff", {to_player = playerName, gain = 0.8}, 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 +})