sorcery  Diff

Differences From Artifact [24333d0a0e]:

  • File data/spells.lua — part of check-in [3f6a913e4e] at 2020-09-29 12:40:28 on branch trunk — * remove former hacky registration system, replace with consistent and flexible API; rewrite metal/gem generation to take advantage of this new API; tweaks to init system to enable world-local tweaks to lore and sorcery behavior * initial documentation commit * initial steps towards calendar - add default date format, astrolabe; prepare infra for division/melding/transmutation spells, various tweaks and fixes (user: lexi, size: 22648) [annotate] [blame] [check-ins using]

To Artifact [807df7a655]:

  • File data/spells.lua — part of check-in [ea6e475e44] at 2020-10-19 09:52:11 on branch trunk — continue dev on celestial mechanics, add melding+division spells (resonance), refine itemclasses, add keypunch and punchcards, add paper pulp, add a shitload of visuals, add convenience scripts for working with the wiki, make the flamebolt spell actually useful instead of just a pretty lightshow, add essences, inferno crystal, and other goodies; iterate on wands, lots of shit i can't remember, various bugfixes (user: lexi, size: 25710) [annotate] [blame] [check-ins using]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22




23

24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48


49

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
...
193
194
195
196
197
198
199
200
201
202
203
204
205





206
207
208
209
210
211
212
213
214

215
216
217
218
219
220
221
...
223
224
225
226
227
228
229




230
231
232
233
234
235
236
237
238
239
240








241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
...
374
375
376
377
378
379
380
381





















































382
383
384
385
386
387
388
389











































390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405




406
407
408
409
410
411
412
413
...
430
431
432
433
434
435
436
437

438
439
440
441
442
443
444
445


















446
447
448
449
450
451
452
...
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
...
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
...
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
...
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
local cast_sparkle = function(ctx,color,strength,duration)
	minetest.add_particlespawner {
		amount = 70 * strength;
		time = duration or 1.5;
		attached = ctx.caster;
		texture = sorcery.lib.image('sorcery_spark.png'):multiply(color):render();
		minpos = { x = -0.1, z =  0.5, y =  1.2}; 
		maxpos = { x =  0.1, z =  0.3, y =  1.6}; 
		minvel = { x = -0.5, z = -0.5, y = -0.5};
		maxvel = { x =  0.5, z =  0.5, y =  0.5};
		minacc = { x =  0.0, z =  0.0, y =  0.5};
		maxacc = { x =  0.0, z =  0.0, y =  0.5};
		minsize = 0.4, maxsize = 0.8;
		minexptime = 1, maxexptime = 1;
		glow = 14;
		animation = {
			type = 'vertical_frames';
			aspect_w = 16;
			aspect_h = 16;
			length = 1.1;
		};
	}




end

local enchantment_sparkle = function(ctx,color)
	local minvel, maxvel
	if minetest.get_node(vector.add(ctx.target.under,{y=1,z=0,x=0})).name == 'air' then
		minvel = {x=0,z=0,y= 0.3}  maxvel = {x=0,z=0,y= 1.5};
	else
		local dir = vector.subtract(ctx.target.under,ctx.target.above)
		minvel = vector.multiply(dir, 0.3)
		maxvel = vector.multiply(dir, 1.2)

	end
	return minetest.add_particlespawner {
		amount = 50;
		time = 0.5;
		minpos = vector.subtract(ctx.target.under, 0.5);
		maxpos = vector.add(ctx.target.under, 0.5);
		minvel = minvel, maxvel = maxvel;
		minexptime = 1, maxexptime = 2;
		minsize = 0.5, maxsize = 2;
		texture = sorcery.lib.image('sorcery_spark.png'):multiply(color):render();
		animation = {
			type = 'vertical_frames';
			aspect_w = 16, aspect_h = 16;
			length = 2;
		};
		glow = 14;
	}


end

local anchorwand = function(aff,uses,recipe)
	local affcolor = sorcery.lib.color(sorcery.data.affinities[aff].color)
	return {
		name = aff .. ' anchor';
		desc = 'With an enchanter, anchor ' .. aff .. ' spells into an object to enable it to produce preternatural effects';
		uses = uses;
		affinity = recipe;
		color = affcolor;
		sound = 'xdecor_enchanting'; -- FIXME make own
		cast = function(ctx)
			if (not ctx.target) or ctx.target.type ~= 'node' then return false end
			local node = minetest.get_node(ctx.target.under)
			if node.name ~= 'sorcery:enchanter' then return false end

			local inv = minetest.get_meta(ctx.target.under):get_inventory()
			if inv:is_empty('item') then return false end
			local subj = inv:get_stack('item',1)

			-- now we have everything we need. this part is complex.
			-- first, we need to check the item to see if it is in a
................................................................................
-- note: this was written before terminology was standardized,
-- and "leytype" corresponds to what is otherwise known as an
-- "affinity"; "affinity" after this comment is widely misused
return {
	flame = {
		name = 'flamebolt';
		color = {255,89,16};
		uses = 64;
		affinity = {'acacia','blazing'};
		leytype = 'praxic';
		desc = 'Conjure a gout of fire to scorch your foes with a flick of this wand';
		cast = function(ctx)
			local speed = 30 -- TODO maybe amethyst tip increases speed?





			local heading = ctx.heading
			heading.pos.y = heading.pos.y + 1.5 -- TODO maths
			local bolt = minetest.add_entity(heading.pos,'sorcery:spell_projectile_flamebolt')
			bolt:set_rotation(heading.yaw)
			local vel = {
				x = heading.yaw.x * speed;
				y = heading.yaw.y * speed;
				z = heading.yaw.z * speed;
			};

			bolt:set_velocity(vel)
		end;
	};
	seal = {
		name = 'sealing';
		color = {255,238,16};
		uses = 32;
................................................................................
		leytype = 'imperic';
		affinity = {'pine','dark'};
		cast = function(ctx)
			if ctx.target == nil or ctx.target.type ~= 'node' then return false end
			local meta = minetest.get_meta(ctx.target.under)
			-- first we need to check if the wand has an identifying 'key' yet,
			-- and set one if not.




			local wandmode = ctx.base.gem == 'sapphire'
			local keycode
			if ctx.meta:contains('sorcery_wand_key') then
				keycode = ctx.meta:get_string('sorcery_wand_key')
			else
				keycode = sorcery.lib.str.rand(32)
				ctx.meta:set_string('sorcery_wand_key', keycode)
				-- ctx.meta:mark_as_private('sorcery_wand_key')
			end
			if meta:contains('owner') then
				-- owner is already set -- can we break the enchantment?








				if meta:get_string('sorcery_wand_key') == keycode then
					meta:set_string('owner','')
					meta:set_string('sorcery_wand_key','')
					meta:set_string('sorcery_seal_mode','')
					enchantment_sparkle(ctx,sorcery.lib.color(101,255,142))
				else return false end
			else
				meta:set_string('sorcery_wand_key',keycode)
				meta:mark_as_private('sorcery_wand_key')
				meta:set_string('owner',ctx.caster:get_player_name())
				if wandmode then
					meta:set_string('sorcery_seal_mode','wand')
				end
				enchantment_sparkle(ctx,sorcery.lib.color(255,201,27))
			end
		end;
	};
	leyspark = {
................................................................................
	};
	meld = {
		name = 'melding';
		uses = 48;
		leytype = 'syncretic';
		color = {172,65,255};
		affinity = {'apple','verdant'};
		desc = 'Meld the properties of three balanced items on an enchanter to create a new one with special properties, but destroying the old ones and losing two thirds of the mass in the process. The precise outcome is not always predictable.';





















































	};
	divide = {
		name = 'division';
		uses = 19;
		leytype = 'syncretic';
		color = {255,65,121};
		affinity = {'apple','shimmering'};
		desc = 'Shatter an item on an enchanter, dividing its essence equally into three parts and precipitating it into new items embodying various properties of the destroyed item. The outcome is not always predictable.';











































	};
	obliterate = {
		name = 'obliteration';
		uses = 129;
		color = {175,6,212};
		affinity = {'aspen','dark'};
		leytype = 'occlutic';
		desc = 'Totally and irreversibly obliterate all items on an enchanter.';
		cast = function(ctx)
			if not ctx.target or ctx.target.type ~= 'node' then return false end
			local tgt = minetest.get_node(ctx.target.under)
			if tgt.name ~= 'sorcery:enchanter' then return false end

			local inv = minetest.get_meta(ctx.target.under):get_inventory()
			for _,name in pairs{'foci','item'} do
				for i=1,inv:get_size(name) do




					inv:set_stack(name,i,ItemStack(nil))
				end
			end

			enchantment_sparkle(ctx,sorcery.lib.color(255,12,0))
			enchantment_sparkle(ctx,sorcery.lib.color(85,18,35))
			enchantment_sparkle(ctx,sorcery.lib.color(0,0,0))
		end
................................................................................
	};
	transmute = {
		name = 'transmutation';
		uses = 7;
		color = {255,90,18};
		leytype = 'imperic';
		affinity = {'aspen','shimmering','dark','blazing'};
		desc = 'Transmute three ingots into one of a different metal, determined by chance and influenced by configuration of the wand';

	};
	disjoin = {
		name = 'disjunction';
		uses = 32;
		color = {17,6,212};
		leytype = 'occlutic';
		affinity = {'jungle','silent'};
		desc = 'With an enchanter, disjoin the anchor holding a spell into an object so a new spell can instead be bound in';


















	};
	divine = {
		name = 'divining';
		desc = 'Steal away the secrets of the cosmos';
		uses = 16;
		color= {97,97,255};
		sound = 'xdecor:enchanting';
................................................................................
		cast = function(ctx)
			local inks = {'black','red','white','violet','blue','green'}
			local getcolor = function(stack)
				 if stack:is_empty() then return nil end
				 if minetest.get_item_group(stack:get_name(), 'dye') == 0 then return nil end
				 for _,ink in pairs(inks) do
					 if minetest.get_item_group(stack:get_name(), 'color_' ..ink) ~= 0
						 then print('found',ink,'ink') return ink end
				 end
			end
			if not ctx.target or ctx.target.type ~= 'node' then return false end
			local tgt = minetest.get_node(ctx.target.under)
			if tgt.name == 'sorcery:enchanter' then
				local meta = minetest.get_meta(ctx.target.under)
				local inv = meta:get_inventory()
................................................................................
					and not inv:is_empty('foci') then
				   local ink1 = getcolor(inv:get_stack('foci',2))
				   local ink2 = getcolor(inv:get_stack('foci',3))
				   local restrict, kind, mod = {} do
					   local ms = inv:get_stack('foci',1)
					   if not ms:is_empty() then mod = ms:get_name() end
				   end
				   print(ink1,ink2,mod)
				   if ink1 == 'black' and ink2 == 'black' then kind = 'craft'
					   if mod then
						   if mod == sorcery.data.metals.cobalt.parts.powder then
							   restrict.group = 'sorcery_magitech'
						   elseif mod == sorcery.data.metals.vidrium.parts.powder then
							   restrict.group = 'sorcery_ley_device'
						   elseif mod == sorcery.data.metals.aluminum.parts.powder then
................................................................................
						   if mod == sorcery.data.metals.cobalt.parts.powder then
							   restrict.aff = 'praxic'
						   elseif mod == sorcery.data.metals.tungsten.parts.powder then
							   restrict.aff = 'counterpraxic'
						   elseif mod == sorcery.data.metals.aluminum.parts.powder then
							   restrict.aff = 'syncretic'
						   elseif mod == sorcery.data.metals.lithium.parts.powder then
							   -- restrict.aff = 'mandatic' -- no enchants yet, will cause infinite loop
						   elseif mod == sorcery.data.metals.iridium.parts.powder then
							   restrict.aff = 'entropic'
						   elseif mod == sorcery.data.metals.gold.parts.powder then
							   restrict.aff = 'cognic'
						   elseif mod == sorcery.data.metals.silver.parts.powder then
							   -- restrict.aff = 'occlutic'
						   elseif mod == sorcery.data.metals.electrum.parts.powder then
							   -- restrict.aff = 'imperic'
						   else return false end
					   end
				   elseif ink1 == 'red' and ink2 == 'yellow' then kind = 'cook';
				   -- elseif ink1 == 'red' and ink2 == 'orange' then kind = 'smelt';
				   end
				   print('result',kind,dump(restrict))
				   if kind then
					   print('found kind')
					   local rec = ItemStack('sorcery:recipe')
					   local m = rec:get_meta()
					   if ctx.base.gem == 'diamond' then
						   -- make recipe for thing in slot 1
					   else
						   sorcery.cookbook.setrecipe(rec,kind,nil,restrict)
					   end
................................................................................
		color = {244,255,157};
		affinity = {'acacia','shimmering','blazing'};
		leytype = 'cognic';
		cast = function(ctx)
			local center = ctx.heading.pos
			local maxpower = 20
			local power = (ctx.base.gem == 'sapphire' and maxpower) or maxpower/2
			local range = (ctx.base.gem == 'emerald' and 10) or 5
			local duration = (ctx.base.gem == 'amethyst' and 60) or 30
			if ctx.base.gem == 'diamond' then
				power = power * (math.random()*2)
				range = range * (math.random()*2)
				duration = duration * (math.random()*2)
			end
			local lum = math.ceil((power/maxpower) * minetest.LIGHT_MAX)
|
|
|
|
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
>
>
>
>

>
|
<
<
<
<
<
<
<
>
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
>
>

>










|
<
|







 







|





>
>
>
>
>

|







>







 







>
>
>
>
|










>
>
>
>
>
>
>
>










|







 







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







|

|
<
|




>
>
>
>
|







 







|
>








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|







 







<







 







|













<

<







 







|







1
2
3
4
5
6















7
8
9
10
11
12
13
14







15
16















17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
40
...
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
...
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
...
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484

485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
...
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
...
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
...
579
580
581
582
583
584
585

586
587
588
589
590
591
592
...
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642

643

644
645
646
647
648
649
650
...
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
local target_node = function(ctx,tgt)
	if not ctx.target or ctx.target.type ~= 'node' then return false end
	local node = minetest.get_node(ctx.target.under)
	if node.name ~= tgt then return false end
	return node
end;
















local get_enchanter = function(ctx)
	local ench = target_node(ctx, 'sorcery:enchanter')
	if not ench then return false end
	return minetest.get_meta(ctx.target.under):get_inventory()
end

local cast_sparkle = function(ctx,color,strength,duration)







	sorcery.vfx.cast_sparkle(ctx.caster,color,strength,duration)
end
















local enchantment_sparkle = function(ctx,color)
	sorcery.vfx.enchantment_sparkle(ctx.target,color)
end

local anchorwand = function(aff,uses,recipe)
	local affcolor = sorcery.lib.color(sorcery.data.affinities[aff].color)
	return {
		name = aff .. ' anchor';
		desc = 'With an enchanter, anchor ' .. aff .. ' spells into an object to enable it to produce preternatural effects';
		uses = uses;
		affinity = recipe;
		color = affcolor;
		sound = 'xdecor_enchanting'; -- FIXME make own
		cast = function(ctx)
			local node = target_node(ctx, 'sorcery:enchanter')

			if not node then return false end

			local inv = minetest.get_meta(ctx.target.under):get_inventory()
			if inv:is_empty('item') then return false end
			local subj = inv:get_stack('item',1)

			-- now we have everything we need. this part is complex.
			-- first, we need to check the item to see if it is in a
................................................................................
-- note: this was written before terminology was standardized,
-- and "leytype" corresponds to what is otherwise known as an
-- "affinity"; "affinity" after this comment is widely misused
return {
	flame = {
		name = 'flamebolt';
		color = {255,89,16};
		uses = 32;
		affinity = {'acacia','blazing'};
		leytype = 'praxic';
		desc = 'Conjure a gout of fire to scorch your foes with a flick of this wand';
		cast = function(ctx)
			local speed = 30 -- TODO maybe amethyst tip increases speed?
			local radius 
			if ctx.base.gem == 'sapphire'
				then radius = math.random(2,3)
				else radius = math.random(1,2)
			end
			local heading = ctx.heading
			heading.pos.y = heading.pos.y + heading.eyeheight*0.9
			local bolt = minetest.add_entity(heading.pos,'sorcery:spell_projectile_flamebolt')
			bolt:set_rotation(heading.yaw)
			local vel = {
				x = heading.yaw.x * speed;
				y = heading.yaw.y * speed;
				z = heading.yaw.z * speed;
			};
			bolt:get_luaentity()._blastradius = radius
			bolt:set_velocity(vel)
		end;
	};
	seal = {
		name = 'sealing';
		color = {255,238,16};
		uses = 32;
................................................................................
		leytype = 'imperic';
		affinity = {'pine','dark'};
		cast = function(ctx)
			if ctx.target == nil or ctx.target.type ~= 'node' then return false end
			local meta = minetest.get_meta(ctx.target.under)
			-- first we need to check if the wand has an identifying 'key' yet,
			-- and set one if not.
			local modes = {
				sapphire = 'lockdown';
				diamond = 'steal';
			}
			local wandmode = modes[ctx.base.gem] or 'seal'
			local keycode
			if ctx.meta:contains('sorcery_wand_key') then
				keycode = ctx.meta:get_string('sorcery_wand_key')
			else
				keycode = sorcery.lib.str.rand(32)
				ctx.meta:set_string('sorcery_wand_key', keycode)
				-- ctx.meta:mark_as_private('sorcery_wand_key')
			end
			if meta:contains('owner') then
				-- owner is already set -- can we break the enchantment?
				if wandmode == 'steal' then
					if meta:get_string('owner') ~= ctx.caster:get_player_name() then
						meta:set_string('owner',ctx.caster:get_player_name())
						enchantment_sparkle(ctx,sorcery.lib.color(101,255,238))
					end
					return
				end

				if meta:get_string('sorcery_wand_key') == keycode then
					meta:set_string('owner','')
					meta:set_string('sorcery_wand_key','')
					meta:set_string('sorcery_seal_mode','')
					enchantment_sparkle(ctx,sorcery.lib.color(101,255,142))
				else return false end
			else
				meta:set_string('sorcery_wand_key',keycode)
				meta:mark_as_private('sorcery_wand_key')
				meta:set_string('owner',ctx.caster:get_player_name())
				if wandmode == 'lockdown' then
					meta:set_string('sorcery_seal_mode','wand')
				end
				enchantment_sparkle(ctx,sorcery.lib.color(255,201,27))
			end
		end;
	};
	leyspark = {
................................................................................
	};
	meld = {
		name = 'melding';
		uses = 48;
		leytype = 'syncretic';
		color = {172,65,255};
		affinity = {'apple','verdant'};
		desc = 'Meld the properties of three balanced items on an enchanter to create a new one with special properties, but destroying the old ones and losing two thirds of the mass in the process. The precise outcome is not always predictable, and may vary with the moons and the stars.';
		cast = function(ctx)
			local e = get_enchanter(ctx)
			if not e then return false end

			for _,m in pairs(sorcery.data.resonance.meld) do
				if m.restrict and not m.restrict(ctx) 
					then goto next_meld end

				local g = {}
				for i,set in ipairs(m.set) do
					if type(set) == 'table' then
						g[i] = set
					else g[i] = { take = set; } end

					local found = false
					for j=1,e:get_size('foci') do
						local match,res = sorcery.lib.item.groupmatch(g[i].take, e:get_stack('foci',j),false)
						if match then
							found = true
							g[i].slot = j
							g[i].leftover = res
							break
						end
					end
					if not found then goto next_meld end
				end
				-- we've made it past the tests; this meld
				-- matches the spec

				for _,t in pairs(g) do
					if t.leftover and t.leftover:get_count() > 0 then
						e:set_stack('foci',t.slot,t.leftover)
						if t.replacement then
							minetest.add_item(ctx.target.above, ItemStack(t.replacement))
						end
					else
						e:set_stack('foci',t.slot,ItemStack(t.replacement))
					end
				end
				
				local res
				if type(m.results) == 'function' then
					res = m.results(ctx)
				elseif type(m.results) == 'table' and m.results[1] then -- haaaack
					res = select(2,sorcery.lib.tbl.pick(m.results))
				else
					res = m.results
				end

				e:set_stack('item',1,ItemStack(res))
				enchantment_sparkle(ctx,sorcery.lib.color(228,4,201))
			::next_meld::end
		end;
	};
	divide = {
		name = 'division';
		uses = 19;
		leytype = 'syncretic';
		color = {255,65,121};
		affinity = {'apple','shimmering'};
		desc = 'Shatter an item on an enchanter, dividing its essence equally into three parts and precipitating it into new items embodying various properties of the destroyed item. The outcome is not always predictable, and may vary with the moons and the stars.';
		cast = function(ctx)
			local e = get_enchanter(ctx)
			if not e then return false end

			local orig = e:get_stack('item',1)
			local div = sorcery.data.resonance.divide[orig:get_name()]
			if not div then return false end

			local bitch = function(err)
				sorcery.log('data/spells(divide)', err .. ' for ' .. orig:get_name())
				return false
			end

			if not (div.mode and div.give) then
				return bitch('improperly specified division')
			end
			
			if div.restrict and not div.restrict(ctx) then
				return false
			end

			local dst
			if div.mode == 'any' then
				local lst = sorcery.lib.tbl.cshuf(div.give)
				dst = function(i) return lst[i] end
			elseif div.mode == 'random' then
				dst = function() return sorcery.lib.tbl.pick(div.give) end
			elseif div.mode == 'set' then
				dst = function(i) return div.give[i] end
			elseif div.mode == 'all' then
				dst = function() return div.give end
			elseif div.mode == 'fn' then
				dst = function(i) return div.give(i,ctx) end
			else return bitch('invalid division mode') end
			for i=1,e:get_size('foci') do
				e:set_stack('foci',i,ItemStack(dst(i)))
			end
			e:set_stack('item',1,ItemStack(div.replacement))

			for _,color in pairs{{245,63,63},{63,245,178}} do
				enchantment_sparkle(ctx, sorcery.lib.color(color))
			end
		end;
	};
	obliterate = {
		name = 'obliteration';
		uses = 129;
		color = {175,6,212};
		affinity = {'aspen','dark'};
		leytype = 'occlutic';
		desc = 'Incinerate all items on an enchanter, rendering them down to ash or obliterating them entirely.';
		cast = function(ctx)
			local tgt = target_node(ctx, 'sorcery:enchanter')

			if not tgt then return false end

			local inv = minetest.get_meta(ctx.target.under):get_inventory()
			for _,name in pairs{'foci','item'} do
				for i=1,inv:get_size(name) do
					local stack = 'sorcery:ash'
					if ctx.base.gem == 'sapphire' then
						stack = nil
					end
					inv:set_stack(name,i,ItemStack(stack))
				end
			end

			enchantment_sparkle(ctx,sorcery.lib.color(255,12,0))
			enchantment_sparkle(ctx,sorcery.lib.color(85,18,35))
			enchantment_sparkle(ctx,sorcery.lib.color(0,0,0))
		end
................................................................................
	};
	transmute = {
		name = 'transmutation';
		uses = 7;
		color = {255,90,18};
		leytype = 'imperic';
		affinity = {'aspen','shimmering','dark','blazing'};
		desc = 'Transmute three ingots into one of a different metal, determined by chance, and influenced by configuration of the wand as well as the stars and the phase of the moon';
		-- diamond = quantity varies between 1-3
	};
	disjoin = {
		name = 'disjunction';
		uses = 32;
		color = {17,6,212};
		leytype = 'occlutic';
		affinity = {'jungle','silent'};
		desc = 'With an enchanter, disjoin the anchor holding a spell into an object so a new spell can instead be bound in';
		cast = function(ctx)
			local ench = target_node(ctx, 'sorcery:enchanter')
			if not ench then return false end
			local ei = minetest.get_meta(ctx.target.under):get_inventory()
			local item = ei:get_stack('item',1)
			local e = sorcery.enchant.get(item)
			if next(e.spells) == nil then return false end
			if #e.spells == 1 then e = nil else
				if ctx.base.gem == 'sapphire'
					then e.spells = {} e.energy = 0
					else table.remove(e.spells, math.random(#e.spells))
				end
			end
			sorcery.enchant.set(item,e)
			ei:set_stack('item',1,item)
			enchantment_sparkle(ctx,sorcery.lib.color(255,154,44))
			enchantment_sparkle(ctx,sorcery.lib.color(226,44,255))
		end;
	};
	divine = {
		name = 'divining';
		desc = 'Steal away the secrets of the cosmos';
		uses = 16;
		color= {97,97,255};
		sound = 'xdecor:enchanting';
................................................................................
		cast = function(ctx)
			local inks = {'black','red','white','violet','blue','green'}
			local getcolor = function(stack)
				 if stack:is_empty() then return nil end
				 if minetest.get_item_group(stack:get_name(), 'dye') == 0 then return nil end
				 for _,ink in pairs(inks) do
					 if minetest.get_item_group(stack:get_name(), 'color_' ..ink) ~= 0
						 then return ink end
				 end
			end
			if not ctx.target or ctx.target.type ~= 'node' then return false end
			local tgt = minetest.get_node(ctx.target.under)
			if tgt.name == 'sorcery:enchanter' then
				local meta = minetest.get_meta(ctx.target.under)
				local inv = meta:get_inventory()
................................................................................
					and not inv:is_empty('foci') then
				   local ink1 = getcolor(inv:get_stack('foci',2))
				   local ink2 = getcolor(inv:get_stack('foci',3))
				   local restrict, kind, mod = {} do
					   local ms = inv:get_stack('foci',1)
					   if not ms:is_empty() then mod = ms:get_name() end
				   end

				   if ink1 == 'black' and ink2 == 'black' then kind = 'craft'
					   if mod then
						   if mod == sorcery.data.metals.cobalt.parts.powder then
							   restrict.group = 'sorcery_magitech'
						   elseif mod == sorcery.data.metals.vidrium.parts.powder then
							   restrict.group = 'sorcery_ley_device'
						   elseif mod == sorcery.data.metals.aluminum.parts.powder then
................................................................................
						   if mod == sorcery.data.metals.cobalt.parts.powder then
							   restrict.aff = 'praxic'
						   elseif mod == sorcery.data.metals.tungsten.parts.powder then
							   restrict.aff = 'counterpraxic'
						   elseif mod == sorcery.data.metals.aluminum.parts.powder then
							   restrict.aff = 'syncretic'
						   elseif mod == sorcery.data.metals.lithium.parts.powder then
							   -- restrict.aff = 'mandatic' -- no enchants yet, will cause infinite loop 🙃
						   elseif mod == sorcery.data.metals.iridium.parts.powder then
							   restrict.aff = 'entropic'
						   elseif mod == sorcery.data.metals.gold.parts.powder then
							   restrict.aff = 'cognic'
						   elseif mod == sorcery.data.metals.silver.parts.powder then
							   -- restrict.aff = 'occlutic'
						   elseif mod == sorcery.data.metals.electrum.parts.powder then
							   -- restrict.aff = 'imperic'
						   else return false end
					   end
				   elseif ink1 == 'red' and ink2 == 'yellow' then kind = 'cook';
				   -- elseif ink1 == 'red' and ink2 == 'orange' then kind = 'smelt';
				   end

				   if kind then

					   local rec = ItemStack('sorcery:recipe')
					   local m = rec:get_meta()
					   if ctx.base.gem == 'diamond' then
						   -- make recipe for thing in slot 1
					   else
						   sorcery.cookbook.setrecipe(rec,kind,nil,restrict)
					   end
................................................................................
		color = {244,255,157};
		affinity = {'acacia','shimmering','blazing'};
		leytype = 'cognic';
		cast = function(ctx)
			local center = ctx.heading.pos
			local maxpower = 20
			local power = (ctx.base.gem == 'sapphire' and maxpower) or maxpower/2
			local range = (ctx.base.gem == 'emerald' and 6) or 3
			local duration = (ctx.base.gem == 'amethyst' and 60) or 30
			if ctx.base.gem == 'diamond' then
				power = power * (math.random()*2)
				range = range * (math.random()*2)
				duration = duration * (math.random()*2)
			end
			local lum = math.ceil((power/maxpower) * minetest.LIGHT_MAX)