--[[ Blink for Minetest Code is licensed under the GNU LGPL version 3 or later. See LICENSE.txt and http://www.gnu.org/licenses/lgpl-3.0.txt To Do: Localize Maybe switch to first weapon in hotbar when blinking behind players or mobs Maybe put messages in HUD instead of in chat Add a check for destination when raycast doesn't intersect It may be possible to glitch through a single layer of nodes if player uses blink at maximum distance since we move the Y - 0.5 ]] blink = {} local S = core.get_translator("blink") -- SETTINGS -- -- Maximum distance to teleport local blink_distance = core.settings:get("blink:blink_distance") or 20 -- Allow teleport into a protected area local tp_into_prot = core.settings:get_bool("blink:tp_into_prot") or false -- Allow teleport out of a protected area local tp_from_prot = core.settings:get_bool("blink:tp_from_prot") or false -- Cooldown period before next blink local cooldown_base = core.settings:get("blink:cooldown_base") or 0 -- Multiply this by distance travelled and add to base local cooldown_factor = core.settings:get("blink:cooldown_factor") or 0.1 -- Blink Behind players or mobs and face their back local blink_behind = core.settings:get("blink:blink_behind") or true -- Time to show destination marker local display_time = core.settings:get("blink:display_time") or 6.0 -- Public areas username. Any areas owned by this user are considered public and will allow users to blink local public_username = core.settings:get("blink:public_username") or "" -- If placing a second marker in the same place as an existing one, tp there local double_tap_tp = core.settings:get("blink:double_tap_tp") or true -- Number of uses for Blink Rune local num_uses = core.settings:get("blink:num_uses") or 150 -- You can easily add to this list externally -- Simply add blink as an optional dependency and do blink.valid_entities["modname"] = true blink.valid_entities = { ["mobs_animal"] = true, ["mobs_monster"] = true, ["mobs_npc"] = true, ["mob_horse"] = true, ["mobs_sharks"] = true, ["mobs_crocs"] = true, ["mobs_fish"] = true, ["mobs_jellyfish"] = true, ["mobs_turtles"] = true, ["nssm"] = true, ["creeper"] = true, ["dmobs"] = true } -- This is number of times to loop through the colors in blink_spectrum.png -- display time of each color is display_time / (number of colors * color_loops) local color_loops = 2 -- If display_time is short this the colors will cycle quickly. If it is long then they will cycle very slowly local tool_wear = math.ceil(65536 / num_uses) -- GLOBALS -- blink.active_marker = {} -- Table of users marker objects blink.cooldown = {} -- Collection of users still in cooldown -- marker should be a registered entity function display_marker(user, itemstack, marker) if marker == "" or marker == nil then marker = "blink:marker" end blink_tp(user, itemstack, marker) end -- This function is available to other mods -- Marker should be nil if you want to tp the player or a string name of a registered entity to display -- Currently blink:marker2 is hardcoded but this could be easily changed if someone wanted to have that functionality in the future -- if itemstack is not nil then add wear and return that itemstack function blink_tp(user, itemstack, marker) local username = user:get_player_name() if not marker then if blink.cooldown[username] then core.chat_send_player(username, S("You must wait before using Blink again")) return end end local markername = marker or "blink:marker" -- change this to 2 if we can't tp to this destination local origin = user:get_pos() local yaw -- use these if we move behind a player or mob local reset_pitch = false if not tp_from_prot and core.is_protected(origin, username) and core.is_protected(origin, public_username) then core.chat_send_player(username, S("Cannot blink from protected areas!")) return end -- Move origin up to eye_height origin.y = origin.y + user:get_properties().eye_height local dir = user:get_look_dir() local dpos = table.copy(origin) -- It's possible to use vector.add but this is a fairly new feature and waasn't compatible with Multicraft servers at the time of development -- The builtin function is written in LUA so there is no performance loss by doing it here manually dpos.x = dpos.x + (dir.x * blink_distance) dpos.y = dpos.y + (dir.y * blink_distance) dpos.z = dpos.z + (dir.z * blink_distance) local no_space_to_blink = false -- Create a ray from player's eyes to blink_distance in the direction player is looking local rc = Raycast(origin, dpos, true, false) -- When I first wrote this code, I thought that all this calculation might cause server lag but it actually does very well -- Loop through objects that the ray intersects with for pt in rc do if blink_behind and pt.type == "object" then if pt.ref ~= user then -- Raycast intersects with players head first local ref_ent = pt.ref:get_luaentity() if pt.ref:is_player() or ( ref_ent and blink.valid_entities[ref_ent.name:split(":")[1]]) then local npos = pt.ref:get_pos() -- It took LOTS of testing and crashing to get this to work correctly if pt.ref:is_player() then yaw = pt.ref:get_look_horizontal() else yaw = pt.ref:get_yaw() + (ref_ent.rotate or 0) end npos.y = npos.y + 0.5 reset_pitch = true -- check line-of-site -- this is to prevent someone blinking past blocks behind a player or entity local lookdir = core.yaw_to_dir(yaw) dpos.x = npos.x - (lookdir.x * 2) dpos.z = npos.z - (lookdir.z * 2) dpos.y = npos.y -- The purpose of adding one here is because we are checking for any blockage between the player/mob and the blink destination -- It's possible that checking a straight line would allow someone to blink behind the target where there is not enough room to blink otherwise -- By adding 1 to the Y, we are now casting a ray at an angle which should only allow to blink as long as it's clear two blocks tall npos.y = npos.y + 1 if Raycast(npos, dpos, false, false):next() then no_space_to_blink = true end break -- At this point, the raycast has intersected with a player or mob and there is no reason to keep looking further end end elseif pt.type == "node" then local npos = core.get_pointed_thing_position(pt) local n = core.get_node(npos).name -- First check for unknown node, then see if node is walkable if core.registered_nodes[n] == nil or core.registered_nodes[n].walkable then dpos.x = npos.x + pt.intersection_normal.x dpos.y = npos.y + pt.intersection_normal.y dpos.z = npos.z + pt.intersection_normal.z break end end end -- Save marker pos to oldpos and compare location to see whether we need to blink to the current dpos or create a new marker local oldpos -- Remove marker if there is one if blink.active_marker[username] then if double_tap_tp and marker then oldpos = blink.active_marker[username]:get_pos() end blink.active_marker[username]:remove() blink.active_marker[username] = nil end if not tp_into_prot and core.is_protected(dpos, username) and core.is_protected(origin, public_username) then core.chat_send_player(username, S("Cannot blink into protected areas")) return else if not no_space_to_blink then no_space_to_blink = true for i = 1,-1,-2 do local p = table.copy(dpos) p.y = p.y + i local n = core.get_node(p).name if core.registered_nodes[n] and not core.registered_nodes[n].walkable then no_space_to_blink = false if i == 1 then break else dpos.y = dpos.y - 1 end end end end if no_space_to_blink then markername = "blink:marker2" core.chat_send_player(username, S("Not enough space to blink here")) end end -- if oldpos is set then that means we just removed a marker from this pos -- if oldpos == dpos then we should blink there instead of creating a new marker if (not no_space_to_blink) and oldpos and math.floor(oldpos.x) == math.floor(dpos.x) and math.floor(oldpos.y) == math.floor(dpos.y) and math.floor(oldpos.z) == math.floor(dpos.z) then marker = nil end -- if marker is not nil then we need to create a marker at dpos, otherwise blink to dpos if marker == nil then if not no_space_to_blink then -- When we set_pos, the player's feet is set to the Y level. Move down 0.5 so we don't fall half a block every time we blink dpos.y = dpos.y - 0.5 user:set_pos(dpos) core.sound_play("blink_swoosh", {pos = dpos, max_hear_distance = 10}) if yaw then user:set_look_horizontal(yaw) end if reset_pitch then user:set_look_vertical(0) end if not core.is_creative_enabled(username) then local cooldown = cooldown_base + cooldown_factor * vector.distance(origin, dpos) blink.cooldown[username] = true core.after(cooldown, function() blink.cooldown[username] = false end) end end else blink.active_marker[username] = core.add_entity(dpos, markername, username) end if itemstack then itemstack:add_wear(tool_wear) return itemstack end end core.register_tool("blink:rune", { description = S("Blink Rune"), inventory_image = "blink_rune.png", wield_image = "blink_rune_wield.png", on_use = function(itemstack, user, pointed_thing) return display_marker(user, itemstack) end, on_place = function(itemstack, user, pointed_thing) return blink_tp(user, itemstack) end, on_secondary_use = function(itemstack, user, pointed_thing) return blink_tp(user, itemstack) end }) core.register_tool("blink:forever_rune", { description = S("Forever Rune"), inventory_image = "blink_rune2.png", wield_image = "blink_rune2_wield.png", on_use = function(itemstack, user, pointed_thing) return display_marker(user) end, on_place = function(itemstack, user, pointed_thing) return blink_tp(user) end, on_secondary_use = function(itemstack, user, pointed_thing) return blink_tp(user) end }) e_def = { physical = false, collisionbox = {0,0,0,0,0,0}, visual = "mesh", visual_size = {x = 3.5, y = 3.5}, textures = {"blink_spectrum.png"}, tframe = 1, tlast = 28, tstep = display_time / (28 * color_loops), mesh = "sphere.obj", timer = 0, glow = 7, owner = nil, on_activate = function(self, staticdata, dtime_s) self.owner = staticdata end, on_step = function(self, ftime) self.timer = self.timer + ftime local tframe = math.fmod(math.floor(self.timer / self.tstep), self.tlast) if self.tframe ~= tframe then self.tframe = tframe self.object:set_texture_mod("^[verticalframe:".. self.tlast .. ":" .. tframe) end self.timer = self.timer + ftime if self.timer > display_time then blink.active_marker[self.owner] = nil self.object:remove() end end } core.register_entity("blink:marker", table.copy(e_def)) e_def.textures = {"blink_spectrum2.png"} e_def.tlast = 7 -- This is the Red/Orange glow when there is no space to blink core.register_entity("blink:marker2", e_def) -- Register items -- local mod_main if core.get_modpath("default") then mod_main = "default" elseif core.get_modpath("mcl_core") then mod_main = "mcl_core" end -- Basic crafting items for Blink runes core.register_craftitem("blink:bone_shard", { description = S("Bone shard"), inventory_image = "blink_bone_shard.png" }) if core.get_modpath("bonemeal") then core.register_alias("blink:bone", "bonemeal:bone") core.register_craft({ output = "blink:bone_shard 4", type = "shapeless", recipe = {"bonemeal:bone", "bonemeal:bone"} }) if core.get_modpath("tubelib_addons1") then tubelib.add_grinder_recipe({input = "bonemeal:bone", output = "blink:bone_shard 3"}) end else core.register_craftitem("blink:bone", { description = S("Bone"), inventory_image = "blink_bone.png" }) if core.get_modpath("bones") then core.register_craft({ output = "blink:bone 3", recipe = {{"bones:bones"}} }) end core.register_craft({ output = "blink:bone_shard 2", recipe = {{"blink:bone"}} }) if core.get_modpath("tubelib_addons1") then tubelib.add_grinder_recipe({input = "blink:bone", output = "blink:bone_shard 3"}) end for k, v in pairs(core.registered_nodes) do if v.groups["soil"] or v.groups["dirt"] then core.log("verbose", "Adding blink:bone drop to " .. k) local ndrop if v.drop then if type(v.drop) == "string" then ndrop = {items = {items = {v.drop}}} elseif type(v.drop) == "table" then ndrop = table.copy(v.drop) end table.insert(ndrop.items, { items = {"blink:bone"}, rarity = 50 }) core.override_item(k, {drop = ndrop}) end end end end -- If bones is not installed and we have dmobs:skeleton, add Blink:bone to skeleton drops local skel = core.registered_entities["dmobs:skeleton"] if skel then skel.drops = {name = "blink:bone_shard", chance = 2, min = 2, max = 5} end -- Register Rune Crafts -- local ing1, ing2 = "", "" if mod_main == "default" then ing1 = "default:mese_crystal" ing2 = "default:mese" elseif mod_main == "mcl_core" then if core.get_modpath("mesecons") then ing1 = "mesecons:redstone" end if core.get_modpath("mesecons_torch") then ing2 = "mesecons_torch:redstoneblock" end end core.register_craft({ output = "blink:rune", recipe = { {"blink:bone_shard", "blink:bone_shard", ""}, {"blink:bone_shard", "blink:bone_shard", ing1}, {"blink:bone_shard", "blink:bone_shard", ""} } }) core.register_craft({ output = "blink:forever_rune", recipe = { {"blink:rune", "blink:rune", "blink:rune"}, {"blink:rune", ing2, "blink:rune"}, {"blink:rune", "blink:rune", "blink:rune"}, } })