sorcery  Diff

Differences From Artifact [d5ad4a6740]:

  • File data/runes.lua — part of check-in [147592b8e9] at 2020-10-26 03:58:08 on branch trunk — add over-time spellcasting abstraction to enable metamagic and in particular disjunction, add more animations and sound effects, add excavation spell, possibly some others, forget when the last commit was, edit a bunch of magitech to make it subject to the disjunction mechanism (throw up a disjunction aura and waltz right through those force fields bby, wheee), also illumination spells, tweak runeforge and rune frequence to better the balance and also limit player frustration, move some math functions into their own library category, various tweaks and bugfixes, probably other shit i don't remember (user: lexi, size: 30324) [annotate] [blame] [check-ins using]

To Artifact [3a290dc995]:


    43     43   		texture = sorcery.vfx.glowspark(color):render();
    44     44   		animation = {
    45     45   			type = 'vertical_frames', length = 5.1;
    46     46   			aspect_w = 16, aspect_h = 16;
    47     47   		};
    48     48   	});
    49     49   end
           50  +
           51  +local teleport = function(ctx,subjects,delay,pos,color)
           52  +	if ctx.amulet.frame == 'tungsten' then delay = delay * 0.5 end
           53  +	color = color or sorcery.lib.color(29,205,247)
           54  +	local center = ctx.caster:get_pos()
           55  +	for _,sub in pairs(subjects) do
           56  +		local s = sub.ref
           57  +		local offset = vector.subtract(s:get_pos(), center)
           58  +		local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
           59  +		if pt then
           60  +			-- minetest.sound_play('sorcery_stutter', {
           61  +			-- 	object = s, gain = 0.8;
           62  +			-- },true)
           63  +			local mydelay = sub.delay or (delay + math.random(-10,10)*.1);
           64  +			local sh = s:get_properties().eye_height
           65  +			local color = sub.color or color
           66  +			sorcery.lib.node.preload(pt,s)
           67  +			sorcery.spell.cast {
           68  +				name = 'sorcery:translocate';
           69  +				duration = mydelay;
           70  +				caster = ctx.caster;
           71  +				subjects = {{player=s,dest=sub.dest or pt}};
           72  +				timeline = {
           73  +					[0] = function(sp,_,timeleft)
           74  +						sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh)
           75  +						sp.windup = (sp.play_now{
           76  +							sound = 'sorcery_windup';
           77  +							where = 'subjects';
           78  +							gain = 0.4;
           79  +							fade = 1.5;
           80  +						})[1]
           81  +					end;
           82  +					[0.4] = function(sp,_,timeleft)
           83  +						sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh)
           84  +					end;
           85  +					[0.7] = function(sp,_,timeleft)
           86  +						sparkle(color,sp,timeleft*80, timeleft, 2,4, sh)
           87  +					end;
           88  +					[1] = function(sp)
           89  +						sp.silence(sp.windup)
           90  +						minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true)
           91  +						minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
           92  +						sorcery.vfx.body_sparkle(nil,color:brighten(1.3),2,s:get_pos())
           93  +						s:set_pos(pt)
           94  +						sorcery.vfx.body_sparkle(s,color:darken(0.3),2)
           95  +					end;
           96  +				};
           97  +				sounds = {
           98  +					[0] = { sound = 'sorcery_stutter', pos = 'subjects' };
           99  +				};
          100  +			}
          101  +		end
          102  +	end
          103  +end
    50    104   return {
    51    105   	translocate = {
    52    106   		name = 'Translocate';
    53    107   		tone = {0,235,233};
    54    108   		minpower = 3;
    55         -		rarity = 10;
          109  +		rarity = 7;
    56    110   		amulets = {
    57    111   			amethyst = {
    58    112   				name = 'Joining';
    59         -				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';
          113  +				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';
    60    114   				apply = function(ctx)
    61    115   					local maker = ctx.user:get_player_name()
    62    116   					ctx.meta:set_string('rune_join_target',maker)
    63    117   				end;
    64    118   				remove = function(ctx) ctx.meta:set_string('rune_join_target','') end;
          119  +				cast = function(ctx)
          120  +					local target = minetest.get_player_by_name(ctx.meta:get_string('rune_join_target'))
          121  +					if not target then return false end
          122  +
          123  +					local subjects if ctx.amulet.frame == 'cobalt' then
          124  +						if ctx.target.type ~= 'object' then return false end
          125  +						subjects = {{ref=ctx.target.ref}}
          126  +					else subjects = {{ref=ctx.caster}} end
          127  +
          128  +					local delay = math.max(5,11 - ctx.stats.power) + 2.3*(math.random()*2-1)
          129  +					local color = sorcery.lib.color(117,38,237)
          130  +					teleport(ctx,subjects,delay,target:get_pos(),color)
          131  +					if ctx.amulet.frame == 'gold' then
          132  +						teleport(ctx,{{ref=target}},delay,ctx.caster:get_pos())
          133  +					else
          134  +						ctx.sparkle = false
          135  +					end
          136  +				end;
    65    137   				frame = {
          138  +					tungsten = {
          139  +						name = 'Quick Joining';
          140  +						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';
          141  +					};
    66    142   					gold = {
    67    143   						name = 'Exchange';
    68    144   						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.'; 
    69    145   					};
    70    146   					cobalt = {
    71    147   						name = 'Sending';
    72    148   						desc = 'Give this amulet to another and by wielding this amulet against another they will be able to transport them instantly to your side';
................................................................................
    91    167   					else
    92    168   						local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest'))
    93    169   						ctx.meta:set_string('rune_return_dest','')
    94    170   						local subjects = { ctx.caster }
    95    171   						local center = ctx.caster:get_pos()
    96    172   						ctx.sparkle = false
    97    173   						local delay = math.max(3,10 - ctx.stats.power) + 3*(math.random()*2-1)
    98         -						if ctx.amulet.frame == 'tungsten' then delay = delay * 0.5 end
    99         -						for _,s in pairs(subjects) do
   100         -							local offset = vector.subtract(s:get_pos(), center)
   101         -							local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
   102         -							if pt then
   103         -								-- minetest.sound_play('sorcery_stutter', {
   104         -								-- 	object = s, gain = 0.8;
   105         -								-- },true)
   106         -								local mydelay = delay + math.random(-10,10)*.1;
   107         -								local sh = s:get_properties().eye_height
   108         -								local color = sorcery.lib.color(29,205,247)
   109         -								sorcery.lib.node.preload(pt,s)
   110         -								sorcery.spell.cast {
   111         -									duration = mydelay;
   112         -									caster = ctx.caster;
   113         -									subjects = {{player=s,dest=pt}};
   114         -									timeline = {
   115         -										[0] = function(sp,_,timeleft)
   116         -											sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh)
   117         -											sp.windup = (sp.play_now{
   118         -												sound = 'sorcery_windup';
   119         -												where = 'subjects';
   120         -												gain = 0.4;
   121         -												fade = 1.5;
   122         -											})[1]
   123         -										end;
   124         -										[0.4] = function(sp,_,timeleft)
   125         -											sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh)
   126         -										end;
   127         -										[0.7] = function(sp,_,timeleft)
   128         -											sparkle(color,sp,timeleft*80, timeleft, 2,4, sh)
   129         -										end;
   130         -										[1] = function(sp)
   131         -											sp.silence(sp.windup)
   132         -											minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true)
   133         -											minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
   134         -											sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos())
   135         -											s:set_pos(pt)
   136         -											sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2)
   137         -										end;
   138         -									};
   139         -									sounds = {
   140         -										[0] = { sound = 'sorcery_stutter', pos = 'subjects' };
   141         -									};
   142         -								}
   143         -							end
   144         -						end
          174  +						teleport(ctx,{{ref=ctx.caster}},delay,pos)
   145    175   					end
   146    176   				end;
   147    177   				frame = {
   148    178   					tungsten = {
   149    179   						name = 'Quick Return';
   150    180   						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.';
   151    181   					};
................................................................................
   163    193   						name = 'Mass Banishment';
   164    194   						desc = 'Use this amulet once to bind it to a particular point in the world, then use it again to seize up everyone surrounding you in the grip of a fearsome magic that will deport them all in the blink of an eye to whatever destination you have chosen';
   165    195   					};
   166    196   				};
   167    197   			};
   168    198   			ruby = minetest.get_modpath('beds') and {
   169    199   				name = 'Escape';
   170         -				desc = 'Immediately transport yourself out of a dangerous situation back to the last place you slept';
          200  +				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';
   171    201   				cast = function(ctx)
   172    202   					-- if not beds.spawns then beds.read_spawns() end
   173    203   					local subjects = {ctx.caster}
   174    204   					for _,s in pairs(subjects) do
   175    205   						local spp = beds.spawn[ctx.caster:get_player_name()]
   176    206   						if spp then
   177    207   							local oldpos = s:get_pos()
   178         -							minetest.sound_play('sorcery_splunch', {pos=oldpos}, true)
   179         -							sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,131),2,oldpos)
   180         -							s:set_pos(spp)
   181         -							minetest.sound_play('sorcery_splunch', {pos=spp}, true)
   182         -							sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,89),2,spp)
          208  +							local jump = function()
          209  +								minetest.sound_play('sorcery_splunch', {pos=oldpos}, true)
          210  +								sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,131),2,oldpos)
          211  +								s:set_pos(spp)
          212  +								minetest.sound_play('sorcery_splunch', {pos=spp}, true)
          213  +								sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,89),2,spp)
          214  +							end
          215  +							if ctx.amulet.frame == 'cobalt' then
          216  +								sorcery.spell.cast {
          217  +									name = 'sorcery:escape';
          218  +									caster = s;
          219  +									duration = random() * 0.4 + 0.3;
          220  +									timeline = {
          221  +										[0] = function()
          222  +											sorcery.vfx.imbue(sorcery.lib.color(244,38,131), s, 1.3)
          223  +										end;
          224  +										[1] = function(sp)
          225  +											local radius = 6 * ctx.stats.power
          226  +											local center = sp.caster:get_pos()
          227  +											local targets = minetest.get_objects_inside_radius(center, radius)
          228  +											jump()
          229  +											-- TODO: shockwave visuals
          230  +											for _,o in pairs(targets) do
          231  +												if not o:get_armor_groups().immortal then
          232  +													local distance = vector.distance(o:get_pos(), center)
          233  +													local dmg = (7 * ctx.stats.power) * (distance / radius)
          234  +													minetest.punch(ctx.caster, 1.0, {
          235  +														full_punch_interval = 1.0;
          236  +														damage_groups = { fleshy = dmg };
          237  +													}, vector.direction(o:get_pos(), center));
          238  +												end
          239  +											end 
          240  +										end;
          241  +									}
          242  +								}
          243  +							else jump() end
   183    244   						end
   184    245   						-- TODO decide what happens to the people who don't have
   185    246   						-- respawn points already set
   186    247   					end
   187    248   				end;
   188    249   				frame = {
   189    250   					cobalt = {
................................................................................
   195    256   						desc = 'Break up even the fiercest of quarrels by transporting yourself and everyone around you out of harms\' way and immediately back to the last place each slept';
   196    257   					};
   197    258   				};
   198    259   			};
   199    260   			diamond = {
   200    261   				name = 'Elevation';
   201    262   				desc = 'Lift yourself and everything around you high up into the sky';
          263  +				cast = function(ctx)
          264  +					local center = ctx.caster:get_pos()
          265  +					local up = ((ctx.stats.power * 7) + math.random(6,17)) * (math.random() * 0.4 + 0.4)
          266  +					if center.y > 0 then up = up + center.y end
          267  +					local newcenter = vector.new(center.x,up,center.z)
          268  +					if not sorcery.lib.node.get_arrival_point(newcenter) then return false end
          269  +					sorcery.lib.node.preload(newcenter,ctx.caster)
          270  +					local jmpcolor = sorcery.lib.color(0,255,144)
          271  +
          272  +					if not ctx.amulet.frame == 'iridium' then
          273  +						local where = vector.offset(center,0,1,0)
          274  +						repeat local ok, nx = minetest.line_of_sight(where, newcenter)
          275  +							if ok then break end
          276  +							if minetest.get_node_or_nil(nx) == nil then
          277  +								minetest.load_area(nx)
          278  +								where = nx -- save some time
          279  +							else return false end
          280  +						until false
          281  +					end
          282  +					local lift = function(n)
          283  +						local dest = vector.new(n.pos.x, up + n.h, n.pos.z)
          284  +						if sorcery.lib.node.is_clear(dest) then
          285  +							minetest.set_node(dest, minetest.get_node(n.pos))
          286  +							minetest.get_meta(dest):from_table(minetest.get_meta(n.pos):to_table())
          287  +							if math.random(5) == 1 then
          288  +								minetest.set_node(n.pos, {name='sorcery:air_flash_' .. tostring(math.random(10))})
          289  +							else minetest.remove_node(n.pos) end
          290  +							local obs = minetest.get_objects_inside_radius(n.pos, 1.5)
          291  +							if obs then for _,o in pairs(obs) do
          292  +								local pt = sorcery.lib.node.get_arrival_point(vector.add(dest, vector.subtract(o:get_pos(),n.pos))) 
          293  +								if pt then
          294  +									o:set_pos(pt)
          295  +									sorcery.vfx.body_sparkle(o,jmpcolor:darken(0.3),2)
          296  +								end
          297  +							end end
          298  +							return true
          299  +						else
          300  +							return false
          301  +						end
          302  +					end
          303  +					local nodes,sparkles,tmap = {},{},{}
          304  +					local r = math.ceil((ctx.stats.power * 0.1) * 8 + 3)
          305  +					for x = -r,r do -- lazy hack to select a sphere
          306  +					for z = -r,r do
          307  +						local col = {}
          308  +						for y = -r,r do
          309  +							local ofs = vector.new(x,y,z)
          310  +							if sorcery.lib.math.vdcomp(r,ofs) <= 1 then
          311  +								local pos = vector.add(center, ofs)
          312  +								if sorcery.lib.node.is_air(pos) then
          313  +									if y > 0 then
          314  +										sparkles[#sparkles+1] = pos
          315  +										break -- levitation is a sin
          316  +									end
          317  +								else
          318  +									nodes[#nodes+1] = {pos=pos, h=y}
          319  +									col[#col+1] = {pos=pos, h=y}
          320  +								end
          321  +							end
          322  +						end
          323  +						if #col > 0 then
          324  +							local seq = math.floor(math.sqrt((x^2) + (z^2)))
          325  +							-- TODO find a way to optimise this shitshow
          326  +							if tmap[seq]
          327  +								then tmap[seq][#(tmap[seq])+1] = col
          328  +								else tmap[seq] = {col}
          329  +							end
          330  +						end
          331  +					end end
          332  +
          333  +					-- for _,n in pairs(nodes) do
          334  +					-- 	local dest = vector.new(n.pos.x, up + n.h, n.pos.z)
          335  +					-- 	if sorcery.lib.node.is_clear(dest) then
          336  +					-- 		minetest.set_node(dest, minetest.get_node(n.pos))
          337  +					-- 		minetest.get_meta(dest):from_table(minetest.get_meta(n.pos):to_table())
          338  +					-- 		if math.random(5) == 1 then
          339  +					-- 			minetest.set_node(n.pos, {name='sorcery:air_flash_' .. tostring(math.random(10))})
          340  +					-- 		else minetest.remove_node(n.pos) end
          341  +					-- 	end
          342  +					-- end
          343  +					local timeline, sounds = {
          344  +						[0] = function(s)
          345  +							-- sorcery.vfx.imbue(jmpcolor,s.caster,1)
          346  +						end;
          347  +					}, {};
          348  +					local time = 0;
          349  +					for i=0,#tmap do
          350  +						local cols = tmap[i]
          351  +						if cols ~= nil then
          352  +							time = time + math.random()*0.2 + 0.1
          353  +							local wh = {whence=0,secs=2+time}
          354  +							timeline[wh] = function(sp)
          355  +								for _,col in pairs(cols) do
          356  +									for _,n in pairs(col) do lift(n) end
          357  +								end
          358  +							end
          359  +							sounds[wh] = {
          360  +								sound = 'sorcery_zap';
          361  +								gain = math.random() + 0.1;
          362  +								where = cols[1][1].pos;
          363  +							}
          364  +						end
          365  +					end
          366  +					sorcery.spell.cast {
          367  +						name = 'sorcery:elevate';
          368  +						caster = ctx.caster;
          369  +						anchor = center, radius = r;
          370  +						duration = 2 + time;
          371  +						timeline = timeline, sounds = sounds;
          372  +					}
          373  +				end;
          374  +				frame = {
          375  +					iridium = {
          376  +						name = 'Ascension';
          377  +						desc = 'Transport yourself and your surroundings high into the heavens, even if you are deep in the bowels of the earth';
          378  +					};
          379  +				};
   202    380   			};
   203    381   		};
   204    382   	};
   205    383   	disjoin = {
   206    384   		name = 'Disjoin';
   207    385   		tone = {159,235,0};
   208    386   		minpower = 4;
   209         -		rarity = 40;
          387  +		rarity = 34;
   210    388   		amulets = {
   211    389   			sapphire = {
   212    390   				name = 'Unsealing';
   213    391   				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';
   214    392   			};
   215    393   			amethyst = {
   216    394   				name = 'Purging';
................................................................................
   251    429   			};
   252    430   			luxite = {
   253    431   				name = 'Disjunctive Aura';
   254    432   				desc = 'For a time, all magic undertaken in your vicinity will fail totally — including your own';
   255    433   				cast = function(ctx)
   256    434   					local h = ctx.heading.eyeheight*1.1
   257    435   					sorcery.spell.cast {
          436  +						name = 'sorcery:disjunctive-aura';
   258    437   						caster = ctx.caster, attach = 'caster';
   259    438   						subjects = {{player=ctx.caster}};
   260    439   						disjunction = true, range = 4 + ctx.stats.power;
   261    440   						duration = 10 + ctx.stats.power * 3;
   262    441   						timeline = {
   263    442   							[0] = function(s,_,tl)
   264    443   								local ttns = 0.8
................................................................................
   348    527   			};
   349    528   			ruby = {
   350    529   				name = 'Liftoff';
   351    530   				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';
   352    531   				cast = function(ctx)
   353    532   					local power = 14 * (1+(ctx.stats.power * 0.2))
   354    533   					minetest.sound_play('sorcery_hurl',{object=ctx.caster},true)
          534  +
          535  +					local oldsp = sorcery.spell.ensorcelled(ctx.caster, 'sorcery:liftoff')
          536  +					if oldsp then oldsp:cancel() end
          537  +
   355    538   					sorcery.spell.cast {
          539  +						name = 'sorcery:liftoff';
   356    540   						caster = ctx.caster;
   357    541   						subjects = {{player=ctx.caster}};
   358    542   						duration = power * 0.30;
   359    543   						timeline = {
   360    544   							[0] = function(s,_,tl)
   361    545   								sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93))
   362         -								ctx.caster:add_velocity{y=power;x=0,z=0}
          546  +								ctx.caster:add_velocity{y=power*1.2;x=0,z=0}
          547  +							end;
          548  +							[{whence=0, secs=1}] = function(s)
   363    549   								s.affect {
   364    550   									duration = power * 0.50;
   365         -									raise = 2;
          551  +									raise = 0.5;
   366    552   									-- fall = (power * 0.25) * 0.3;
   367    553   									impacts = {
   368    554   										gravity = 0.1;
   369    555   									};
   370    556   								}
   371    557   							end;
   372    558   						};
................................................................................
   396    582   				desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you';
   397    583   				cast = function(ctx)
   398    584   					if not (ctx.target and ctx.target.type == 'object') then return false end
   399    585   					local tgt = ctx.target.ref
   400    586   					local power = 16 * (1+(ctx.stats.power * 0.2))
   401    587   					minetest.sound_play('sorcery_hurl',{object=ctx.caster},true)
   402    588   					sorcery.spell.cast {
          589  +						name = 'sorcery:flinging';
   403    590   						caster = ctx.caster;
   404    591   						subjects = {{player=tgt}};
   405    592   						duration = 4;
   406    593   						timeline = {
   407    594   							[0] = function(s,_,tl)
   408    595   								for _,sub in pairs(s.subjects) do
   409         -									local height = (sub.player:get_properties().eye_height or 1)*1.3
   410         -									local scenter = vector.add(sub.player:get_pos(), {x=0,y=height/2,z=0})
   411         -									for i=1,math.random(64,128) do
   412         -										local high = (height+0.8)*math.random() - 0.8
   413         -										local far = (high >= -0.5 and high <= height) and
   414         -											(math.random() * 0.3 + 0.4) or
   415         -											(math.random() * 0.5)
   416         -										local yaw = {x=0, y = math.random()*100, z=0}
   417         -										local po = vector.rotate({x=far,y=high,z=0}, yaw)
   418         -										local ppos = vector.add(po,sub.player:get_pos())
   419         -										local dir = vector.direction(ppos,scenter)
   420         -										local vel = math.random() * 0.8 + 0.4
   421         -										minetest.add_particle {
   422         -											pos = ppos;
   423         -											velocity = vector.multiply(dir,vel);
   424         -											expirationtime = far / vel;
   425         -											size = math.random()*2.4 + 0.6;
   426         -											texture = sorcery.lib.image('sorcery_sputter.png'):glow(sorcery.lib.color{
   427         -												hue = math.random(41,63);
   428         -												saturation = 100;
   429         -												luminosity = 0.5 + math.random()*0.3;
   430         -											}):render();
   431         -											glow = 14;
   432         -											animation = {
   433         -												type = 'vertical_frames', length = far/vel;
   434         -												aspect_w = 16, aspect_h = 16;
   435         -											};
          596  +									sorcery.vfx.imbue(function() return
          597  +										sorcery.lib.color {
          598  +											hue = math.random(41,63);
          599  +											saturation = 100;
          600  +											luminosity = 0.5 + math.random()*0.3;
   436    601   										}
   437         -									end
          602  +									end, sub.player)
   438    603   								end
   439    604   							end;
   440         -							[0.3] = function(s,te,tl)
          605  +							[{whence=0, secs=1}] = function(s,te,tl)
   441    606   								sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93))
   442    607   								for _,sub in pairs(s.subjects) do
   443    608   									sub.player:add_velocity{y=power;x=0,z=0}
   444    609   								end
   445    610   							end;
   446    611   							[1] = (ctx.amulet.frame == 'cobalt') and function(s,te,tl)
   447    612   								-- TODO add visuals
................................................................................
   489    654   			};
   490    655   		};
   491    656   	};
   492    657   	obliterate = {
   493    658   		name = 'Obliterate';
   494    659   		tone = {255,0,10};
   495    660   		minpower = 5;
   496         -		rarity = 35;
          661  +		rarity = 30;
   497    662   		amulets = {
   498    663   			amethyst = {
   499    664   				name = 'Sapping';
   500    665   				desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention';
   501    666   			};
   502    667   			ruby = {
   503    668   				name = 'Shattering';
................................................................................
   548    713   			};
   549    714   		};
   550    715   	};
   551    716   	excavate = {
   552    717   		name = 'Excavate';
   553    718   		tone = {0,68,235};
   554    719   		minpower = 3;
   555         -		rarity = 30;
          720  +		rarity = 17;
   556    721   		amulets = {
   557    722   			luxite = {
   558    723   				name = 'Stonestride';
   559    724   				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';
   560    725   			};
   561    726   			sapphire = {
   562    727   				name = 'Tunnelling';
................................................................................
   651    816   							sound='sorcery_crunch', where='pos';
   652    817   							ephemeral=true, gain = math.random(3,10) * 0.1;
   653    818   						}
   654    819   						tp = tp + (math.random(2,5) * 0.1)
   655    820   					end
   656    821   					sounds[1] = {sound='sorcery_powerdown', where='pos'}
   657    822   					sorcery.spell.cast {
          823  +						name = 'sorcery:excavate';
   658    824   						caster = ctx.caster;
   659    825   						duration = tp;
   660    826   						timeline = timeline, sounds = sounds;
   661    827   						-- spell state
   662    828   						anchor = ctx.target.under;
   663    829   						tunnel_angle = ctx.caster:get_look_horizontal();
   664    830   						tunnel_radius = math.floor(math.random(3,5) * (ctx.stats.power * 0.1));
................................................................................
   675    841   			};
   676    842   		};
   677    843   	};
   678    844   	genesis = {
   679    845   		name = 'Genesis';
   680    846   		tone = {235,0,175};
   681    847   		minpower = 5;
   682         -		rarity = 25;
          848  +		rarity = 23;
   683    849   		amulets = {
   684    850   			mese = {
   685    851   				mingrade = 4;
   686    852   				name = 'Duplication';
   687         -				desc = 'Generate a copy of any object or item, no matter how common or rare';
          853  +				desc = 'Bring an exact twin of any object or item into existence, no matter how common or rare it might be';
          854  +				cast = function(ctx)
          855  +					local color = sorcery.lib.color(255,61,205)
          856  +					local dup, sndpos, anchor, sbj, ty
          857  +					if ctx.target.type == 'object' and ctx.target.ref:get_luaentity().name == '__builtin:item' then
          858  +						sorcery.vfx.imbue(color, ctx.target.ref)
          859  +						sndpos = 'subjects'
          860  +						sbj = {{player = ctx.target.ref}}
          861  +						local item = ItemStack(ctx.target.ref:get_luaentity().itemstring)
          862  +						local r = function() return math.random() * 2 - 1 end
          863  +						local putpos = vector.offset(ctx.target.ref:get_pos(), r(), 1, r())
          864  +						dup = function()
          865  +							item:set_count(1) -- nice try bouge-san
          866  +							return minetest.add_item(putpos, item), false
          867  +						end
          868  +					elseif ctx.target.type == 'node' then
          869  +						ty = minetest.get_node(ctx.target.under).name
          870  +						sorcery.vfx.imbue(color, ctx.target.under)
          871  +						sndpos = 'pos';
          872  +						anchor = ctx.target.under;
          873  +						dup = function()
          874  +							local origmeta = minetest.get_meta(ctx.target.under):to_table()
          875  +							origmeta.inventory = nil
          876  +							local npos
          877  +							do local vp = {}
          878  +								for _, of in pairs(sorcery.lib.node.offsets.neighbors) do
          879  +									local sum = vector.add(ctx.target.under, of)
          880  +									if sorcery.lib.node.is_clear(sum) then
          881  +										vp[#vp+1] = sum
          882  +									end
          883  +								end
          884  +								if #vp > 0 then npos=vp[math.random(#vp)] end
          885  +							end
          886  +							if npos then
          887  +								minetest.set_node(npos, minetest.get_node(ctx.target.under))
          888  +								minetest.get_meta(npos):from_table(origmeta)
          889  +								return npos, true
          890  +							else
          891  +								local nstack = ItemStack(ty)
          892  +								nstack:get_meta():from_table(origmeta)
          893  +								local leftover = ctx.caster:get_inventory():add_item('main',nstack)
          894  +								if leftover and not leftover.is_empty() then
          895  +									minetest.add_item(ctx.caster:get_pos(), leftover)
          896  +								end
          897  +							end
          898  +						end
          899  +					else
          900  +						return false
          901  +					end
          902  +					if minetest.get_item_group(ty,'do_not_duplicate') ~= 0 then
          903  +						return true
          904  +					end
          905  +
          906  +					sorcery.spell.cast {
          907  +						name = 'sorcery:duplicate';
          908  +						caster = ctx.caster;
          909  +						duration = math.random(10,20) * ((10 - ctx.stats.power)*0.1);
          910  +						anchor = anchor;
          911  +						timeline = {
          912  +							[{whence=0, secs=1}] = function(s,te,tl)
          913  +								local mag = sbj and 0.5 or 0.7
          914  +								local pv = sbj and vector.new(0,0,0) or ctx.target.under
          915  +								local vfn = (sbj and s.visual_subjects or s.visual)
          916  +								vfn {
          917  +									amount = tl * 30, time = tl;
          918  +									minpos = vector.offset(pv,-mag,-mag,-mag);
          919  +									maxpos = vector.offset(pv, mag, mag, mag);
          920  +									minsize = 0.5, maxsize = 2.3;
          921  +									minexptime = 1.0, maxexptime = 1.5;
          922  +									texture = sorcery.lib.image('sorcery_sputter.png'):glow(color):render();
          923  +									animation = {
          924  +										type = 'vertical_frames', length = 1.6;
          925  +										aspect_w = 16, aspect_h = 16;
          926  +									};
          927  +								}
          928  +							end;
          929  +							[1] = function(s,te)
          930  +								local where, node = dup()
          931  +								if where == nil then return end
          932  +								local pv = node and where or vector.new(0,0,0)
          933  +								local mp = (not node) and vector.new(0,0,0) or {
          934  +									x = 0.5, y = 0.5, z = 0.5
          935  +								}
          936  +								minetest.add_particlespawner {
          937  +									amount = 170, time = 0.2;
          938  +									minpos = vector.subtract(pv,mp);
          939  +									maxpos = vector.add(pv,mp);
          940  +									attached = (not node) and where or nil;
          941  +									minvel = {x = -2.0, y = -1.8, z = -2.0};
          942  +									maxvel = {x =  2.0, y =  0.2, z =  2.0};
          943  +									minacc = {x = -0.0, y = -0.1, z = -0.0};
          944  +									maxacc = {x =  0.0, y = -0.3, z =  0.0};
          945  +									minsize = 0.3, maxsize = 2;
          946  +									minexptime = 1, maxexptime = 3.0;
          947  +									texture = sorcery.lib.image('sorcery_spark.png'):glow(color):render();
          948  +									animation = {
          949  +										type = 'vertical_frames', length = 3.1;
          950  +										aspect_w = 16, aspect_h = 16;
          951  +									};
          952  +								}
          953  +							end;
          954  +						};
          955  +						sounds = {
          956  +							[0] = {
          957  +								sound = 'sorcery_duplicate_bg';
          958  +								where = sndpos, stop = 1, fade = 2;
          959  +							};
          960  +							[1] = {
          961  +								sound = 'sorcery_genesis';
          962  +								where = sndpos, ephemeral = true;
          963  +							};
          964  +						};
          965  +					}
          966  +				end;
   688    967   			};
   689    968   		};
   690    969   	};
   691    970   	luminate = {
   692    971   		name = 'Luminate';
   693    972   		tone = {255,194,0};
   694    973   		minpower = 1;
   695    974   		rarity = 5;
   696    975   		amulets = {
   697    976   			luxite = {
   698    977   				name = 'Glow';
   699    978   				desc = 'Swathe yourself in an aura of sparkling radiance, casting light upon all the dark places where you voyage';
          979  +				iridium = {
          980  +					name = 'Aura';
          981  +					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';
          982  +				};
   700    983   			};
   701    984   			diamond = {
   702    985   				name = 'Radiance';
   703    986   				desc = 'Set the air around you alight with a mystic luminance, letting you see clearly a great distance in every direction for several minutes';
   704    987   				frame = {
   705    988   					iridium = {
   706    989   						name = 'Sunshine';
................................................................................
   711    994   			};
   712    995   		};
   713    996   	};
   714    997   	dominate = {
   715    998   		name = 'Dominate';
   716    999   		tone = {235,0,228};
   717   1000   		minpower = 4;
   718         -		rarity = 20;
         1001  +		rarity = 13;
   719   1002   		amulets = {
   720   1003   			amethyst = {
   721   1004   				name = 'Suffocation';
   722   1005   				desc = 'Wrap this spell tightly around your victim\'s throat, cutting off their oxygen until you release them.';
   723   1006   			};
   724   1007   			emerald = {
   725   1008   				name = 'Caging';