diff --git a/magic/crafts.lua b/magic/crafts.lua index bd23a04..b506e8b 100644 --- a/magic/crafts.lua +++ b/magic/crafts.lua @@ -68,3 +68,11 @@ minetest.register_craft({ {"default:bronzeblock", "magic:rage_essence", "magic:control_essence"}, }, }) + +minetest.register_craft({ + output = "magic:turret", + recipe = { + {"magic:rage_essence", "magic:control_essence"}, + {"default:steelblock", "group:spellbinding"}, + }, +}) diff --git a/magic/defaults.lua b/magic/defaults.lua index f5659bc..a058e18 100644 --- a/magic/defaults.lua +++ b/magic/defaults.lua @@ -9,3 +9,9 @@ magic.config.mana_fast_regen_threshold = 18 -- Fast regeneration multiplier. magic.config.mana_fast_regen = 2 + +-- Search radius of a missile turret. +magic.config.turret_missile_radius = 24 + +-- Block radius of a defense turret. +magic.config.turret_shield_radius = 5 diff --git a/magic/init.lua b/magic/init.lua index 52b75f2..eb6dde3 100644 --- a/magic/init.lua +++ b/magic/init.lua @@ -31,6 +31,7 @@ domodfile("crafts.lua") domodfile("timegens.lua") domodfile("crystals.lua") domodfile("spells.lua") +domodfile("turrets.lua") domodfile("spells/action.lua") domodfile("spells/attack.lua") diff --git a/magic/spells.lua b/magic/spells.lua index 3933d2f..6f5505e 100644 --- a/magic/spells.lua +++ b/magic/spells.lua @@ -34,8 +34,11 @@ function magic.damage_obj(obj, g) local heldstack = obj:get_wielded_item() local def = minetest.registered_items[heldstack:get_name()] local remove = false - if def.original.protects then + if (def.groups.spell or 0) > 0 and def.original.protects then for k,protect in pairs(def.original.protects) do + if not groups[k] then + break + end if def.original.harmful then if not magic.require_energy(obj, def.original.cost) then break diff --git a/magic/spells/action.lua b/magic/spells/action.lua index 85e7e05..498fd99 100644 --- a/magic/spells/action.lua +++ b/magic/spells/action.lua @@ -70,9 +70,10 @@ magic.register_spell("magic:spell_ice", { emblem = "action", speed = 20, cost = 8, + element = "cold", hit_node = drop_ice, hit_object = function(self, pos, obj) - magic.damage_obj(obj, {cold = 4}) + magic.damage_obj(obj, {cold = (self.was_near_turret > 0 and 2 or 4)}) return true end, }) diff --git a/magic/spells/attack.lua b/magic/spells/attack.lua index 6da8204..069e5cb 100644 --- a/magic/spells/attack.lua +++ b/magic/spells/attack.lua @@ -6,6 +6,8 @@ magic.register_spell("magic:spell_fire", { emblem = "attack", speed = 30, cost = 2, + allow_turret = true, + element = "fire", hit_node = function(self, pos, last_empty_pos) local flammable = minetest.get_item_group(minetest.get_node(pos).name, "flammable") local puts_out = minetest.get_item_group(minetest.get_node(pos).name, "puts_out_fire") @@ -16,6 +18,8 @@ magic.register_spell("magic:spell_fire", { if flammable > 0 then minetest.set_node(pos, {name = "fire:basic_flame"}) return true + elseif magic.missile_passable(pos) then + return false elseif last_empty_pos then minetest.set_node(last_empty_pos, {name = "fire:basic_flame"}) return true @@ -23,7 +27,7 @@ magic.register_spell("magic:spell_fire", { return false end, hit_object = function(self, pos, obj) - magic.damage_obj(obj, {fire = 4}) + magic.damage_obj(obj, {fire = (self.was_near_turret > 0 and 2 or 4)}) return true end, }) @@ -47,6 +51,9 @@ if rawget(_G, 'tnt') and tnt.boom then -- This spell can travel through water. return false end + if magic.missile_passable(pos) then + return false + end tnt.boom(pos, { radius = 3, damage_radius = 5, @@ -61,10 +68,16 @@ if rawget(_G, 'tnt') and tnt.boom then speed = 15, cost = 6, gravity = 0.5, + element = "fire", hit_node = hit_node, hit_object = function(self, pos, obj) return hit_node(self, pos) end, + near_turret = function(self, pos, spell) + if spell.protects and spell.protects.fire and magic.use_turrent_spell(pos) then + return true + end + end, }) minetest.register_craft({ output = "magic:spell_bomb", @@ -82,8 +95,10 @@ magic.register_spell("magic:spell_dart", { emblem = "attack", speed = 60, cost = 1, + element = "fleshy", + allow_turret = true, hit_object = function(self, pos, obj) - magic.damage_obj(obj, {fleshy = 2}) + magic.damage_obj(obj, {fleshy = (self.was_near_turret > 0 and 1 or 2)}) return true end, }) @@ -103,8 +118,10 @@ magic.register_spell("magic:spell_missile", { emblem = "attack", speed = 50, cost = 1, + element = "magic", + allow_turret = true, hit_object = function(self, pos, obj) - magic.damage_obj(obj, {magic = 1, fire = 1}) + magic.damage_obj(obj, {magic = (self.was_near_turret > 0 and 0.5 or 1), fire = (self.was_near_turret > 0 and 0.5 or 1)}) return true end, }) diff --git a/magic/spells/defense.lua b/magic/spells/defense.lua index 809ea8b..70e5745 100644 --- a/magic/spells/defense.lua +++ b/magic/spells/defense.lua @@ -5,6 +5,7 @@ magic.register_spell("magic:spell_dark_shield", { color = "#222", emblem = "defense", cost = 1, + allow_turret = true, protects = { fire = { max = 4, @@ -26,6 +27,7 @@ magic.register_spell("magic:spell_white_shield", { color = "#DDD", emblem = "defense", cost = 1, + allow_turret = true, protects = { magic = { max = 4, @@ -48,6 +50,7 @@ magic.register_spell("magic:spell_solid_shield", { color = "#AA0", emblem = "defense", cost = 2, + allow_turret = true, protects = { fleshy = { max = 4, diff --git a/magic/textures/magic_turret.png b/magic/textures/magic_turret.png new file mode 100644 index 0000000..e62d276 Binary files /dev/null and b/magic/textures/magic_turret.png differ diff --git a/magic/throwing.lua b/magic/throwing.lua index 38d18d6..9c18142 100644 --- a/magic/throwing.lua +++ b/magic/throwing.lua @@ -62,6 +62,14 @@ local function rayIter(pos, dir, range) end -- END COPIED +function magic.missile_passable(pos) + local def = minetest.registered_nodes[minetest.get_node(pos).name] + if not def.walkable and def.buildable_to then + return true + end + return false +end + function magic.register_missile(name, texture, def, item_def) def.hit_object = def.hit_object or function(self, pos, obj) @@ -73,9 +81,16 @@ function magic.register_missile(name, texture, def, item_def) end def.hit_node = def.hit_node or function(self, pos, last_empty_pos) + if magic.missile_passable(pos) then + return false + end return true end + def.near_turret = def.near_turret or function(self, pos, spell) + return false + end + def.is_passthrough_node = def.is_passthrough_node or function(self, pos, node) return node.name == "air" end @@ -89,6 +104,7 @@ function magic.register_missile(name, texture, def, item_def) textures = {texture}, lastpos={}, lastair = nil, + was_near_turret = 0, collisionbox = {0,0,0,0,0,0}, } @@ -105,6 +121,7 @@ function magic.register_missile(name, texture, def, item_def) elseif def.is_passthrough_node(self, vector.add(pos, {x=0, y=2, z=0}), minetest.get_node(vector.add(pos, {x=0, y=2, z=0}))) then self.lastair = vector.add(pos, {x=0, y=2, z=0}) end + return end if self.timer > TIMEOUT then @@ -115,11 +132,12 @@ function magic.register_missile(name, texture, def, item_def) local line = { start = self.lastpos, finish = pos, - middle = { - x = (self.lastpos.x + pos.x) / 2, - y = (self.lastpos.y + pos.y) / 2, - z = (self.lastpos.z + pos.z) / 2, - }, + } + + line.middle = { + x = (line.start.x + line.finish.x) / 2, + y = (line.start.y + line.finish.y) / 2, + z = (line.start.z + line.finish.z) / 2, } local Hit = {x=0, y=0, z=0}; @@ -166,11 +184,11 @@ function magic.register_missile(name, texture, def, item_def) end local function CheckLineNear(line, pos, distance) - local nx = 0.5 + local nx = 0.25 if line.finish.x < line.start.x then nx = -nx end - local ny = 0.5 + local ny = 0.25 if line.finish.y < line.start.y then ny = -ny end - local nz = 0.5 + local nz = 0.25 if line.finish.z < line.start.z then nz = -nz end for x=line.start.x,line.finish.x,nx do @@ -186,31 +204,22 @@ function magic.register_missile(name, texture, def, item_def) return false end - local objs = minetest.get_objects_inside_radius(line.middle, (math.ceil(vector.distance(line.middle, line.start)) + math.ceil(vector.distance(line.middle, line.finish)) * 2) + 6) - for k, obj in pairs(objs) do - local bb = obj:get_properties().collisionbox - -- If bb collides with line... - local b1 = vector.add(obj:getpos(), vector.multiply({x=bb[1], y=bb[2], z=bb[3]}, 1.5)) - local b2 = vector.add(obj:getpos(), vector.multiply({x=bb[4], y=bb[5], z=bb[6]}, 1.5)) - if CheckLineBox(b1, b2, line.start, line.finish) or CheckLineNear(line, obj:getpos(), 1) then - if obj:get_luaentity() ~= nil then - if obj:get_luaentity().name ~= name and not NO_HIT_ENTS[obj:get_luaentity().name] then - if def.hit_object(self, obj:getpos(), obj) then - self.object:remove() - end - end - elseif obj:is_player() then - local can = true - if self.timer > 0.5 or not self.player or obj:get_player_name() ~= self.player:get_player_name() then - if def.hit_player(self, obj:getpos(), obj) then - self.object:remove() - end - end - end - end + for _,pos in ipairs(kingdoms.utils.find_nodes_by_area(pos, magic.config.turret_shield_radius, {"magic:turret"})) do + if self.kingdom == minetest.get_meta(pos):get_string("kingdom.id") then + break + end + local turret_spell = magic.get_turret_spell(pos) + if turret_spell.protects and turret_spell.protects[def.element] and (self.was_near_turret > 1 or magic.use_turrent_spell(pos)) then + self.was_near_turret = self.was_near_turret + 1 + end + if def.near_turret(self, pos, turret_spell) then + self.object:remove() + return + end end local hitnode = nil + local willremove = false for pos in rayIter(line.start, self.object:getvelocity(), vector.distance(line.start, line.finish)) do local node = minetest.get_node(pos) @@ -225,9 +234,45 @@ function magic.register_missile(name, texture, def, item_def) if hitnode then if def.hit_node(self, hitnode, self.lastair) then self.object:remove() + willremove = true end end + local objs = minetest.get_objects_inside_radius(line.middle, (math.ceil(vector.distance(line.middle, line.start)) + math.ceil(vector.distance(line.middle, line.finish)) * 2) + 6) + for k, obj in pairs(objs) do + local bb = obj:get_properties().collisionbox + local pp = vector.add(obj:getpos(), {x=0, y=0.5, z=0}) + -- If bb collides with line... + local b1 = vector.add(pp, vector.multiply({x=bb[1], y=bb[2], z=bb[3]}, 1.5)) + local b2 = vector.add(pp, vector.multiply({x=bb[4], y=bb[5], z=bb[6]}, 1.5)) + if willremove and vector.distance(obj:getpos(), line.start) > vector.distance(hitnode, line.start) then + break + end + if CheckLineBox(b1, b2, line.start, line.finish) or CheckLineNear(line, pp, 1) then + if obj:get_luaentity() ~= nil then + if obj:get_luaentity().name ~= name and not NO_HIT_ENTS[obj:get_luaentity().name] then + if def.hit_object(self, obj:getpos(), obj) then + self.object:remove() + return + end + end + elseif obj:is_player() then + local can = true + if self.timer > 0.5 or not self.player or obj:get_player_name() ~= self.player:get_player_name() then + if def.hit_player(self, obj:getpos(), obj) then + self.object:remove() + return + end + end + end + end + end + + if willremove then + self.object:remove() + return + end + if self.particletimer > 0.05 then minetest.add_particle({ pos = pos, @@ -252,11 +297,8 @@ function magic.register_missile(name, texture, def, item_def) obj:setvelocity({x=dir.x*def.speed, y=dir.y*def.speed, z=dir.z*def.speed}) obj:setacceleration({x=0, y=-8.5*(def.gravity or 0), z=0}) obj:setyaw(player:get_look_yaw()+math.pi) - if obj:get_luaentity() then - obj:get_luaentity().player = player - else - obj:remove() - end + obj:get_luaentity().player = player + obj:get_luaentity().kingdom = kingdoms.player.kingdom(player:get_player_name()) and kingdoms.player.kingdom(player:get_player_name()).id or nil itemstack:take_item() return itemstack end diff --git a/magic/turrets.lua b/magic/turrets.lua new file mode 100644 index 0000000..044d748 --- /dev/null +++ b/magic/turrets.lua @@ -0,0 +1,163 @@ +local function update_formspec(pos) + local meta = minetest.get_meta(pos) + local name = meta:get_string("spell_name") + name = (name ~= "") and name or "N/A" + local count = meta:get_int("spell_count") + meta:set_string("formspec", ([[ + size[8,6] + label[0,0.1;Input spells. Current: ]]..tostring(name).." qty "..tostring(count)..[[] + button[0,1;8,1;setp_%d;Only Players (Current: %s)] + list[context;input;7,0;1,1;] + list[current_player;main;0,2;8,4;] + ]]):format(meta:get_int("onlyplayers"), (meta:get_int("onlyplayers") == 1) and "yes" or "no")) +end + +minetest.register_node("magic:turret", { + description = "Turret", + tiles = {"magic_turret.png"}, + groups = {cracky = 1, kingdom_infotext = 1}, + + on_place = function(itemstack, placer, pointed_thing) + if not placer or pointed_thing.type ~= "node" then + return itemstack + end + + local kingdom = kingdoms.player.kingdom(placer:get_player_name()) + if not kingdom then + minetest.chat_send_player(placer:get_player_name(), "You cannot place a turret if you are not a member of a kingdom.") + return itemstack + end + + return minetest.item_place(itemstack, placer, pointed_thing) + end, + + after_place_node = function(pos, placer) + local kingdom = kingdoms.player.kingdom(placer:get_player_name()) + local meta = minetest.get_meta(pos) + meta:set_string("kingdom.id", kingdom.id) + meta:set_string("spell_name", "") + meta:set_int("spell_count", 0) + + meta:get_inventory():set_size("input", 8) + update_formspec(pos) + end, + on_metadata_inventory_put = function(pos, listname, index, stack, player) + local meta = minetest.get_meta(pos) + meta:get_inventory():set_list("input", {}) + meta:set_string("spell_name", stack:get_name()) + meta:set_int("spell_count", meta:get_int("spell_count") + stack:get_count()) + meta:set_int("spell_max", stack:get_stack_max()) + meta:set_int("onlyplayers", 0) + update_formspec(pos) + end, + allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) + return 0 + end, + allow_metadata_inventory_put = function(pos, listname, index, stack, player) + local meta = minetest.get_meta(pos) + if listname ~= "input" then return 0 end + if not minetest.registered_items[stack:get_name()].original or not minetest.registered_items[stack:get_name()].original.allow_turret then + minetest.chat_send_player(player:get_player_name(), "You must place turret-enabled spells in this device.") + return 0 + end + if stack:get_name() ~= meta:get_string("spell_name") and meta:get_int("spell_count") > 0 then + minetest.chat_send_player(player:get_player_name(), "This turret still is holding "..meta:get_string("spell_name")) + return 0 + end + if not kingdoms.player.canpos(pos, player:get_player_name(), "devices") then + minetest.chat_send_player(player:get_player_name(), "You cannot use this device.") + return 0 + end + return stack:get_count() + end, + allow_metadata_inventory_take = function(pos, listname, index, stack, player) + return 0 + end, + on_destruct = function(pos) + local meta = minetest.get_meta(pos) + if meta:get_string("spell_name") ~= "" then + local count = meta:get_int("spell_count") + local needed = math.ceil(count / meta:get_int("spell_max")) + for i=1,needed do + minetest.add_item(pos, meta:get_string("spell_name").." "..tostring(math.min(meta:get_int("spell_max"), count))) + count = count - meta:get_int("spell_max") + end + end + end, + on_receive_fields = function(pos, formname, fields, player) + if not kingdoms.player.canpos(pos, player:get_player_name(), "devices") then + minetest.chat_send_player(player:get_player_name(), "You cannot use this device.") + return 0 + end + local meta = minetest.get_meta(pos) + if fields.setp_0 then + meta:set_int("onlyplayers", 1) + elseif fields.setp_1 then + meta:set_int("onlyplayers", 0) + end + update_formspec(pos) + end, +}) + +minetest.register_abm({ + nodenames = {"magic:turret"}, + interval = 3, + chance = 1, + action = function(pos, node) + local meta = minetest.get_meta(pos) + if not kingdoms.db.kingdoms[meta:get_string("kingdom.id")] then + return + end + local name = meta:get_string("spell_name") + local count = meta:get_int("spell_count") + if count <= 0 then return end + local def = minetest.registered_items[name].original + if def.type == "missile" then + local closest = nil + local objs = minetest.get_objects_inside_radius(pos, magic.config.turret_missile_radius) + for _,obj in pairs(objs) do + local ok = true + if obj:get_luaentity() ~= nil and meta:get_int("onlyplayers") == 0 then + if NO_HIT_ENTS[obj:get_luaentity().name] then + ok = false + end + elseif obj:is_player() then + if kingdoms.player.kingdom(obj:get_player_name()) and kingdoms.player.kingdom(obj:get_player_name()).id == meta:get_string("kingdom.id") then + ok = false + end + end + if ok and (not closest or vector.distance(obj:getpos(), pos) < vector.distance(closest:getpos(), pos)) then + closest = obj + end + end + if closest then + local dir = vector.normalize{x=closest:getpos().x - pos.x, y=(closest:getpos().y + 0.5) - pos.y, z=closest:getpos().z - pos.z} + local mobj = minetest.add_entity(pos, name.."_missile") + mobj:setvelocity({x=dir.x*def.speed, y=dir.y*def.speed, z=dir.z*def.speed}) + mobj:setacceleration({x=0, y=-8.5*(def.gravity or 0), z=0}) + mobj:get_luaentity().kingdom = meta:get_string("kingdom.id") + meta:set_int("spell_count", count - 1) + update_formspec(pos) + end + end + end, +}) + +function magic.get_turret_spell(pos) + local meta = minetest.get_meta(pos) + local name = meta:get_string("spell_name") + local count = meta:get_int("spell_count") + if count <= 0 then return end + return minetest.registered_items[name].original +end + +function magic.use_turret_spell(pos) + local meta = minetest.get_meta(pos) + local name = meta:get_string("spell_name") + local count = meta:get_int("spell_count") + if count <= 0 then return false end + meta:set_int("spell_count", count - 1) + update_formspec(pos) + return true +end + diff --git a/manual.md b/manual.md index 5737d7b..6ab3401 100644 --- a/manual.md +++ b/manual.md @@ -16,9 +16,8 @@ These claims cannot overlap with other claims, and must be spaced with at least Defending a neutral area, or even preventing enemies from entering your clamied area, is enhanced beyond personal battle by the addition of several nodes: -* (TODO) Turrets, which ward off enemies by blasting spells at them. +* Turrets, which, after a missile or shield spell is inserted, will ward off enemies by blasting spells at them and cancel or subdue enemy spells. * Materializers and Materialized Walls. The Walls act like hard stone, but when a Materializer is placed by them they will level from 1 to 4 over time. A level 4 node, when dug, will revert to a level 3 instead of vanishing, and so on down to a level 1. They will also absorb explosions, preventing damage inside them. -* (TODO) Spell Wards, which prevent destructive or disruptive spells from entering your territory. ## Destroying a Corestone