Index: data/runes.lua ================================================================== --- data/runes.lua +++ data/runes.lua @@ -45,26 +45,102 @@ type = 'vertical_frames', length = 5.1; aspect_w = 16, aspect_h = 16; }; }); end + +local teleport = function(ctx,subjects,delay,pos,color) + if ctx.amulet.frame == 'tungsten' then delay = delay * 0.5 end + color = color or sorcery.lib.color(29,205,247) + local center = ctx.caster:get_pos() + for _,sub in pairs(subjects) do + local s = sub.ref + 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 mydelay = sub.delay or (delay + math.random(-10,10)*.1); + local sh = s:get_properties().eye_height + local color = sub.color or color + sorcery.lib.node.preload(pt,s) + sorcery.spell.cast { + name = 'sorcery:translocate'; + duration = mydelay; + caster = ctx.caster; + subjects = {{player=s,dest=sub.dest or 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,color:brighten(1.3),2,s:get_pos()) + s:set_pos(pt) + sorcery.vfx.body_sparkle(s,color:darken(0.3),2) + end; + }; + sounds = { + [0] = { sound = 'sorcery_stutter', pos = 'subjects' }; + }; + } + end + end +end return { translocate = { name = 'Translocate'; tone = {0,235,233}; minpower = 3; - rarity = 10; + rarity = 7; 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'; + desc = 'Give this amulet to another and with a snap of their fingers they can arrive safely at your side from anywhere in the world — though returning whence they came may be a more difficult matter'; apply = function(ctx) local maker = ctx.user:get_player_name() ctx.meta:set_string('rune_join_target',maker) end; remove = function(ctx) ctx.meta:set_string('rune_join_target','') end; + cast = function(ctx) + local target = minetest.get_player_by_name(ctx.meta:get_string('rune_join_target')) + if not target then return false end + + local subjects if ctx.amulet.frame == 'cobalt' then + if ctx.target.type ~= 'object' then return false end + subjects = {{ref=ctx.target.ref}} + else subjects = {{ref=ctx.caster}} end + + local delay = math.max(5,11 - ctx.stats.power) + 2.3*(math.random()*2-1) + local color = sorcery.lib.color(117,38,237) + teleport(ctx,subjects,delay,target:get_pos(),color) + if ctx.amulet.frame == 'gold' then + teleport(ctx,{{ref=target}},delay,ctx.caster:get_pos()) + else + ctx.sparkle = false + end + end; frame = { + tungsten = { + name = 'Quick Joining'; + desc = 'Give this amulet to another and they can arrive safely at your side almost instantaneously from anywhere in the world — though returning whence they came may be a more difficult matter'; + }; gold = { name = 'Exchange'; desc = 'Give this amulet to another and they will be able to trade places with you no matter where in the world each of you might be.'; }; cobalt = { @@ -93,57 +169,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', { - -- object = s, gain = 0.8; - -- },true) - local mydelay = delay + math.random(-10,10)*.1; - local sh = s:get_properties().eye_height - 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] = { sound = 'sorcery_stutter', pos = 'subjects' }; - }; - } - end - end + teleport(ctx,{{ref=ctx.caster}},delay,pos) end end; frame = { tungsten = { name = 'Quick Return'; @@ -165,23 +195,54 @@ }; }; }; ruby = minetest.get_modpath('beds') and { name = 'Escape'; - desc = 'Immediately transport yourself out of a dangerous situation back to the last place you slept'; + desc = 'Immediately transport yourself out of a dangerous situation back to the last place you slept, before anyone has time to net you in a disjunction'; cast = function(ctx) -- if not beds.spawns then beds.read_spawns() end local subjects = {ctx.caster} for _,s in pairs(subjects) do local spp = beds.spawn[ctx.caster:get_player_name()] if spp then local oldpos = s:get_pos() - minetest.sound_play('sorcery_splunch', {pos=oldpos}, true) - sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,131),2,oldpos) - s:set_pos(spp) - minetest.sound_play('sorcery_splunch', {pos=spp}, true) - sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,89),2,spp) + local jump = function() + minetest.sound_play('sorcery_splunch', {pos=oldpos}, true) + sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,131),2,oldpos) + s:set_pos(spp) + minetest.sound_play('sorcery_splunch', {pos=spp}, true) + sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,89),2,spp) + end + if ctx.amulet.frame == 'cobalt' then + sorcery.spell.cast { + name = 'sorcery:escape'; + caster = s; + duration = random() * 0.4 + 0.3; + timeline = { + [0] = function() + sorcery.vfx.imbue(sorcery.lib.color(244,38,131), s, 1.3) + end; + [1] = function(sp) + local radius = 6 * ctx.stats.power + local center = sp.caster:get_pos() + local targets = minetest.get_objects_inside_radius(center, radius) + jump() + -- TODO: shockwave visuals + for _,o in pairs(targets) do + if not o:get_armor_groups().immortal then + local distance = vector.distance(o:get_pos(), center) + local dmg = (7 * ctx.stats.power) * (distance / radius) + minetest.punch(ctx.caster, 1.0, { + full_punch_interval = 1.0; + damage_groups = { fleshy = dmg }; + }, vector.direction(o:get_pos(), center)); + end + end + end; + } + } + else jump() end end -- TODO decide what happens to the people who don't have -- respawn points already set end end; @@ -197,18 +258,135 @@ }; }; diamond = { name = 'Elevation'; desc = 'Lift yourself and everything around you high up into the sky'; + cast = function(ctx) + local center = ctx.caster:get_pos() + local up = ((ctx.stats.power * 7) + math.random(6,17)) * (math.random() * 0.4 + 0.4) + if center.y > 0 then up = up + center.y end + local newcenter = vector.new(center.x,up,center.z) + if not sorcery.lib.node.get_arrival_point(newcenter) then return false end + sorcery.lib.node.preload(newcenter,ctx.caster) + local jmpcolor = sorcery.lib.color(0,255,144) + + if not ctx.amulet.frame == 'iridium' then + local where = vector.offset(center,0,1,0) + repeat local ok, nx = minetest.line_of_sight(where, newcenter) + if ok then break end + if minetest.get_node_or_nil(nx) == nil then + minetest.load_area(nx) + where = nx -- save some time + else return false end + until false + end + local lift = function(n) + local dest = vector.new(n.pos.x, up + n.h, n.pos.z) + if sorcery.lib.node.is_clear(dest) then + minetest.set_node(dest, minetest.get_node(n.pos)) + minetest.get_meta(dest):from_table(minetest.get_meta(n.pos):to_table()) + if math.random(5) == 1 then + minetest.set_node(n.pos, {name='sorcery:air_flash_' .. tostring(math.random(10))}) + else minetest.remove_node(n.pos) end + local obs = minetest.get_objects_inside_radius(n.pos, 1.5) + if obs then for _,o in pairs(obs) do + local pt = sorcery.lib.node.get_arrival_point(vector.add(dest, vector.subtract(o:get_pos(),n.pos))) + if pt then + o:set_pos(pt) + sorcery.vfx.body_sparkle(o,jmpcolor:darken(0.3),2) + end + end end + return true + else + return false + end + end + local nodes,sparkles,tmap = {},{},{} + local r = math.ceil((ctx.stats.power * 0.1) * 8 + 3) + for x = -r,r do -- lazy hack to select a sphere + for z = -r,r do + local col = {} + for y = -r,r do + local ofs = vector.new(x,y,z) + if sorcery.lib.math.vdcomp(r,ofs) <= 1 then + local pos = vector.add(center, ofs) + if sorcery.lib.node.is_air(pos) then + if y > 0 then + sparkles[#sparkles+1] = pos + break -- levitation is a sin + end + else + nodes[#nodes+1] = {pos=pos, h=y} + col[#col+1] = {pos=pos, h=y} + end + end + end + if #col > 0 then + local seq = math.floor(math.sqrt((x^2) + (z^2))) + -- TODO find a way to optimise this shitshow + if tmap[seq] + then tmap[seq][#(tmap[seq])+1] = col + else tmap[seq] = {col} + end + end + end end + + -- for _,n in pairs(nodes) do + -- local dest = vector.new(n.pos.x, up + n.h, n.pos.z) + -- if sorcery.lib.node.is_clear(dest) then + -- minetest.set_node(dest, minetest.get_node(n.pos)) + -- minetest.get_meta(dest):from_table(minetest.get_meta(n.pos):to_table()) + -- if math.random(5) == 1 then + -- minetest.set_node(n.pos, {name='sorcery:air_flash_' .. tostring(math.random(10))}) + -- else minetest.remove_node(n.pos) end + -- end + -- end + local timeline, sounds = { + [0] = function(s) + -- sorcery.vfx.imbue(jmpcolor,s.caster,1) + end; + }, {}; + local time = 0; + for i=0,#tmap do + local cols = tmap[i] + if cols ~= nil then + time = time + math.random()*0.2 + 0.1 + local wh = {whence=0,secs=2+time} + timeline[wh] = function(sp) + for _,col in pairs(cols) do + for _,n in pairs(col) do lift(n) end + end + end + sounds[wh] = { + sound = 'sorcery_zap'; + gain = math.random() + 0.1; + where = cols[1][1].pos; + } + end + end + sorcery.spell.cast { + name = 'sorcery:elevate'; + caster = ctx.caster; + anchor = center, radius = r; + duration = 2 + time; + timeline = timeline, sounds = sounds; + } + end; + frame = { + iridium = { + name = 'Ascension'; + desc = 'Transport yourself and your surroundings high into the heavens, even if you are deep in the bowels of the earth'; + }; + }; }; }; }; disjoin = { name = 'Disjoin'; tone = {159,235,0}; minpower = 4; - rarity = 40; + rarity = 34; 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'; }; @@ -253,10 +431,11 @@ name = 'Disjunctive Aura'; 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 { + name = 'sorcery:disjunctive-aura'; caster = ctx.caster, attach = 'caster'; subjects = {{player=ctx.caster}}; disjunction = true, range = 4 + ctx.stats.power; duration = 10 + ctx.stats.power * 3; timeline = { @@ -350,21 +529,28 @@ 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) + + local oldsp = sorcery.spell.ensorcelled(ctx.caster, 'sorcery:liftoff') + if oldsp then oldsp:cancel() end + sorcery.spell.cast { + name = 'sorcery:liftoff'; caster = ctx.caster; subjects = {{player=ctx.caster}}; 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} + ctx.caster:add_velocity{y=power*1.2;x=0,z=0} + end; + [{whence=0, secs=1}] = function(s) s.affect { duration = power * 0.50; - raise = 2; + raise = 0.5; -- fall = (power * 0.25) * 0.3; impacts = { gravity = 0.1; }; } @@ -398,48 +584,27 @@ 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 { + name = 'sorcery:flinging'; 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; - }; + sorcery.vfx.imbue(function() return + sorcery.lib.color { + hue = math.random(41,63); + saturation = 100; + luminosity = 0.5 + math.random()*0.3; } - end + end, sub.player) end end; - [0.3] = function(s,te,tl) + [{whence=0, secs=1}] = 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; @@ -491,11 +656,11 @@ }; obliterate = { name = 'Obliterate'; tone = {255,0,10}; minpower = 5; - rarity = 35; + rarity = 30; amulets = { amethyst = { name = 'Sapping'; desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention'; }; @@ -550,11 +715,11 @@ }; excavate = { name = 'Excavate'; tone = {0,68,235}; minpower = 3; - rarity = 30; + rarity = 17; 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'; }; @@ -653,10 +818,11 @@ } tp = tp + (math.random(2,5) * 0.1) end sounds[1] = {sound='sorcery_powerdown', where='pos'} sorcery.spell.cast { + name = 'sorcery:excavate'; caster = ctx.caster; duration = tp; timeline = timeline, sounds = sounds; -- spell state anchor = ctx.target.under; @@ -677,16 +843,129 @@ }; genesis = { name = 'Genesis'; tone = {235,0,175}; minpower = 5; - rarity = 25; + rarity = 23; amulets = { mese = { mingrade = 4; name = 'Duplication'; - desc = 'Generate a copy of any object or item, no matter how common or rare'; + desc = 'Bring an exact twin of any object or item into existence, no matter how common or rare it might be'; + cast = function(ctx) + local color = sorcery.lib.color(255,61,205) + local dup, sndpos, anchor, sbj, ty + if ctx.target.type == 'object' and ctx.target.ref:get_luaentity().name == '__builtin:item' then + sorcery.vfx.imbue(color, ctx.target.ref) + sndpos = 'subjects' + sbj = {{player = ctx.target.ref}} + local item = ItemStack(ctx.target.ref:get_luaentity().itemstring) + local r = function() return math.random() * 2 - 1 end + local putpos = vector.offset(ctx.target.ref:get_pos(), r(), 1, r()) + dup = function() + item:set_count(1) -- nice try bouge-san + return minetest.add_item(putpos, item), false + end + elseif ctx.target.type == 'node' then + ty = minetest.get_node(ctx.target.under).name + sorcery.vfx.imbue(color, ctx.target.under) + sndpos = 'pos'; + anchor = ctx.target.under; + dup = function() + local origmeta = minetest.get_meta(ctx.target.under):to_table() + origmeta.inventory = nil + local npos + do local vp = {} + for _, of in pairs(sorcery.lib.node.offsets.neighbors) do + local sum = vector.add(ctx.target.under, of) + if sorcery.lib.node.is_clear(sum) then + vp[#vp+1] = sum + end + end + if #vp > 0 then npos=vp[math.random(#vp)] end + end + if npos then + minetest.set_node(npos, minetest.get_node(ctx.target.under)) + minetest.get_meta(npos):from_table(origmeta) + return npos, true + else + local nstack = ItemStack(ty) + nstack:get_meta():from_table(origmeta) + local leftover = ctx.caster:get_inventory():add_item('main',nstack) + if leftover and not leftover.is_empty() then + minetest.add_item(ctx.caster:get_pos(), leftover) + end + end + end + else + return false + end + if minetest.get_item_group(ty,'do_not_duplicate') ~= 0 then + return true + end + + sorcery.spell.cast { + name = 'sorcery:duplicate'; + caster = ctx.caster; + duration = math.random(10,20) * ((10 - ctx.stats.power)*0.1); + anchor = anchor; + timeline = { + [{whence=0, secs=1}] = function(s,te,tl) + local mag = sbj and 0.5 or 0.7 + local pv = sbj and vector.new(0,0,0) or ctx.target.under + local vfn = (sbj and s.visual_subjects or s.visual) + vfn { + amount = tl * 30, time = tl; + minpos = vector.offset(pv,-mag,-mag,-mag); + maxpos = vector.offset(pv, mag, mag, mag); + minsize = 0.5, maxsize = 2.3; + minexptime = 1.0, maxexptime = 1.5; + texture = sorcery.lib.image('sorcery_sputter.png'):glow(color):render(); + animation = { + type = 'vertical_frames', length = 1.6; + aspect_w = 16, aspect_h = 16; + }; + } + end; + [1] = function(s,te) + local where, node = dup() + if where == nil then return end + local pv = node and where or vector.new(0,0,0) + local mp = (not node) and vector.new(0,0,0) or { + x = 0.5, y = 0.5, z = 0.5 + } + minetest.add_particlespawner { + amount = 170, time = 0.2; + minpos = vector.subtract(pv,mp); + maxpos = vector.add(pv,mp); + attached = (not node) and where or nil; + minvel = {x = -2.0, y = -1.8, z = -2.0}; + maxvel = {x = 2.0, y = 0.2, z = 2.0}; + minacc = {x = -0.0, y = -0.1, z = -0.0}; + maxacc = {x = 0.0, y = -0.3, z = 0.0}; + minsize = 0.3, maxsize = 2; + minexptime = 1, maxexptime = 3.0; + texture = sorcery.lib.image('sorcery_spark.png'):glow(color):render(); + animation = { + type = 'vertical_frames', length = 3.1; + aspect_w = 16, aspect_h = 16; + }; + } + end; + }; + sounds = { + [0] = { + sound = 'sorcery_duplicate_bg'; + where = sndpos, stop = 1, fade = 2; + }; + [1] = { + sound = 'sorcery_genesis'; + where = sndpos, ephemeral = true; + }; + }; + } + end; }; }; }; luminate = { name = 'Luminate'; @@ -695,10 +974,14 @@ 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'; + iridium = { + name = 'Aura'; + desc = 'Dazzling golden luminance emanates from the bodies of all those around you, and you walk in light even amid the darkest depths of the earth'; + }; }; diamond = { name = 'Radiance'; desc = 'Set the air around you alight with a mystic luminance, letting you see clearly a great distance in every direction for several minutes'; frame = { @@ -713,11 +996,11 @@ }; dominate = { name = 'Dominate'; tone = {235,0,228}; minpower = 4; - rarity = 20; + rarity = 13; amulets = { amethyst = { name = 'Suffocation'; desc = 'Wrap this spell tightly around your victim\'s throat, cutting off their oxygen until you release them.'; }; Index: gems.lua ================================================================== --- gems.lua +++ gems.lua @@ -57,21 +57,28 @@ local img_stone = img('sorcery_amulet.png'):multiply(sorcery.lib.color(gem.tone)) 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 usedamulet if stack:get_count() == 1 then + usedamulet = stack + else + usedamulet = ItemStack(stack) + usedamulet:set_count(1) + end 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 stats = sorcery.amulet.stats(usedamulet) local ctx = { caster = user; target = target; stats = stats; - wield = stack; + wield = usedamulet; 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; @@ -95,17 +102,27 @@ }) end if ctx.sparkle then sorcery.vfx.cast_sparkle(user, ctx.color, stats.power,0.5) end + local infinirune = minetest.check_player_privs(user, 'sorcery:infinirune') if res == nil then - if not minetest.check_player_privs(user, 'sorcery:infinirune') then - sorcery.amulet.setrune(stack) - end + if not infinirune then sorcery.amulet.setrune(usedamulet) end end - return ctx.wield + if stack:get_count() == 1 then + return ctx.wield + else + if not infinirune then + stack:take_item(1) + local leftover = user:get_inventory():add_item('main',usedamulet) + if leftover and leftover:get_count() > 0 then + minetest.add_item(user:get_pos(), leftover) + end + end + return stack + end 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: init.lua ================================================================== --- init.lua +++ init.lua @@ -88,11 +88,11 @@ -- serialization 'marshal', 'json'; -- data structures 'tbl', 'class'; -- wrappers - 'color', 'image', 'ui'; + 'color', 'image', 'ui', 'obj'; -- game 'node', 'item'; } sorcery.stage('worldbuilding',data,root) Index: interop.lua ================================================================== --- interop.lua +++ interop.lua @@ -43,7 +43,37 @@ {'side', 'sorcery:mill', 'grinder'}; {'bottom', 'sorcery:mill', 'input'}; {'bottom', 'sorcery:harvester', 'charge'}; -- output handled on our side + + {'bottom', 'sorcery:runeforge', 'amulet'}; + -- output handled on our side } end + +if minetest.get_modpath('mtg_craftguide') and minetest.get_modpath('sfinv') then +-- the craft guide is handy, but not only is it glitched to the point of enabling +-- trivial denial of service attacks against a server, it breaks some of the most +-- basic mechanics of the sorcery mod. we disable it except for players with a +-- specific debugging privilege. i suppose we could also add a 'potion of +-- omniscience' that allows brief access, but i'm disinclined to; it feels gross. + local pg = sfinv.pages['mtg_craftguide:craftguide'] + local cb = pg.is_in_nav + -- currently this isn't used by mtgcg, but doing this gives us some future- + -- proofing, and keeps us from fucking up any competing access control that + -- might be in use. + pg.is_in_nav = function(self,player, ...) + -- unfortunately, this is a purely cosmetic "access control" mechanism; + -- sfinv doesn't actually check if a page is available to a player before + -- showing it to them. ironic, given how the author specifically warns + -- people in his modding tutorial that the client can submit any form it + -- wants at any time… 🙄 + if not minetest.check_player_privs(player, 'sorcery:omniscience') then + return false + end + if cb + then return cb(self,player,...) + else return true + end + end +end Index: lib/node.lua ================================================================== --- lib/node.lua +++ lib/node.lua @@ -73,19 +73,33 @@ if n.name == 'air' then return true end local d = minetest.registered_nodes[n.name] if not d then return false end return not d.walkable end; + + is_clear = function(pos) + if not sorcery.lib.node.is_air(pos) then return false end + local ents = minetest.get_objects_inside_radius(pos,0.5) + if #ents > 0 then return false end + return true + end; get_arrival_point = function(pos) - local air = sorcery.lib.node.is_air - if air(pos) then - local n = {x=0,y=1,z=0} - if air(vector.add(pos,n)) then return pos end - local down = vector.subtract(pos,n) - if air(down) then return down end - else return nil end + local try = function(p) + local air = sorcery.lib.node.is_clear + if air(p) then + if air(vector.offset(p,0,1,0)) then return p end + if air(vector.offset(p,0,-1,0)) then return vector.offset(p,0,-1,0) end + end + return false + end + + do local t = try(pos) if t then return t end end + for _,o in pairs(ofs.neighbors) do + local p = vector.add(pos, o) + do local t = try(p) if t then return t end end + end end; amass = function(startpoint,names,directions) if not directions then directions = ofs.neighbors end local nodes, positions, checked = {},{},{} ADDED lib/obj.lua Index: lib/obj.lua ================================================================== --- lib/obj.lua +++ lib/obj.lua @@ -0,0 +1,110 @@ +-- functions for working with entities inexplicably missing +-- from the game API + +local fn = {} + +-- WARNING: INEFFICIENT AS FUCK +fn.identify = function(objref) --> objectid + for _, o in pairs(minetest.get_connected_players()) do + if objref == o then return o:get_player_name(), 'player' end + end + for id, le in pairs(minetest.luaentities) do + if le.object == objref then return id, 'entity' end + end +end + +fn.handle = sorcery.lib.class { + __newindex = function(self,key,newval) + local hnd if self.player + then hnd = minetest.get_player_by_name(self._id) + else hnd = minetest.luaentities[self._id] + end + if key == 'id' then + if type(newval) == 'string' then + local p = minetest.get_player_by_name(newval) + if p then + self._id = newval + self.player = true + return + end + end + if minetest.luaentities[newval] then + self._id = newval + self.player = false + else error('attempted to assign invalid ID to entity handle') end + elseif key == 'obj' then + local no, kind = fn.identify(newval) + if no then + self._id = no + if kind == 'player' + then self.player = true + else self.player = false + end + else error('attempted to assign invalid ObjectRef to entity handle') end + elseif key == 'stack' and self.kind == 'item' then + hnd:set_item(newval) + end + end; + __index = function(self,key) + local hnd if self.player then + hnd = minetest.get_player_by_name(self._id) + else + hnd = minetest.luaentities[self._id] + end + if key == 'online' then + return hnd ~= nil + elseif key == 'id' then + if self.player then return nil + else return self._id end + elseif key == 'obj' then + if self.player + then return hnd + else return hnd.object + end + elseif key == 'kind' then + if self.player then return 'player' + elseif hnd.name == '__builtin:item' then return 'item' + else return 'object' end + elseif key == 'name' then + if self.player then return self._id + elseif self.kind == 'item' then + return ItemStack(hnd.itemstring):get_name() + else return hnd.name end + elseif key == 'stack' and self.kind == 'item' then + return ItemStack(hnd.itemstring) + elseif key == 'height' then + if kind == 'item' then return 0.5 + elseif kind == 'player' then + local eh = hnd.object:get_properties().eye_height + return eh and (eh*1.2) or 1 + else + local box = hnd.object:get_properties().collisionbox + if box then + local miny,maxy = box[2], box[5] + return maxy-miny, miny + else return 0 end + end + end + end; + construct = function(h) + local kind, id + if type(h) == 'string' and minetest.get_player_by_name(h) ~= nil then + kind = 'player'; + id = h + elseif minetest.luaentities[h] then + kind = 'entity'; + id = h + else id, kind = fn.identify(h) end + + if not id then + error('attempted to construct object handle from invalid value') + end + + return { + player = kind == 'player'; + _id = id; + } + end; +} + +return fn Index: privs.lua ================================================================== --- privs.lua +++ privs.lua @@ -1,5 +1,13 @@ minetest.register_privilege('sorcery:infinirune', { description = "runes don't discharge upon use, for debugging use only"; give_to_singleplayer = false; give_to_admin = false; }) + +if minetest.get_modpath('mtg_craftguide') then + minetest.register_privilege('sorcery:omniscience', { + description = "access the all-knowing crafting guide"; + give_to_singleplayer = false; + give_to_admin = false; + }) +end ADDED sounds/sorcery_duplicate_bg.ogg Index: sounds/sorcery_duplicate_bg.ogg ================================================================== --- sounds/sorcery_duplicate_bg.ogg +++ sounds/sorcery_duplicate_bg.ogg ADDED sounds/sorcery_genesis.ogg Index: sounds/sorcery_genesis.ogg ================================================================== --- sounds/sorcery_genesis.ogg +++ sounds/sorcery_genesis.ogg cannot compute difference between binary files Index: spell.lua ================================================================== --- spell.lua +++ spell.lua @@ -135,10 +135,36 @@ if v == spell then sorcery.spell.active[k] = nil break end end end end end + +sorcery.spell.ensorcelled = function(player,spell) + if type(player) == 'string' then player = minetest.get_player_by_name(player) end + for _,s in pairs(sorcery.spell.active) do + if spell and (s.name ~= spell) then goto skip end + for _,sub in pairs(s.subjects) do + if sub.player == player then return s end + end + ::skip::end + return false +end + +sorcery.spell.each = function(player,spell) + local idx = 0 + return function() + repeat idx = idx + 1 + local sp = sorcery.spell.active[idx] + if sp == nil then return nil end + if spell == nil or sp.name == spell then + for _,sub in pairs(sp.subjects) do + if sub.player == player then return sp end + end + end + until idx >= #sorcery.spell.active + 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 @@ -185,11 +211,10 @@ 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 @@ -365,5 +390,9 @@ end) end s.starttime = minetest.get_server_uptime() return s end + +minetest.register_on_dieplayer(function(player) + sorcery.spell.disjoin{target=player} +end) Index: vfx.lua ================================================================== --- vfx.lua +++ vfx.lua @@ -106,7 +106,48 @@ x = 0, y = -1, z = 0 } } + end +end + +-- target can be an entity or a pos vector +sorcery.vfx.imbue = function(color, target, strength, height) + local tpos if target.get_pos then + tpos = target:get_pos() + if target.get_properties then + height = height or ((target:get_properties().eye_height or 1)*1.3) + end + else + tpos = target + end + height = height or 1 + local scenter = vector.add(tpos, {x=0,y=height/2,z=0}) + for i=1,math.random(64*(strength or 1),128*(strength or 1)) 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()*(2*math.pi), z=0} + local po = vector.rotate({x=far,y=high,z=0}, yaw) + local ppos = vector.add(po,tpos) + local dir = vector.direction(ppos,scenter) + local vel = math.random() * 0.8 + 0.4 + local col if type(color) == 'function' + then col = color(i, {high = high, far = far, dir = dir, vel = vel, pos = po}) + else col = color + end + 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(col):render(); + glow = 14; + animation = { + type = 'vertical_frames', length = far/vel; + aspect_w = 16, aspect_h = 16; + }; + } end end