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]
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';