Index: data/runes.lua ================================================================== --- data/runes.lua +++ data/runes.lua @@ -2,31 +2,40 @@ -- applied to an amulet in order to imbue that amulet with unique -- and fearsome powers. the specific spell depends on the stone the -- rune is applied to, and not all runes can necessarily be applied -- to all stones. -local sparkle = function(color, spell, amt,time,minsize,maxsize,sh) - spell.visual_subjects { - amount = amt, time = time, -- attached = s; - minpos = { x = -0.3, y = -0.5, z = -0.3 }; - maxpos = { x = 0.3, y = sh*1.1, z = 0.3 }; +local sparkle_region = function(s) + s.spell.visual_subjects { + amount = s.amt, time = s.time, -- attached = s; + minpos = s.minpos; + maxpos = s.maxpos; minvel = { x = -0.4, y = -0.2, z = -0.4 }; maxvel = { x = 0.4, y = 0.2, z = 0.4 }; minacc = { x = -0.5, y = -0.4, z = -0.5 }; maxacc = { x = 0.5, y = 0.4, z = 0.5 }; - minexptime = 1.0, maxexptime = 2.0; - minsize = minsize, maxsize = maxsize, glow = 14; - texture = sorcery.vfx.glowspark(color):render(); + minexptime = 1.0*(s.length or 1), maxexptime = 2.0 * (s.length or 1); + minsize = s.minsize, maxsize = s.maxsize, glow = 14; + texture = (s.img or sorcery.vfx.glowspark(s.color)):render(); animation = { type = 'vertical_frames'; aspect_w = 16, aspect_h = 16; + length = 0.1 + (s.length or 1)*2; }; } end -local sparktrail = function(fn,tgt,color) +local sparkle = function(color, spell, amt,time,minsize,maxsize,sh) + sparkle_region { spell = spell; + amt = amt, time = time, color = color; + minsize = minsize, maxsize = maxsize; + minpos = { x = -0.3, y = -0.5, z = -0.3 }; + maxpos = { x = 0.3, y = sh*1.1, z = 0.3 }; + } +end +local sparktrail = function(fn,tgt,color,time) return (fn or minetest.add_particlespawner)({ - amount = 240, time = 1, attached = tgt; + amount = 240, time = time or 1, attached = tgt; minpos = {x = -0.4, y = -0.5, z = -0.4}; maxpos = {x = 0.4, y = tgt:get_properties().eye_height or 0.5, z = 0.4}; minacc = {x = 0.0, y = 0.05, z = 0.0}; maxacc = {x = 0.0, y = 0.15, z = 0.0}; minexptime = 1.5, maxexptime = 5; @@ -41,11 +50,11 @@ return { translocate = { name = 'Translocate'; tone = {0,235,233}; minpower = 3; - rarity = 15; + rarity = 10; amulets = { amethyst = { name = 'Joining'; desc = 'Give this amulet to another and they can arrive safely at your side in a flash from anywhere in the world — though returning whence they came may be a more difficult matter'; apply = function(ctx) @@ -84,10 +93,11 @@ ctx.meta:set_string('rune_return_dest','') local subjects = { ctx.caster } local center = ctx.caster:get_pos() ctx.sparkle = false local delay = math.max(3,10 - ctx.stats.power) + 3*(math.random()*2-1) + if ctx.amulet.frame == 'tungsten' then delay = delay * 0.5 end for _,s in pairs(subjects) do local offset = vector.subtract(s:get_pos(), center) local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset)) if pt then -- minetest.sound_play('sorcery_stutter', { @@ -125,21 +135,22 @@ s:set_pos(pt) sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2) end; }; sounds = { - [0] = { - pos = 'subjects'; - sound = 'sorcery_stutter'; - }; + [0] = { sound = 'sorcery_stutter', pos = 'subjects' }; }; } end end end end; frame = { + tungsten = { + name = 'Quick Return'; + desc = 'Use this amulet once to bind it to a particular place, then discharge its spell to translocate yourself rapidly back to that point from anywhere in the world.'; + }; iridium = { name = 'Mass Return'; desc = 'Use this amulet once to bind it to a particular place, then carry yourself and everyone around you back to that point in a flash simply by using it again'; }; }; @@ -193,11 +204,11 @@ }; disjoin = { name = 'Disjoin'; tone = {159,235,0}; minpower = 4; - rarity = 20; + rarity = 40; amulets = { sapphire = { name = 'Unsealing'; desc = 'Wielding this amulet, a touch of your hand will unravel even the mightiest protective magics, leaving doors unsealed and walls free to tear down'; }; @@ -238,25 +249,57 @@ }; }; }; luxite = { name = 'Disjunctive Aura'; - desc = 'For a time, all magic undertaken in your vicinity will fail totally'; + desc = 'For a time, all magic undertaken in your vicinity will fail totally — including your own'; cast = function(ctx) + local h = ctx.heading.eyeheight*1.1 sorcery.spell.cast { caster = ctx.caster, attach = 'caster'; + subjects = {{player=ctx.caster}}; disjunction = true, range = 4 + ctx.stats.power; duration = 10 + ctx.stats.power * 3; timeline = { [0] = function(s,_,tl) - sparkle(sorcery.lib.color(120,255,30), s, - 30 * tl, tl, 0.3,1.4, ctx.heading.eyeheight*1.1) - end + local ttns = 0.8 + local vel = s.range / ttns + s.visual_caster { + amount = 300, time = ttns, glow = 14; + texture = sorcery.lib.image('sorcery_sputter.png'):glow(sorcery.lib.color(160,255,80)):render(); + minpos = { x = -0.0, y = h*0.5,z = -0.0 }; + maxpos = { x = 0.0, y = h*0.5,z = 0.0 }; + minvel = { x = -vel, y = -0.0, z = -vel }; + maxvel = { x = vel, y = 0.0, z = vel }; + minacc = { x = -0.2, y = -0.0, z = -0.2 }; + maxacc = { x = 0.2, y = 0.0, z = 0.2 }; + minexptime = ttns, maxexptime = ttns * 2; + minsize = 0.2, maxsize = 4.5; + animation = { + type = 'vertical_frames', length = 0.1 + ttns*2; + aspect_w = 16, aspect_h = 16; + } + } + end; + [{whence=0,secs=0.8}] = function(s,te,tl) + local range = s.range + sparkle_region { + spell = s, amt = 150*tl, time = tl; + minsize = 1, maxsize = 8.4; + minpos = { x = 0-range, y = -0.5, z = 0-range }; + maxpos = { x = range, y = h, z = range }; + img = sorcery.lib.image('sorcery_flicker.png'):glow(sorcery.lib.color(120,255,30)); + } + end; }; sounds = { - [0] = { sound = 'sorcery_disjoin', pos = 'caster' }; - [1] = { sound = 'sorcery_powerdown', pos = 'caster' }; + [0.00] = {sound='sorcery_disjoin', where='caster'}; + [{whence=0,secs=0.8}] = { + sound='sorcery_disjoin_bg', where='subjects'; + gain=0.5, stop = {whence=1,secs=-1.5} + }; + [1.00] = {sound='sorcery_powerdown', where='caster'}; }; } end }; diamond = { @@ -273,21 +316,20 @@ }; repulse = { name = 'Repulse'; tone = {0,180,235}; minpower = 1; - rarity = 7; + rarity = 5; amulets = { amethyst = { name = 'Hurling'; desc = 'Wielding this amulet, a mere flick of your fingers will lift any target of your choice bodily into the air and press upon them with tremendous repulsive force, throwing them like a hapless ragdoll out of your path'; cast = function(ctx) if not (ctx.target and ctx.target.type == 'object') then return false end local tgt = ctx.target.ref local line = vector.subtract(ctx.caster:get_pos(), tgt:get_pos()) -- direction vector from target to caster - print('line',dump(line)) local dir,mag = sorcery.lib.math.vsep(line) if mag > 6 then return false end -- no cheating! local force = 20 + (ctx.stats.power * 2.5) minetest.sound_play('sorcery_hurl',{pos=tgt:get_pos()},true) local immortal = tgt:get_luaentity():get_armor_groups().immortal or 0 @@ -311,31 +353,129 @@ local power = 14 * (1+(ctx.stats.power * 0.2)) minetest.sound_play('sorcery_hurl',{object=ctx.caster},true) sorcery.spell.cast { caster = ctx.caster; subjects = {{player=ctx.caster}}; - duration = power * 0.25; + duration = power * 0.30; timeline = { [0] = function(s,_,tl) sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93)) ctx.caster:add_velocity{y=power;x=0,z=0} s.affect { - duration = power * 0.25; + duration = power * 0.50; raise = 2; - fall = (power * 0.25) * 0.3; + -- fall = (power * 0.25) * 0.3; impacts = { gravity = 0.1; }; } end; + }; + intervals = { + {period = 0.2, after = {whence=0, secs=2}; fn = function(c) + -- return gravity to normal once they touch down + for si,sub in pairs(c.spell.subjects) do + local p = sub.player:get_pos() + for i=1,3 do + local sum = vector.offset(p,0,-i,0) + if not sorcery.lib.node.is_air(sum) then + c.spell.release_subject(si) + if #c.spell.subjects == 0 then + return false + end + break + end + end + end + end}; }; } end; }; sapphire = { name = 'Flinging'; desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you'; + cast = function(ctx) + if not (ctx.target and ctx.target.type == 'object') then return false end + local tgt = ctx.target.ref + local power = 16 * (1+(ctx.stats.power * 0.2)) + minetest.sound_play('sorcery_hurl',{object=ctx.caster},true) + sorcery.spell.cast { + caster = ctx.caster; + subjects = {{player=tgt}}; + duration = 4; + timeline = { + [0] = function(s,_,tl) + for _,sub in pairs(s.subjects) do + local height = (sub.player:get_properties().eye_height or 1)*1.3 + local scenter = vector.add(sub.player:get_pos(), {x=0,y=height/2,z=0}) + for i=1,math.random(64,128) do + local high = (height+0.8)*math.random() - 0.8 + local far = (high >= -0.5 and high <= height) and + (math.random() * 0.3 + 0.4) or + (math.random() * 0.5) + local yaw = {x=0, y = math.random()*100, z=0} + local po = vector.rotate({x=far,y=high,z=0}, yaw) + local ppos = vector.add(po,sub.player:get_pos()) + local dir = vector.direction(ppos,scenter) + local vel = math.random() * 0.8 + 0.4 + minetest.add_particle { + pos = ppos; + velocity = vector.multiply(dir,vel); + expirationtime = far / vel; + size = math.random()*2.4 + 0.6; + texture = sorcery.lib.image('sorcery_sputter.png'):glow(sorcery.lib.color{ + hue = math.random(41,63); + saturation = 100; + luminosity = 0.5 + math.random()*0.3; + }):render(); + glow = 14; + animation = { + type = 'vertical_frames', length = far/vel; + aspect_w = 16, aspect_h = 16; + }; + } + end + end + end; + [0.3] = function(s,te,tl) + sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93)) + for _,sub in pairs(s.subjects) do + sub.player:add_velocity{y=power;x=0,z=0} + end + end; + [1] = (ctx.amulet.frame == 'cobalt') and function(s,te,tl) + -- TODO add visuals + for _,sub in pairs(s.subjects) do + sub.player:add_velocity{y=-power*2;x=0,z=0} + end + end or nil; + }; + sounds = { + [0.3] = { + sound = 'sorcery_hurl'; + where = 'subjects'; + ephemeral = true; + }; + [1] = (ctx.amulet.frame == 'cobalt') and { + sound = 'sorcery_hurl'; + where = 'subjects'; + ephemeral = true; + } or nil; + }; + }; + end; + frame = { + cobalt = { + name = 'Crushing'; + desc = 'Toss an enemy violently into the air, then bring them crashing down to earth with bone-shattering force'; + }; + iridium = { + name = 'Mass Flinging'; + desc = 'Send everyone around you hurtling into the sky, and allow the inevitable impact to do your dirty work for you'; + }; + }; }; emerald = { name = 'Shockwave'; desc = 'Let loose a stream of concussive force that slams into everything in your path and sends them hurtling away from you'; }; @@ -351,11 +491,11 @@ }; obliterate = { name = 'Obliterate'; tone = {255,0,10}; minpower = 5; - rarity = 30; + rarity = 35; amulets = { amethyst = { name = 'Sapping'; desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention'; }; @@ -377,16 +517,16 @@ bolt:get_luaentity()._blastradius = radius bolt:set_velocity(vel) end; }; luxite = { - name = 'Lethal Aura'; - desc = 'For a time, anyone who approaches you, whether friend or foe, will suffer immediate retaliation as they are quickly sapped of their life force'; + name = 'Cataclysmic Aura'; + desc = 'A storm of destructive force rages about you as you stand untouched, the master of its voracious dark energies'; }; mese = { name = 'Cataclysm'; - desc = 'Use this amulet once to pick a target, then visit devastation upon it from afar with a mere snap of your fingers'; + desc = 'Use this amulet once to pick a target, then visit devastation upon it from afar whenever you so will with a mere snap of your fingers'; }; diamond = { name = 'Killing'; mingrade = 4; desc = 'Wield this amulet against a foe to instantly snuff the life out of their mortal form, regardless of their physical protections.'; @@ -410,39 +550,151 @@ }; excavate = { name = 'Excavate'; tone = {0,68,235}; minpower = 3; - rarity = 60; + rarity = 30; amulets = { + luxite = { + name = 'Stonestride'; + desc = 'Rock walls will open up before you when you brandish this amulet before them, closing up again behind you without leaving a trace of your passage'; + }; sapphire = { name = 'Tunnelling'; - desc = 'Carve a long tunnel ahead of you into the rock'; + desc = 'Carve a long tunnel ahead of you into the rock and dirt'; + cast = function(ctx) + if ctx.target.type ~= 'node' then return false end + local allowed = { + ['default:stone'] = true; + ['default:desert_stone'] = true; + ['default:dirt'] = true; + ['default:gravel'] = true; + } + if allowed[minetest.get_node(ctx.target.under).name] ~= true then + return false + end + local timeline,sounds = {}, {} + local tunnel_depth = math.random(5,9) * ctx.stats.power + local cname = ctx.caster:get_player_name() + local cut = function(step,s,te,tl) + local smash = function(pos) + if not allowed[minetest.get_node(pos).name] then return end + if minetest.is_protected(pos, cname) then return end + s.visual { + amount = math.random(32,48), time = 0.2, glow = 14; + texture = sorcery.lib.image('sorcery_spark.png'):glow(sorcery.lib.color(10,20,255)):render(); + minpos = vector.subtract(pos, {x=0.5,y=0.5,z=0.5}); + maxpos = vector.add (pos, {x=0.5,y=0.5,z=0.5}); + minvel = {x = -0.3, y = -0.3, z = -0.3}; + maxvel = {x = 0.3, y = 0.3, z = 0.3}; + minacc = {x = -0.6, y = -0.6, z = -0.6}; + maxacc = {x = 0.6, y = 0.6, z = 0.6}; + minexptime = 0.4, maxexptime = 1.2; + minsize = 0.3, maxsize = 1.2; + animation = { + type = 'vertical_frames', length = 1.3; + aspect_w = 16, aspect_h = 16; + }; + } + minetest.dig_node(pos) + if math.random(5) == 1 then + minetest.set_node(pos, {name='sorcery:air_flash_' .. tostring(math.random(10))}) + end + -- TODO visuals + end + local r = s.tunnel_radius + local yaw = {x=0,y=s.tunnel_angle,z=0} + s.visual { + amount = 16, time = 3, glow = 14; + texture = sorcery.lib.image('sorcery_sparking.png'):glow(sorcery.lib.color(20,60,255)):render(); + minpos = vector.subtract(s.anchor, {x=r,y=r,z=r}); + maxpos = vector.add (s.anchor, {x=r,y=r,z=r}); + minvel = {x = -0.1, y = -0.1, z = -0.1}; + maxvel = {x = 0.1, y = 0.1, z = 0.1}; + minexptime = 1.0, maxexptime = 1.4; + minsize = 1.5, maxsize = 4; + animation = { + type = 'vertical_frames', length = 1.5; + aspect_w = 64, aspect_h = 64; + }; + } + for x=-r,r do for y=-r,r do + local xs = x < 0 and -1 or 1 + local ys = y < 0 and -1 or 1 + if x^2 + y^2 <= r^2 then + if (x+xs)^2 + y^2 > r^2 or + (y+ys)^2 + x^2 > r^2 then + -- we're right at the edge - make a mess + if math.random(5) == 1 then goto skip end + end + local p = vector.add(s.anchor,vector.rotate({x=x,y=y,z=0},yaw)) + smash(p) + end + ::skip::end end + -- if math.random(1,10) == 1 then + -- s.tunnel_angle = s.tunnel_angle + math.random(-0.05,0.05) + -- yaw.y = s.tunnel_angle + -- end + if math.random(1,21) == 1 then + s.tunnel_radius = math.min(6,math.max(3,s.tunnel_radius + math.random(-1,1))) + end + local dir = vector.rotate({x=0,y=0,z=1},yaw) + if sorcery.lib.math.vdcomp(1, dir) < 1 then + dir = vector.normalize(dir) + end + s.anchor = vector.add(s.anchor,dir) + end + local tp = 0 + for i=1,tunnel_depth do + local now = {whence=0,secs=tp} + timeline[now] = function(...) cut(i,...) end + sounds[now] = { + sound='sorcery_crunch', where='pos'; + ephemeral=true, gain = math.random(3,10) * 0.1; + } + tp = tp + (math.random(2,5) * 0.1) + end + sounds[1] = {sound='sorcery_powerdown', where='pos'} + sorcery.spell.cast { + caster = ctx.caster; + duration = tp; + timeline = timeline, sounds = sounds; + -- spell state + anchor = ctx.target.under; + tunnel_angle = ctx.caster:get_look_horizontal(); + tunnel_radius = math.floor(math.random(3,5) * (ctx.stats.power * 0.1)); + } + end; }; emerald = { name = 'Boring'; desc = 'Release the force of this amulet to punch a deep borehole down into the earth below'; - } + }; + amethyst = { + name = 'Shaftcutting'; + desc = 'Cut a wide shaft up into the ceiling of a cavern'; + }; }; }; genesis = { name = 'Genesis'; tone = {235,0,175}; minpower = 5; - rarity = 50; + rarity = 25; amulets = { mese = { + mingrade = 4; name = 'Duplication'; desc = 'Generate a copy of any object or item, no matter how common or rare'; }; }; }; luminate = { name = 'Luminate'; tone = {255,194,0}; minpower = 1; - rarity = 25; + rarity = 5; amulets = { luxite = { name = 'Glow'; desc = 'Swathe yourself in an aura of sparkling radiance, casting light upon all the dark places where you voyage'; }; @@ -461,29 +713,32 @@ }; dominate = { name = 'Dominate'; tone = {235,0,228}; minpower = 4; - rarity = 40; + rarity = 20; amulets = { amethyst = { name = 'Suffocation'; desc = 'Wrap this spell tightly around your victim\'s throat, cutting off their oxygen until you release them.'; }; emerald = { name = 'Caging'; desc = 'Trap your victim in an impenetrable field of force, leaving them with no way out but translocation or waiting for the field to release them'; }; + luxite = { + name = 'Vampiric Aura'; + desc = 'For a time, anyone who approaches you, whether friend or foe, will suffer immediate retaliation as they are quickly sapped of their vital force in order to replenish your own'; + }; ruby = { name = 'Exsanguination'; desc = 'Rip the life force out of another, leaving them on the brink of death, and use it to mend your own wounds and invigorate your being'; cast = function(ctx) if not (ctx.target and ctx.target.type == 'object') then return false end local tgt = ctx.target.ref local takefac = math.min(99,50 + (ctx.stats.power * 5)) / 100 local dmg = tgt:get_hp() * takefac - print("!!! dmg calc",takefac,dmg,tgt:get_hp()) local numhits = math.random(6,10+ctx.stats.power/2) local function dohit(hitsleft) if tgt == nil or tgt:get_properties() == nil then return end tgt:punch(ctx.caster, 1, { Index: displacer.lua ================================================================== --- displacer.lua +++ displacer.lua @@ -122,10 +122,13 @@ end; on_timer = function(pos,delta) local meta = minetest.get_meta(pos) if not meta:contains('active-device') then return false end + local probe = sorcery.spell.probe(pos) + if probe.disjunction then return true end + local inv = meta:get_inventory() if inv:is_empty('cache') then return false end local dev = gettxr(pos) local active = minetest.string_to_pos(meta:get_string('active-device')) @@ -145,22 +148,25 @@ elseif ad.code then local net = sorcery.farcaster.junction(pos,constants.xmit_wattage) for _,n in pairs(net) do for _,d in pairs(n.caps.net.devices.consume) do if d.id == 'sorcery:displacer' then - local t = gettxr(d.pos) - for _,d in pairs(t.connections) do - if d.mode == 'receive' and d.code then - local match = true - for i=1,#d.code do - if d.code[i] ~= ad.code[i] then - match = false break + local dp = sorcery.spell.probe(d.pos) + if not dp.disjunction then + local t = gettxr(d.pos) + for _,d in pairs(t.connections) do + if d.mode == 'receive' and d.code then + local match = true + for i=1,#d.code do + if d.code[i] ~= ad.code[i] then + match = false break + end end - end - if match then - remote = t - break + if match then + remote = t + break + end end end end end if remote then break end Index: enchanter.lua ================================================================== --- enchanter.lua +++ enchanter.lua @@ -402,10 +402,15 @@ -- perform leyline checks and call notify if necessary if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then sorcery.lib.node.notifyneighbors(pos) end + + -- is there an active disjunction in effect here? + -- if so, return immediately and perform no magic + local probe = sorcery.spell.probe(pos) + if probe.disjunction then return end -- we're goint to do something VERY evil here and -- replace the air with a "glow-air" that removes -- itself after a short period of time, to create -- a flash of light when an enchanted tool's used Index: forcefield.lua ================================================================== --- forcefield.lua +++ forcefield.lua @@ -125,21 +125,26 @@ minetest.get_node_timer(pos):start(1) end; on_timer = function(pos,delta) local orientation = math.floor(minetest.get_node(pos).param2 / 4) local costs = calc_cost(pos,delta) + local probe = sorcery.spell.probe(pos) + if probe.disjunction then return true end local l = sorcery.ley.netcaps(pos,delta) if l.self.powerdraw >= costs.mincost then local dist = l.self.powerdraw / (constants.cost_per_barrier * delta) for i=1,math.floor(dist) do local t = costs.targets[i] local str = math.min(0xFF,t[2] + 50*delta); - minetest.swap_node(t[1], { - name = 'sorcery:air_barrier_' .. math.max(1, math.floor(10*(str/0xFF))); - param2 = str; - }) - minetest.get_node_timer(t[1]):start(1) + local fprobe = sorcery.spell.probe(t[1]) + if not fprobe.disjunction then + minetest.swap_node(t[1], { + name = 'sorcery:air_barrier_' .. math.max(1, math.floor(10*(str/0xFF))); + param2 = str; + }) + minetest.get_node_timer(t[1]):start(1) + end end local pn = vector.add(pos, vector.divide(costs.aim,2)); local pp = vector.add(pn, pofstbl[orientation]) pn = vector.subtract(pn, pofstbl[orientation]) Index: gems.lua ================================================================== --- gems.lua +++ gems.lua @@ -67,11 +67,12 @@ local ctx = { caster = user; target = target; stats = stats; - amulet = stack; + wield = stack; + amulet = stack:get_definition()._sorcery.amulet; meta = stack:get_meta(); -- avoid spell boilerplate color = sorcery.lib.color(sp.tone); today = minetest.get_day_count(); probe = probe; heading = { @@ -83,11 +84,10 @@ }; sound = "xdecor_enchanting"; --FIXME make own sounds sparkle = true; } - print('casting') local res = sp.cast(ctx) if res == nil or res == true then minetest.sound_play(ctx.sound, { pos = user:get_pos(); @@ -101,11 +101,11 @@ if not minetest.check_player_privs(user, 'sorcery:infinirune') then sorcery.amulet.setrune(stack) end end - return ctx.amulet + return ctx.wield end; minetest.register_craftitem(amuletname, { description = sorcery.lib.str.capitalize(name) .. ' amulet'; inventory_image = img_sparkle:blit(img_stone):render(); wield_scale = { x = 0.6, y = 0.6, z = 0.6 }; Index: gravitator.lua ================================================================== --- gravitator.lua +++ gravitator.lua @@ -79,11 +79,14 @@ setmeta(pos,'off') end; on_timer = function(pos) if p.color == nil then return false end - local vee = {x=0,y=-1,z=0}; + local probe = sorcery.spell.probe(pos) + if probe.disjunction then return true end + + local vee = {x=0,y=-1,z=0} minetest.add_particlespawner { amount = 128; time = 4; minpos = vector.subtract(pos,radius); maxpos = vector.add(pos,radius); Index: harvester.lua ================================================================== --- harvester.lua +++ harvester.lua @@ -36,10 +36,13 @@ on_timer = function(pos,elapse) local meta = minetest.get_meta(pos) local inv = meta:get_inventory() if inv:is_empty('charge') then return false end + local probe = sorcery.spell.probe(pos) + if probe.disjunction then return true end + local put_in_hopper = sorcery.lib.node.discharger(pos) local discharge = function(item,idx) inv:set_stack('charge',idx,put_in_hopper(item)) end Index: infuser.lua ================================================================== --- infuser.lua +++ infuser.lua @@ -76,10 +76,13 @@ local inv = meta:get_inventory() local infusion = inv:get_list('infusion') local potions = inv:get_list('potions') local elixir = infusion[1]:get_definition() + local probe = sorcery.spell.probe(pos) + if probe.disjunction then return true end + local potionct = 0 do local ingredient -- *eyeroll* if infusion[1]:is_empty() then goto cancel end Index: lib/image.lua ================================================================== --- lib/image.lua +++ lib/image.lua @@ -53,8 +53,10 @@ transform = function(self, kind) return image.change(self, { fx = sorcery.lib.tbl.append(self.fx, {'transform' .. tostring(kind)}) }) end; + + glow = function(self,color) return self:blit(self:multiply(color)) end; } end; } return image ADDED lib/math.lua Index: lib/math.lua ================================================================== --- lib/math.lua +++ lib/math.lua @@ -0,0 +1,22 @@ +local fn = {} + +fn.vsep = function(vec) -- separate a vector into a direction + magnitude + local magnitude = math.max(math.abs(vec.x), math.abs(vec.y), math.abs(vec.z)) + local inv = 1 / magnitude + return vector.multiply(vec,inv), magnitude +end + +fn.vdcomp = function(dist,v1,v2) -- compare the distance between two points + -- (cheaper than calculating distance outright) + local d if v2 + then d = vector.subtract(v1,v2) + else d = v1 + end + local dsq = (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2) + return dsq / (dist^2) + -- [0,1) == less then + -- 1 == equal + -- >1 == greater than +end + +return fn Index: portal.lua ================================================================== --- portal.lua +++ portal.lua @@ -276,10 +276,11 @@ local dsp = portal_disposition(dev) local crc = portal_circuit(pos) local cap = sorcery.ley.netcaps(pos,delta) local tune = sorcery.attunement.verify(pos) local partner -- raw position of partner node, if any + local probe = sorcery.spell.probe(pos) if tune and tune.partner then minetest.load_area(tune.partner) -- we are attuned to a partner, but is it in the circuit? for _,v in pairs(crc) do if vector.equals(v.pos,tune.partner) then @@ -293,21 +294,24 @@ -- clean out user table for name,user in pairs(portal_context.users) do if user and vector.equals(user.portal, pos) then local found = false - for _,u in pairs(dsp.users) do - if u.object:get_player_name() == name then - found = true + if not probe.disjunction then + for _,u in pairs(dsp.users) do + if u.object:get_player_name() == name then + found = true + end end end if not found then if user.sound then minetest.sound_fade(user.sound,1,0) end portal_context.users[name] = nil end end end + if probe.disjunction then return true end -- one user per pad only! for _,n in pairs(dev.nodes) do for _,u in pairs(dsp.users) do if u.slot == n then Index: runeforge.lua ================================================================== --- runeforge.lua +++ runeforge.lua @@ -49,11 +49,11 @@ }) end) for name,p in pairs(constants.phial_kinds) do local f = string.format - local color = sorcery.lib.color(204,38,235) + local color = sorcery.lib.color(142,232,0) local fac = p.grade / 6 local id = f('phial_%s', name); sorcery.register_potion_tbl { name = id; label = f('%s Phial',p.name); @@ -228,37 +228,47 @@ tone = sorcery.lib.color(rd.tone); base_spell = base_spell; } end - local runeforge_update = function(pos,time) local m = minetest.get_meta(pos) local i = m:get_inventory() local l = sorcery.ley.netcaps(pos,time or 1) + local probe = sorcery.spell.probe(pos) local pow_min = l.self.powerdraw >= l.self.minpower local pow_max = l.self.powerdraw >= l.self.maxpower local has_phial = function() return not i:is_empty('phial') end - if time and has_phial() and pow_min then -- roll for runes + if time and has_phial() and pow_min and not probe.disjunction then -- roll for runes local int, powerfac = calc_phial_props(i:get_stack('phial',1)) local rolls = math.floor(time/int) local newrunes = {} for _=1,rolls do local choices = {} for name,rune in pairs(sorcery.data.runes) do - print('considering',name) - print('-- power',rune.minpower,(rune.minpower*powerfac)*time,'//',l.self.powerdraw,l.self.powerdraw/time,'free',l.freepower,'max',l.maxpower) + -- print('considering',name) + -- print('-- power',rune.minpower,(rune.minpower*powerfac)*time,'//',l.self.powerdraw,l.self.powerdraw/time,'free',l.freepower,'max',l.maxpower) if (rune.minpower*powerfac)*time <= l.self.powerdraw and math.random(rune.rarity) == 1 then - local n = ItemStack(rune.item) - choices[#choices + 1] = n + choices[#choices + 1] = rune + end + end + if #choices > 0 then + -- if multiple runes were rolled up, be nice to the player + -- and pick the rarest one to give them + local rare, choice = 0 + for i,c in pairs(choices) do + if c.rarity > rare then + rare = c.rarity + choice = c + end end + newrunes[#newrunes + 1] = ItemStack(choice.item) end - if #choices > 0 then newrunes[#newrunes + 1] = choices[math.random(#choices)] end - print('rune choices:',dump(choices)) - print('me',dump(l.self)) + -- print('rune choices:',dump(choices)) + -- print('me',dump(l.self)) end for _,r in pairs(newrunes) do if i:room_for_item('cache',r) and has_phial() then local qual = math.random(#constants.rune_grades) @@ -286,12 +296,13 @@ list[current_player;main;0.25,3;8,4;] image[0.25,0.50;1,1;sorcery_statlamp_%s.png] ]], (10.5 - constants.rune_cache_max*1.25)/2, constants.rune_cache_max, - ((has_phial and pow_max) and 'green' ) or - ((has_phial and pow_min) and 'yellow') or 'off') + ((not (has_phial and pow_min)) and 'off' ) or + ( probe.disjunction and 'blue' ) or + ((has_phial and pow_max) and 'green') or 'yellow') local ghost = function(slot,x,y,img) if i:is_empty(slot) then spec = spec .. string.format([[ image[%f,%f;1,1;%s.png] ]], x,y,img) end @@ -448,14 +459,16 @@ if list == 'phial' or list == 'refuse' then return stack:get_count() end return 0 end; allow_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user) local inv = minetest.get_meta(pos):get_inventory() + local probe = sorcery.spell.probe(pos) local wrench if not inv:is_empty('wrench') then wrench = inv:get_stack('wrench',1):get_definition()._proto end if fl == 'cache' then + if probe.disjunction then return 0 end if tl == 'cache' then return 1 end if tl == 'active' and inv:is_empty('active') then print(dump(wrench)) if wrench and wrench.powers.imbue and not inv:is_empty('amulet') then local amulet = inv:get_stack('amulet',1) @@ -476,10 +489,11 @@ end end end end if fl == 'active' then + if probe.disjunction then return 0 end if tl == 'cache' and wrench and (wrench.powers.extract or wrench.powers.purge) then return 1 end end return 0 end; }) ADDED sounds/sorcery_disjoin_bg.ogg Index: sounds/sorcery_disjoin_bg.ogg ================================================================== --- sounds/sorcery_disjoin_bg.ogg +++ sounds/sorcery_disjoin_bg.ogg cannot compute difference between binary files ADDED sounds/sorcery_hurl.ogg Index: sounds/sorcery_hurl.ogg ================================================================== --- sounds/sorcery_hurl.ogg +++ sounds/sorcery_hurl.ogg cannot compute difference between binary files ADDED sounds/sorcery_powerdown.ogg Index: sounds/sorcery_powerdown.ogg ================================================================== --- sounds/sorcery_powerdown.ogg +++ sounds/sorcery_powerdown.ogg cannot compute difference between binary files ADDED spell.lua Index: spell.lua ================================================================== --- spell.lua +++ spell.lua @@ -0,0 +1,369 @@ +-- this file is used to track active spells, for the purposes of metamagic +-- like disjunction. a "spell" is a table consisting of several properties: +-- a "disjoin" function that, if present, is called when the spell is +-- abnormally interrupted, a "terminate" function that calls when the spell +-- completes, a "duration" property specifying how long the spell lasts in +-- seconds, and a "timeline" table that maps floats to functions called at +-- specific points during the function's activity. it can also have a +-- 'delay' property that specifies how long to wait until the spell sequence +-- starts; the spell is however still vulnerable to disjunction during this +-- period. there can also be a sounds table that maps timepoints to sounds +-- the same way timeline does. each value should be a table of form {sound, +-- where}. the `where` field may contain one of 'pos', 'caster', 'subjects', or +-- a vector specifying a position in the world, and indicate where the sound +-- should be played. by default 'caster' and 'subjects' sounds will be attached +-- to the objects they reference; 'attach=false' can be added to prevent this. +-- by default sounds will be faded out quickly when disjunction occurs; this +-- can be controlled by the fade parameter. +-- +-- spells can have various other properties, for instance 'disjunction', which +-- when true prevents other spells from being cast in its radius while it is +-- still in effect. disjunction is absolute; there is no way to overwhelm it. +-- +-- the spell also needs at least one of "anchor", "subjects", or "caster". +-- * an anchor is a position that, in combination with 'range', specifies the area +-- where a spell is in effect; this is used for determining whether it +-- is affected by a disjunction that incorporates part of that position +-- * subjects is an array of individuals affected by the spell. when +-- disjunction is cast on one of them, they will be removed from the +-- table. each entry should have at least a 'player' field; they can +-- also contain any other data useful to the spell. if a subject has +-- a 'disjoin' field it must be a function called when they are removed +-- from the list of spell targets. +-- * caster is the individual who cast the spell, if any. a disjunction +-- against their person will totally disrupt the spell. +local log = function(...) sorcery.log('spell',...) end + +-- FIXME saving object refs is iffy, find a better alternative +sorcery.spell = { + active = {} +} + +local get_spell_positions = function(spell) + local spellpos + if spell.anchor then + spellpos = {spell.anchor} + elseif spell.attach then + if spell.attach == 'caster' then + spellpos = {spell.caster:get_pos()} + elseif spell.attach == 'subjects' or spell.attach == 'both' then + if spell.attach == 'both' then + spellpos = {spell.caster:get_pos()} + else spellpos = {} end + for _,s in pairs(spell.subjects) do + spellpos[#spellpos+1] = s.player:get_pos() + end + else spellpos = {spell.attach:get_pos()} end + else assert(false) end + return spellpos +end + +local inspellrange = function(spell,pos,range) + local spellpos = get_spell_positions(spell) + + for _,p in pairs(spellpos) do + if vector.equals(pos,p) or + (range and sorcery.lib.math.vdcomp(range, pos,p)<=1) or + (spell.range and sorcery.lib.math.vdcomp(spell.range,p,pos)<=1) then + return true + end + end + return false +end + +sorcery.spell.probe = function(pos,range) + -- this should be called before any magical effects are performed. + -- other mods can overlay their own functions to e.g. protect areas + -- from magic + local result = {} + + -- first we need to check if any active injunctions are in effect + -- injunctions are registered as spells with a 'disjunction = true' + -- property + for id,spell in pairs(sorcery.spell.active) do + if not (spell.disjunction and (spell.anchor or spell.attach)) then goto skip end + if inspellrange(spell,pos,range) then + result.disjunction = true + break + end + ::skip::end + + -- at some point we might also check to see if certain anti-magic + -- blocks are nearby or suchlike. there should also be regions where + -- perhaps certain kinds of magic are unusually empowered or weak + -- (perhaps drawing on leyline affinity) + return result +end +sorcery.spell.disjoin = function(d) + local spells,targets = {},{} + if d.spell then spells = {{v=d.spell}} + elseif d.target then targets = {d.target} + elseif d.pos then -- find spells anchored here and people in range + for id,spell in pairs(sorcery.spell.active) do + if not spell.anchor then goto skip end -- this intentionally excludes attached spells + if inspellrange(spell,d.pos,d.range) then + spells[#spells+1] = {v=spell,i=id} + end + ::skip::end + local ppl = minetest.get_objects_inside_radius(d.pos,d.range) + if #targets == 0 then targets = ppl else + for _,p in pairs(ppl) do targets[#targets+1] = p end + end + end + + -- iterate over targets to remove from any spell's influence + for _,t in pairs(targets) do + for id,spell in pairs(sorcery.spell.active) do + if spell.caster == t then spells[#spells+1] = {v=spell,i=id} else + for si, sub in pairs(spell.subjects) do + if sub.player == t then + if sub.disjoin then sub:disjoin(spell) end + spell.release_subject(si) + break + end + end + end + end + end + + -- spells to disjoin entirely + for _,s in pairs(spells) do local spell = s.v + if spell.disjoin then spell:disjoin() end + spell.abort() + if s.i then sorcery.spell.active[s.i] = nil else + for k,v in pairs(sorcery.spell.active) do + if v == spell then sorcery.spell.active[k] = nil break end + end + end + end +end + +-- when a new spell is created, we analyze it and make the appropriate calls +-- to minetest.after to queue up the events. each job returned needs to be +-- saved in 'jobs' so they can be canceled if the spell is disjoined. no polling +-- necessary :D + +sorcery.spell.cast = function(proto) + local s = table.copy(proto) + s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {} + s.impacts = s.impacts or {} s.subjects = s.subjects or {} + s.delay = s.delay or 0 + s.visual = function(def,subj) + s.vfx[#s.vfx + 1] = { + handle = minetest.add_particlespawner(def); + subject = subj; + } + end + s.visual_caster = function(def) -- convenience function + local d = table.copy(def) + d.attached = s.caster + s.visual(d) + end + s.visual_subjects = function(def) + for _,sub in pairs(s.subjects) do + local d = table.copy(def) + d.attached = sub.player + s.visual(d,sub) + end + end + s.affect = function(i) + local etbl = {} + for _,sub in pairs(s.subjects) do + local eff = late.new_effect(sub.player, i) + local rec = { + effect = eff; + subject = sub; + } + s.impacts[#s.impacts+1] = rec + etbl[#etbl+1] = rec + end + return etbl + end + s.abort = function() + for _,j in ipairs(s.jobs) do j:cancel() end + for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end + for _,i in ipairs(s.sfx) do s.silence(i) end + for _,i in ipairs(s.impacts) do i.effect:stop() end + end + s.release_subject = function(si) + local t = s.subjects[si] + print('releasing against',si,t) + for _,f in pairs(s.sfx) do if f.subject == t then s.silence(f) end end + for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end + for _,f in pairs(s.vfx) do + if f.subject == t then minetest.delete_particlespawner(f.handle) end + end + s.subjects[si] = nil + end + local interpret_timespec = function(when) + local t if type(when) == 'number' then + t = s.duration * when + else + t = (s.duration * (when.whence or 0)) + when.secs + end + if t then return math.min(s.duration,math.max(0,t)) end + + log('invalid timespec ' .. dump(when)) + return 0 + end + s.queue = function(when,fn) + local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0 + local timepast = interpret_timespec(when) + if not timepast then timepast = 0 end + local timeleft = s.duration - timepast + local howlong = (s.delay + timepast) - elapsed + if howlong < 0 then + log('cannot time-travel! queue() called with `when` specifying timepoint that has already passed') + howlong = 0 + end + s.jobs[#s.jobs+1] = minetest.after(howlong, function() + -- this is somewhat awkward. since we're using a non-polling approach, we + -- need to find a way to account for a caster or subject walking into an + -- existing antimagic field, or someone with an existing antimagic aura + -- walking into range of the anchor. so every time a spell effect would + -- take place, we first check to see if it's in range of something nasty + if not s.disjunction and -- avoid self-disjunction + (s.caster and sorcery.spell.probe(s.caster:get_pos()).disjunction) or + (s.anchor and sorcery.spell.probe(s.anchor,s.range).disjunction) then + sorcery.spell.disjoin{spell=s} + else + if not s.disjunction then for _,sub in pairs(s.subjects) do + local sp = sub.player:get_pos() + if sorcery.spell.probe(sp).disjunction then + sorcery.spell.disjoin{pos=sp} + end + end end + -- spell still exists and we've removed any subjects who have been + -- affected by a disjunction spell, it's now time to actually perform + -- the queued-up action + fn(s,timepast,timeleft) + end + end) + end + s.play_now = function(spec) + local specs, stbl = {}, {} + local addobj = function(obj,sub) + if spec.attach == false then specs[#specs+1] = { + spec = { pos = obj:get_pos() }; + obj = obj, subject = sub; + } else specs[#specs+1] = { + spec = { object = obj }; + obj = obj, subject = sub; + } end + end + + if spec.where == 'caster' then addobj(s.caster) + elseif spec.where == 'subjects' then + for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end + elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} } + else specs[#specs+1] = { spec = {pos = spec.where} } end + + for _,sp in pairs(specs) do + sp.spec.gain = spec.gain + local so = { + handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral); + ctl = spec; + -- object = sp.obj; + subject = sp.subject; + } + stbl[#stbl+1] = so + s.sfx[#s.sfx+1] = so + end + return stbl + end + s.play = function(when,spec) + s.queue(when, function() + local snds = s.play_now(spec) + if spec.stop then + s.queue(spec.stop, function() + for _,snd in pairs(snds) do s.silence(snd) end + end) + end + end) + end + s.silence = function(sound) + if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle) + else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end + end + local startqueued, termqueued = false, false + local myid = #sorcery.spell.active+1 + s.cancel = function() + s.abort() + sorcery.spell.active[myid] = nil + end + local perform_disjunction_calls = function() + local positions = get_spell_positions(s) + for _,p in pairs(positions) do + sorcery.spell.disjoin{pos = p, range = s.range} + end + end + if s.timeline then + for when_raw,what in pairs(s.timeline) do + local when = interpret_timespec(when_raw) + if s.delay == 0 and when == 0 then + startqueued = true + if s.disjunction then perform_disjunction_calls() end + what(s,0,s.duration) + elseif when_raw == 1 or when >= s.duration then -- avoid race conditions + if not termqueued then + termqueued = true + s.queue(1,function(s,...) + what(s,...) + if s.terminate then s:terminate() end + sorcery.spell.active[myid] = nil + end) + else + log('multiple final timeline events not possible, ignoring') + end + elseif when == 0 and s.disjunction then + startqueued = true + s.queue(when_raw,function(...) + perform_disjunction_calls() + what(...) + end) + else s.queue(when_raw,what) end + end + end + if s.intervals then + for _,int in pairs(s.intervals) do + local timeleft = s.duration - interpret_timespec(int.after) + local iteration, itercount = 0, timeleft / int.period + local function iterate(lastreturn) + iteration = iteration + 1 + local nr = int.fn { + spell = s; + iteration = iteration; + iterationcount = itercount; + timeleft = timeleft; + timeelapsed = s.duration - timeleft; + lastreturn = lastreturn; + } + if nr ~= false and iteration < itercount then + s.jobs[#s.jobs+1] = minetest.after(int.period, + function() iterate(nr) end) + end + end + if int.after + then s.queue(int.after, iterate) + else s.queue({whence=0, secs=s.period}, iterate) + end + end + end + if s.disjunction and not startqueued then + if s.delay == 0 then perform_disjunction_calls() else + s.queue(0, function() perform_disjunction_calls() end) + end + end + if s.sounds then + for when,what in pairs(s.sounds) do s.play(when,what) end + end + sorcery.spell.active[myid] = s + if not termqueued then + s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function() + if s.terminate then s:terminate() end + sorcery.spell.active[myid] = nil + end) + end + s.starttime = minetest.get_server_uptime() + return s +end Index: textures/sorcery_crackle.png ================================================================== --- textures/sorcery_crackle.png +++ textures/sorcery_crackle.png cannot compute difference between binary files ADDED textures/sorcery_flicker.png Index: textures/sorcery_flicker.png ================================================================== --- textures/sorcery_flicker.png +++ textures/sorcery_flicker.png cannot compute difference between binary files ADDED textures/sorcery_fog.png Index: textures/sorcery_fog.png ================================================================== --- textures/sorcery_fog.png +++ textures/sorcery_fog.png cannot compute difference between binary files ADDED textures/sorcery_glitter.png Index: textures/sorcery_glitter.png ================================================================== --- textures/sorcery_glitter.png +++ textures/sorcery_glitter.png cannot compute difference between binary files ADDED textures/sorcery_poof.png Index: textures/sorcery_poof.png ================================================================== --- textures/sorcery_poof.png +++ textures/sorcery_poof.png cannot compute difference between binary files ADDED textures/sorcery_sparking.png Index: textures/sorcery_sparking.png ================================================================== --- textures/sorcery_sparking.png +++ textures/sorcery_sparking.png cannot compute difference between binary files ADDED textures/sorcery_sputter.png Index: textures/sorcery_sputter.png ================================================================== --- textures/sorcery_sputter.png +++ textures/sorcery_sputter.png cannot compute difference between binary files Index: tnodes.lua ================================================================== --- tnodes.lua +++ tnodes.lua @@ -20,10 +20,15 @@ on_timer = function(pos,dtime) local meta = minetest.get_meta(pos) local elapsed = dtime + meta:get_float('duration') - meta:get_float('timeleft') local level = 1 - (elapsed / meta:get_float('duration')) local lum = math.ceil(level*meta:get_int('power')) + local probe = sorcery.spell.probe(pos) + if probe.disjunction then + minetest.remove_node(pos) + return false + end if lum ~= i then if lum <= 0 then minetest.remove_node(pos) return false else