Index: data/runes.lua ================================================================== --- data/runes.lua +++ data/runes.lua @@ -2,10 +2,44 @@ -- 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 }; + 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(); + animation = { + type = 'vertical_frames'; + aspect_w = 16, aspect_h = 16; + }; + } +end +local sparktrail = function(fn,tgt,color) + return (fn or minetest.add_particlespawner)({ + amount = 240, time = 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; + minsize = 0.5, maxsize = 2.6, glow = 14; + texture = sorcery.vfx.glowspark(color):render(); + animation = { + type = 'vertical_frames', length = 5.1; + aspect_w = 16, aspect_h = 16; + }; + }); +end return { translocate = { name = 'Translocate'; tone = {0,235,233}; minpower = 3; @@ -50,60 +84,57 @@ 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) - print('teledelay',delay,ctx.stats.power) 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', { - object = s, gain = 0.8; - },true) - local windup = minetest.sound_play('sorcery_windup',{ - object = s, gain = 0.4; - }) + -- minetest.sound_play('sorcery_stutter', { + -- object = s, gain = 0.8; + -- },true) local mydelay = delay + math.random(-10,10)*.1; - local spark = sorcery.lib.image('sorcery_spark.png') local sh = s:get_properties().eye_height - local sparkle = function(amt,time,minsize,maxsize) - minetest.add_particlespawner { - 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 }; - 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 = spark:blit(spark:multiply(sorcery.lib.color(29,205,247))):render(); - animation = { - type = 'vertical_frames'; - aspect_w = 16, aspect_h = 16; + local color = sorcery.lib.color(29,205,247) + sorcery.lib.node.preload(pt,s) + sorcery.spell.cast { + duration = mydelay; + caster = ctx.caster; + subjects = {{player=s,dest=pt}}; + timeline = { + [0] = function(sp,_,timeleft) + sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh) + sp.windup = (sp.play_now{ + sound = 'sorcery_windup'; + where = 'subjects'; + gain = 0.4; + fade = 1.5; + })[1] + end; + [0.4] = function(sp,_,timeleft) + sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh) + end; + [0.7] = function(sp,_,timeleft) + sparkle(color,sp,timeleft*80, timeleft, 2,4, sh) + end; + [1] = function(sp) + sp.silence(sp.windup) + minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true) + minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true) + sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos()) + s:set_pos(pt) + sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2) + end; + }; + sounds = { + [0] = { + pos = 'subjects'; + sound = 'sorcery_stutter'; }; - } - end - sparkle(mydelay*100,mydelay,0.3,1.3) - minetest.after(mydelay*0.4, function() - local timeleft = mydelay - (mydelay*0.4) - sparkle(timeleft*150, timeleft, 0.6,1.8) - end) - minetest.after(mydelay*0.7, function() - local timeleft = mydelay - (mydelay*0.7) - sparkle(timeleft*80, timeleft, 2,4) - end) - sorcery.lib.node.preload(pt,s) - minetest.after(mydelay, function() - minetest.sound_stop(windup) - minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true) - minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true) - sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos()) - s:set_pos(pt) - sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2) - end) + }; + } end end end end; frame = { @@ -164,17 +195,81 @@ name = 'Disjoin'; tone = {159,235,0}; minpower = 4; rarity = 20; amulets = { - amethyst = { + 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'; + }; + amethyst = { + name = 'Purging'; + desc = 'Free yourself from the grip of any malicious spellwork with a snap of your fingers — interrupting all of your own active spells in the process, including impending translocations'; + cast = function(ctx) + local h = ctx.heading.eyeheight * 1.1 + minetest.add_particlespawner { + time = 0.2, amount = math.random(200,250), attached = ctx.caster; + glow = 14, texture = sorcery.vfx.glowspark(sorcery.lib.color(156,255,10)):render(); + minpos = {x = -0.3, y = -0.5, z = -0.3}; + maxpos = {x = 0.3, y = h, z = 0.3}; + minvel = {x = -1.8, y = -1.8, z = -1.8}; + maxvel = {x = 1.8, y = 1.8, z = 1.8}; + minsize = 0.2, maxsize = 5; + animation = { + type = 'vertical_frames', length = 4.1; + aspect_w = 16, aspect_h = 16; + }; + minexptime = 2, maxexptime = 4; + } + minetest.sound_play('sorcery_disjoin',{object=ctx.caster},true) + sorcery.spell.disjoin{target=ctx.caster} + end; }; emerald = { + name = 'Disjunction Field'; + desc = 'Render an area totally opaque to spellwork for a period of time, disrupting any existing spells and preventing further spellcasting therein'; + }; + ruby = { + name = 'Disjunction'; + desc = 'Wield this amulet against a spellcaster to disrupt and abort all their spells in progress, perhaps to trap a foe intent on translocating away, or unleash its force upon the victim of a malign hex to free them from its clutches'; + frame = { + iridium = { + name = 'Nullification'; + desc = 'Not only will your victim\'s spells be nullified, but all enchanted objects they carry will be stripped of their power — or possibly even destroyed outright'; + }; + }; + }; + luxite = { + name = 'Disjunctive Aura'; + desc = 'For a time, all magic undertaken in your vicinity will fail totally'; + cast = function(ctx) + sorcery.spell.cast { + caster = ctx.caster, attach = '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 + }; + sounds = { + [0] = { sound = 'sorcery_disjoin', pos = 'caster' }; + [1] = { sound = 'sorcery_powerdown', pos = 'caster' }; + }; + } + end + }; + diamond = { name = 'Mundanity'; desc = 'Strip away the effects of all active potions and spells in your immediate vicinity, leaving adversaries without their magicks to enhance and protect them, and allies free of any curses they may be hobbled by -- and, of course, vice versa'; + frame = { + iridium = { + name = 'Spellshatter'; + desc = 'Blast out a tidal wave of anti-magic that will nullify active spells, but also disenchant or destroy all magical items in range of its violently mundane grip'; + }; + }; }; } }; repulse = { name = 'Repulse'; @@ -183,10 +278,60 @@ rarity = 7; 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 + if minetest.is_player(tgt) or immortal == 0 then + tgt:punch(ctx.caster, 1, { + full_punch_interval = 1; + damage_groups = { fleshy = force / 10 }; + }) + end + sparktrail(nil,tgt,sorcery.lib.color(101,226,255)) + if dir.y > 0 then dir.y = 0 end -- spell always lifts + dir = vector.add(dir, {x=0,z=0,y=-0.25}) + local vel = vector.multiply(dir,0-force) + tgt:add_velocity(vel) + end; + }; + ruby = { + name = 'Liftoff'; + desc = 'Lift yourself high into the air with a blast of violent repulsive force against the ground, and drift down safely to a position of your choice'; + cast = function(ctx) + 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; + 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; + raise = 2; + fall = (power * 0.25) * 0.3; + impacts = { + gravity = 0.1; + }; + } + 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'; }; @@ -214,11 +359,11 @@ name = 'Sapping'; desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention'; }; ruby = { name = 'Shattering'; - desc = 'Tear a violent wound in the earth with the destructive force of this amulet'; + desc = 'Tear a violent wound in the land with the destructive force of this amulet'; }; emerald = { name = 'Detonate'; desc = 'Wielding this amulet, you can loose an extraordinarily powerful bolt of flame from your fingertips that will explode violently on impact, wreaking total havoc wherever it lands'; cast = function(ctx) @@ -235,10 +380,14 @@ }; 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'; }; + 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'; + }; 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.'; cast = function(ctx) Index: entities.lua ================================================================== --- entities.lua +++ entities.lua @@ -5,20 +5,23 @@ } minetest.register_entity('sorcery:spell_projectile_flamebolt',{ initial_properties = { visual = "sprite"; - -- use_texture_alpha = true; + use_texture_alpha = true; textures = {'sorcery_fireball.png'}; - groups = {immortal = 1}; visual_size = { x = 2, y = 2, z = 2 }; physical = true; collide_with_objects = true; pointable = false; glow = 14; static_save = false; + shaded = false; }; + on_activate = function(self) + self.object:set_armor_groups{immortal = 1} + end; on_step = function(self,dtime,collision) local pos = self.object:get_pos() if not self._meta then self._meta = { age = 0; lastemit = 0; emitters = {} } goto emit @@ -28,10 +31,17 @@ if self._meta.age >= 6 then goto destroy elseif (self._meta.age - self._meta.lastemit) < 3 then goto collcheck end + + -- fireballs dissipate when entering antimagic fields + do local probe = sorcery.spell.probe(self.object:get_pos()) + if probe.disjunction and not self._meta.ignore_disjunction then + sorcery.vfx.cast_sparkle(nil,sorcery.lib.color(255,90,10),3,0.3,self.object:get_pos()) + goto destroy + end end ::emit:: do self._meta.lastemit = self._meta.age local spawn = function(num, life_min, life_max, size_min, size_max, gl, speed, img) table.insert(self._meta.emitters, minetest.add_particlespawner { Index: gems.lua ================================================================== --- gems.lua +++ gems.lua @@ -58,19 +58,24 @@ local img_sparkle = img('sorcery_amulet_sparkle.png') local useamulet = function(stack,user,target) local sp = sorcery.amulet.getspell(stack) if not sp or not sp.cast then return nil end local stats = sorcery.amulet.stats(stack) + local probe = sorcery.spell.probe(user:get_pos()) + -- amulets don't work in antimagic fields, though some may want to + -- implement this logic themselves (for instance to check a range) + if (probe.disjunction and not sp.ignore_disjunction) then return nil end local ctx = { caster = user; target = target; stats = stats; amulet = stack; meta = stack:get_meta(); -- avoid spell boilerplate color = sorcery.lib.color(sp.tone); today = minetest.get_day_count(); + probe = probe; heading = { pos = user:get_pos(); yaw = user:get_look_dir(); pitch = user:get_look_vertical(); angle = user:get_look_horizontal(); Index: init.lua ================================================================== --- init.lua +++ init.lua @@ -82,11 +82,11 @@ sorcery.stage('bootstrap',data,root) data {'ui'} sorcery.unit('lib') { -- convenience - 'str'; + 'str', 'math'; -- serialization 'marshal', 'json'; -- data structures 'tbl', 'class'; -- wrappers @@ -120,11 +120,11 @@ end end sorcery.stage('startup',data) for _,u in pairs { - 'vfx'; 'attunement'; 'context'; 'itemclass'; + 'vfx'; 'attunement'; 'context'; 'itemclass'; 'spell'; 'potions'; 'metal', 'gems'; 'leylines'; 'infuser'; 'altar'; 'wands'; 'tools', 'crafttools'; 'enchanter'; 'harvester'; 'metallurgy-hot', 'metallurgy-cold'; 'entities'; 'recipes'; 'coins'; 'interop'; 'tnodes'; 'forcefield'; 'farcaster'; 'portal'; Index: lib/tbl.lua ================================================================== --- lib/tbl.lua +++ lib/tbl.lua @@ -88,14 +88,10 @@ new[#new + 1] = r2[i] end return new end -fn.capitalize = function(str) - return string.upper(string.sub(str, 1,1)) .. string.sub(str, 2) -end - fn.has = function(tbl,value,eqfn) for k,v in pairs(tbl) do if eqfn then if eqfn(v,value,tbl) then return true, k end else @@ -144,13 +140,11 @@ return f(tbl[k],k,i) end) end fn.iter = function(tbl,fn) - for i=1,#tbl do - fn(tbl[i], i) - end + for i,v in ipairs(tbl) do fn(v, i) end end fn.map = function(tbl,fn) local new = {} for k,v in pairs(tbl) do @@ -164,12 +158,12 @@ if #tbl == 0 then fn.each_o(tbl, function(v) acc = fn(acc, v, k) end) else - for i=0,#tbl do - acc = fn(acc,tbl[i],i) + for i,v in ipairs(tbl) do + acc = fn(acc,v,i) end end return acc end Index: sorcery.md ================================================================== --- sorcery.md +++ sorcery.md @@ -18,10 +18,13 @@ * **instant_ores** for ore generation. temporary, will be removed and replaced with home-grown mechanism soon * **farming redo** for potion ingredients * **late** for spell, potion, and gravitator effects * **note**: in order for the gravitator to work, the late condition interval must be lowered from its default of 1.0 to 0.1. this currently can only be done by altering a variable at the top of `late/conditions.lua`, though a note in the source suggests a configuration option will be added eventually. hopefully this is so. +## libraries + * **luajit**, because `sorcery`'s code uses modern features not available in the ancient lua version bundled with minetest. alternately, it may be possible to build minetest against a more recent lua version if you're feeling masochistic; luajit will probably be faster tho and has first-party support + # interoperability sorcery has special functionality to ensure it can cooperate with various other modules, although they are not necessarily required for it to function. ## xdecor by default, `sorcery` disables the xdecor enchanter, since `sorcery` offers its own, much more sophisticated enchantment mechanism. however, the two can coexist if you really want; a configuration flag can be used to prevent `sorcery` disabling the xdecor enchanter. Index: vfx.lua ================================================================== --- vfx.lua +++ vfx.lua @@ -1,6 +1,11 @@ sorcery.vfx = {} + +sorcery.vfx.glowspark = function(color) + local spark = sorcery.lib.image('sorcery_spark.png') + return spark:blit(spark:multiply(color)) +end sorcery.vfx.cast_sparkle = function(caster,color,strength,duration,pos) local ofs = pos and function(x) return vector.add(pos,x) end or function(x) return x end Index: wands.lua ================================================================== --- wands.lua +++ wands.lua @@ -209,11 +209,17 @@ local wand_cast = function(stack, user, target) local meta = stack:get_meta() local wand = sorcery.wands.util.getproto(stack) if meta:contains('sorcery_wand_spell') == false then return nil end local spell = meta:get_string('sorcery_wand_spell') - local castfn = sorcery.data.spells[spell].cast + local spelldata = sorcery.data.spells[spell] + + -- wands don't work in anti-magic fields + local probe = sorcery.spell.probe(user:get_pos()) + if probe.disjunction and not spelldata.ignore_disjunction then return nil end + + local castfn = spelldata.cast if castfn == nil then return nil end local matprops = sorcery.wands.util.matprops(wand) if matprops.bond then local userct, found = 0, false for i=1,matprops.bond do @@ -255,10 +261,11 @@ stats = matprops; meta = meta; item = stack; caster = user; target = target; + probe = probe; today = minetest.get_day_count(); heading = { pos = user:get_pos(); yaw = user:get_look_dir(); pitch = user:get_look_vertical();