sorcery  Check-in [00922196a9]

Overview
Comment:add some more spells, add spell infrastructure to support metamagic, especially disjunction, various tweaks and bugfixes. [emergency commit]
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 00922196a9362e9cadeb8f074f7a94590c213adf34cfc28a1ab51fe86ba99b5b
User & Date: lexi on 2020-10-24 01:21:08
Other Links: manifest | tags
Context
2020-10-26
03:58
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 check-in: 147592b8e9 user: lexi tags: trunk
2020-10-24
01:21
add some more spells, add spell infrastructure to support metamagic, especially disjunction, various tweaks and bugfixes. [emergency commit] check-in: 00922196a9 user: lexi tags: trunk
2020-10-23
00:08
fix some showstopping bugs, more amulet spells, add sound effects, improve teleportation visuals check-in: 90e64c483c user: lexi tags: trunk
Changes

Modified data/runes.lua from [47aa367750] to [4908250df2].

1
2
3
4
5
6


































7
8
9
10
11
12
13
..
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
...
162
163
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
...
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
...
233
234
235
236
237
238
239




240
241
242
243
244
245
246
-- a rune is an abstract object created by a runeforge, which can be
-- applied to an amulet in order to imbue that amulet with unique
-- and fearsome powers. the specific spell depends on the stone the
-- rune is applied to, and not all runes can necessarily be applied
-- to all stones.



































return {
	translocate = {
		name = 'Translocate';
		tone = {0,235,233};
		minpower = 3;
		rarity = 15;
		amulets = {
................................................................................
					else
						local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest'))
						ctx.meta:set_string('rune_return_dest','')
						local subjects = { ctx.caster }
						local center = ctx.caster:get_pos()
						ctx.sparkle = false
						local delay = math.max(3,10 - ctx.stats.power) + 3*(math.random()*2-1)
						print('teledelay',delay,ctx.stats.power)
						for _,s in pairs(subjects) do
							local offset = vector.subtract(s:get_pos(), center)
							local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
							if pt then
								minetest.sound_play('sorcery_stutter', {
									object = s, gain = 0.8;
								},true)
								local windup = minetest.sound_play('sorcery_windup',{
									object = s, gain = 0.4;
								})
								local mydelay = delay + math.random(-10,10)*.1;
								local spark = sorcery.lib.image('sorcery_spark.png')
								local sh = s:get_properties().eye_height
								local sparkle = function(amt,time,minsize,maxsize)
									minetest.add_particlespawner {
										amount = amt, time = time, attached = s;
										minpos = { x = -0.3, y = -0.5, z = -0.3 };
										maxpos = { x =  0.3, y = sh*1.1, z = 0.3 };
										minvel = { x = -0.4, y = -0.2, z = -0.4 };
										maxvel = { x =  0.4, y =  0.2, z =  0.4 };
										minacc = { x = -0.5, y = -0.4, z = -0.5 };
										maxacc = { x =  0.5, y =  0.4, z =  0.5 };
										minexptime = 1.0, maxexptime = 2.0;
										minsize = minsize, maxsize = maxsize, glow = 14;
										texture = spark:blit(spark:multiply(sorcery.lib.color(29,205,247))):render();
										animation = {
											type = 'vertical_frames';
											aspect_w = 16, aspect_h = 16;
										};
									}
								end
								sparkle(mydelay*100,mydelay,0.3,1.3)
								minetest.after(mydelay*0.4, function()
									local timeleft = mydelay - (mydelay*0.4)
									sparkle(timeleft*150, timeleft, 0.6,1.8)
								end)
								minetest.after(mydelay*0.7, function()
									local timeleft = mydelay - (mydelay*0.7)
									sparkle(timeleft*80, timeleft, 2,4)
								end)
								sorcery.lib.node.preload(pt,s)
								minetest.after(mydelay, function()
									minetest.sound_stop(windup)
									minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true)
									minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
									sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos())
									s:set_pos(pt)
									sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2)
								end)
							end
						end
					end
				end;
				frame = {
					iridium = {
						name = 'Mass Return';
................................................................................
	};
	disjoin = {
		name = 'Disjoin';
		tone = {159,235,0};
		minpower = 4;
		rarity = 20;
		amulets = {
			amethyst = {
				name = 'Unsealing';
				desc = 'Wielding this amulet, a touch of your hand will unravel even the mightiest protective magics, leaving doors unsealed and walls free to tear down';
			};























			emerald = {



































				name = 'Mundanity';
				desc = 'Strip away the effects of all active potions and spells in your immediate vicinity, leaving adversaries without their magicks to enhance and protect them, and allies free of any curses they may be hobbled by -- and, of course, vice versa';






			};
		}
	};
	repulse = {
		name = 'Repulse';
		tone = {0,180,235};
		minpower = 1;
		rarity = 7;
		amulets = {
			amethyst = {
				name = 'Hurling';
				desc = 'Wielding this amulet, a mere flick of your fingers will lift any target of your choice bodily into the air and press upon them with tremendous repulsive force, throwing them like a hapless ragdoll out of your path';


















































			};
			sapphire = {
				name = 'Flinging';
				desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you';
			};
			emerald = {
				name = 'Shockwave';
................................................................................
		amulets = {
			amethyst = {
				name = 'Sapping';
				desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention';
			};
			ruby = {
				name = 'Shattering';
				desc = 'Tear a violent wound in the earth with the destructive force of this amulet';
			};
			emerald = {
				name = 'Detonate';
				desc = 'Wielding this amulet, you can loose an extraordinarily powerful bolt of flame from your fingertips that will explode violently on impact, wreaking total havoc wherever it lands';
				cast = function(ctx)
					local speed = 40
					local radius = math.random(math.floor(ctx.stats.power*0.5),math.ceil(ctx.stats.power))
................................................................................
					bolt:set_velocity(vel)
				end;
			};
			luxite = {
				name = 'Lethal Aura';
				desc = 'For a time, anyone who approaches you, whether friend or foe, will suffer immediate retaliation as they are quickly sapped of their life force';
			};




			diamond = {
				name = 'Killing';
				mingrade = 4;
				desc = 'Wield this amulet against a foe to instantly snuff the life out of their mortal form, regardless of their physical protections.';
				cast = function(ctx)
					if not (ctx.target and ctx.target.type == 'object') then return false end
					local tgt = ctx.target.ref






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







 







<




|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
<







 







|



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

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


>
>
>
>
>
>












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







 







|







 







>
>
>
>







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
..
82
83
84
85
86
87
88

89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135


136
137
138
139
140
141
142
...
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
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
...
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
...
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
-- a rune is an abstract object created by a runeforge, which can be
-- applied to an amulet in order to imbue that amulet with unique
-- and fearsome powers. the specific spell depends on the stone the
-- rune is applied to, and not all runes can necessarily be applied
-- to all stones.

local sparkle = function(color, spell, amt,time,minsize,maxsize,sh)
	spell.visual_subjects {
		amount = amt, time = time, -- attached = s;
		minpos = { x = -0.3, y = -0.5, z = -0.3 };
		maxpos = { x =  0.3, y = sh*1.1, z = 0.3 };
		minvel = { x = -0.4, y = -0.2, z = -0.4 };
		maxvel = { x =  0.4, y =  0.2, z =  0.4 };
		minacc = { x = -0.5, y = -0.4, z = -0.5 };
		maxacc = { x =  0.5, y =  0.4, z =  0.5 };
		minexptime = 1.0, maxexptime = 2.0;
		minsize = minsize, maxsize = maxsize, glow = 14;
		texture = sorcery.vfx.glowspark(color):render();
		animation = {
			type = 'vertical_frames';
			aspect_w = 16, aspect_h = 16;
		};
	}
end
local sparktrail = function(fn,tgt,color)
	return (fn or minetest.add_particlespawner)({
		amount = 240, time = 1, attached = tgt;
		minpos = {x = -0.4, y = -0.5, z = -0.4};
		maxpos = {x =  0.4, y = tgt:get_properties().eye_height or 0.5, z =  0.4};
		minacc = {x =  0.0, y = 0.05, z =  0.0};
		maxacc = {x =  0.0, y = 0.15, z =  0.0};
		minexptime = 1.5, maxexptime = 5;
		minsize = 0.5, maxsize = 2.6, glow = 14;
		texture = sorcery.vfx.glowspark(color):render();
		animation = {
			type = 'vertical_frames', length = 5.1;
			aspect_w = 16, aspect_h = 16;
		};
	});
end
return {
	translocate = {
		name = 'Translocate';
		tone = {0,235,233};
		minpower = 3;
		rarity = 15;
		amulets = {
................................................................................
					else
						local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest'))
						ctx.meta:set_string('rune_return_dest','')
						local subjects = { ctx.caster }
						local center = ctx.caster:get_pos()
						ctx.sparkle = false
						local delay = math.max(3,10 - ctx.stats.power) + 3*(math.random()*2-1)

						for _,s in pairs(subjects) do
							local offset = vector.subtract(s:get_pos(), center)
							local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
							if pt then
								-- minetest.sound_play('sorcery_stutter', {
								-- 	object = s, gain = 0.8;
								-- },true)
								local mydelay = delay + math.random(-10,10)*.1;
								local sh = s:get_properties().eye_height
								local color = sorcery.lib.color(29,205,247)
								sorcery.lib.node.preload(pt,s)
								sorcery.spell.cast {
									duration = mydelay;
									caster = ctx.caster;
									subjects = {{player=s,dest=pt}};
									timeline = {
										[0] = function(sp,_,timeleft)
											sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh)
											sp.windup = (sp.play_now{
												sound = 'sorcery_windup';
												where = 'subjects';
												gain = 0.4;
												fade = 1.5;
											})[1]
										end;
										[0.4] = function(sp,_,timeleft)
											sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh)
										end;
										[0.7] = function(sp,_,timeleft)
											sparkle(color,sp,timeleft*80, timeleft, 2,4, sh)
										end;
										[1] = function(sp)
											sp.silence(sp.windup)
											minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true)
											minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
											sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos())
											s:set_pos(pt)
											sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2)
										end;
									};
									sounds = {
										[0] = {
											pos = 'subjects';
											sound = 'sorcery_stutter';
										};
									};
								}


							end
						end
					end
				end;
				frame = {
					iridium = {
						name = 'Mass Return';
................................................................................
	};
	disjoin = {
		name = 'Disjoin';
		tone = {159,235,0};
		minpower = 4;
		rarity = 20;
		amulets = {
			sapphire = {
				name = 'Unsealing';
				desc = 'Wielding this amulet, a touch of your hand will unravel even the mightiest protective magics, leaving doors unsealed and walls free to tear down';
			};
			amethyst = {
				name = 'Purging';
				desc = 'Free yourself from the grip of any malicious spellwork with a snap of your fingers — interrupting all of your own active spells in the process, including impending translocations';
				cast = function(ctx)
					local h = ctx.heading.eyeheight * 1.1
					minetest.add_particlespawner {
						time = 0.2, amount = math.random(200,250), attached = ctx.caster;
						glow = 14, texture = sorcery.vfx.glowspark(sorcery.lib.color(156,255,10)):render();
						minpos = {x = -0.3, y = -0.5, z = -0.3};
						maxpos = {x =  0.3, y =  h,   z =  0.3};
						minvel = {x = -1.8, y = -1.8, z = -1.8};
						maxvel = {x =  1.8, y =  1.8, z =  1.8};
						minsize = 0.2, maxsize = 5;
						animation = {
							type = 'vertical_frames', length = 4.1;
							aspect_w = 16, aspect_h = 16;
						};
						minexptime = 2, maxexptime = 4;
					}
					minetest.sound_play('sorcery_disjoin',{object=ctx.caster},true)
					sorcery.spell.disjoin{target=ctx.caster}
				end;
			};
			emerald = {
				name = 'Disjunction Field';
				desc = 'Render an area totally opaque to spellwork for a period of time, disrupting any existing spells and preventing further spellcasting therein';
			};
			ruby = {
				name = 'Disjunction';
				desc = 'Wield this amulet against a spellcaster to disrupt and abort all their spells in progress, perhaps to trap a foe intent on translocating away, or unleash its force upon the victim of a malign hex to free them from its clutches';
				frame = {
					iridium = {
						name = 'Nullification';
						desc = 'Not only will your victim\'s spells be nullified, but all enchanted objects they carry will be stripped of their power — or possibly even destroyed outright';
					};
				};
			};
			luxite = {
				name = 'Disjunctive Aura';
				desc = 'For a time, all magic undertaken in your vicinity will fail totally';
				cast = function(ctx)
					sorcery.spell.cast {
						caster = ctx.caster, attach = 'caster';
						disjunction = true, range = 4 + ctx.stats.power;
						duration = 10 + ctx.stats.power * 3;
						timeline = {
							[0] = function(s,_,tl)
								sparkle(sorcery.lib.color(120,255,30), s,
									30 * tl, tl, 0.3,1.4, ctx.heading.eyeheight*1.1)
							end
						};
						sounds = {
							[0] = { sound = 'sorcery_disjoin',   pos = 'caster' };
							[1] = { sound = 'sorcery_powerdown', pos = 'caster' };
						};
					}
				end
			};
			diamond = {
				name = 'Mundanity';
				desc = 'Strip away the effects of all active potions and spells in your immediate vicinity, leaving adversaries without their magicks to enhance and protect them, and allies free of any curses they may be hobbled by -- and, of course, vice versa';
				frame = {
					iridium = {
						name = 'Spellshatter';
						desc = 'Blast out a tidal wave of anti-magic that will nullify active spells, but also disenchant or destroy all magical items in range of its violently mundane grip';
					};
				};
			};
		}
	};
	repulse = {
		name = 'Repulse';
		tone = {0,180,235};
		minpower = 1;
		rarity = 7;
		amulets = {
			amethyst = {
				name = 'Hurling';
				desc = 'Wielding this amulet, a mere flick of your fingers will lift any target of your choice bodily into the air and press upon them with tremendous repulsive force, throwing them like a hapless ragdoll out of your path';
				cast = function(ctx)
					if not (ctx.target and ctx.target.type == 'object') then return false end
					local tgt = ctx.target.ref
					local line = vector.subtract(ctx.caster:get_pos(), tgt:get_pos())
					-- direction vector from target to caster
					print('line',dump(line))
					local dir,mag = sorcery.lib.math.vsep(line)
					if mag > 6 then return false end -- no cheating!
					local force = 20 + (ctx.stats.power * 2.5)
					minetest.sound_play('sorcery_hurl',{pos=tgt:get_pos()},true)
					local immortal = tgt:get_luaentity():get_armor_groups().immortal or 0
					if minetest.is_player(tgt) or immortal == 0 then
						tgt:punch(ctx.caster, 1, {
							full_punch_interval = 1;
							damage_groups = { fleshy = force / 10 };
						})
					end
					sparktrail(nil,tgt,sorcery.lib.color(101,226,255))
					if dir.y > 0 then dir.y = 0 end -- spell always lifts
					dir = vector.add(dir, {x=0,z=0,y=-0.25})
					local vel = vector.multiply(dir,0-force)
					tgt:add_velocity(vel)
				end;
			};
			ruby = {
				name = 'Liftoff';
				desc = 'Lift yourself high into the air with a blast of violent repulsive force against the ground, and drift down safely to a position of your choice';
				cast = function(ctx)
					local power = 14 * (1+(ctx.stats.power * 0.2))
					minetest.sound_play('sorcery_hurl',{object=ctx.caster},true)
					sorcery.spell.cast {
						caster = ctx.caster;
						subjects = {{player=ctx.caster}};
						duration = power * 0.25;
						timeline = {
							[0] = function(s,_,tl)
								sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93))
								ctx.caster:add_velocity{y=power;x=0,z=0}
								s.affect {
									duration = power * 0.25;
									raise = 2;
									fall = (power * 0.25) * 0.3;
									impacts = {
										gravity = 0.1;
									};
								}
							end;
						};
					}
				end;
			};
			sapphire = {
				name = 'Flinging';
				desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you';
			};
			emerald = {
				name = 'Shockwave';
................................................................................
		amulets = {
			amethyst = {
				name = 'Sapping';
				desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention';
			};
			ruby = {
				name = 'Shattering';
				desc = 'Tear a violent wound in the land with the destructive force of this amulet';
			};
			emerald = {
				name = 'Detonate';
				desc = 'Wielding this amulet, you can loose an extraordinarily powerful bolt of flame from your fingertips that will explode violently on impact, wreaking total havoc wherever it lands';
				cast = function(ctx)
					local speed = 40
					local radius = math.random(math.floor(ctx.stats.power*0.5),math.ceil(ctx.stats.power))
................................................................................
					bolt:set_velocity(vel)
				end;
			};
			luxite = {
				name = 'Lethal Aura';
				desc = 'For a time, anyone who approaches you, whether friend or foe, will suffer immediate retaliation as they are quickly sapped of their life force';
			};
			mese = {
				name = 'Cataclysm';
				desc = 'Use this amulet once to pick a target, then visit devastation upon it from afar with a mere snap of your fingers';
			};
			diamond = {
				name = 'Killing';
				mingrade = 4;
				desc = 'Wield this amulet against a foe to instantly snuff the life out of their mortal form, regardless of their physical protections.';
				cast = function(ctx)
					if not (ctx.target and ctx.target.type == 'object') then return false end
					local tgt = ctx.target.ref

Modified entities.lua from [2261aac8b4] to [9e257faffc].

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
	age = u.marshal.t.u32;
	lastemit = u.marshal.t.u32;
}

minetest.register_entity('sorcery:spell_projectile_flamebolt',{
	initial_properties = {
		visual = "sprite";
        -- use_texture_alpha = true;
		textures = {'sorcery_fireball.png'};
		groups = {immortal = 1};
		visual_size = { x = 2, y = 2, z = 2 };
		physical = true;
		collide_with_objects = true;
		pointable = false;
		glow = 14;
		static_save = false;

	};



	on_step = function(self,dtime,collision)
		local pos = self.object:get_pos()
		if not self._meta then
			self._meta = { age = 0; lastemit = 0; emitters = {} }
			goto emit
		end

		self._meta.age = self._meta.age + dtime
		if self._meta.age >= 6 then
			goto destroy
		elseif (self._meta.age - self._meta.lastemit) < 3 then
			goto collcheck
		end







		
		::emit:: do
			self._meta.lastemit = self._meta.age
			local spawn = function(num, life_min, life_max, size_min, size_max, gl, speed, img)
				table.insert(self._meta.emitters, minetest.add_particlespawner {
					amount = num;
					minexptime = life_min;







|

<






>

>
>
>













>
>
>
>
>
>
>







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
	age = u.marshal.t.u32;
	lastemit = u.marshal.t.u32;
}

minetest.register_entity('sorcery:spell_projectile_flamebolt',{
	initial_properties = {
		visual = "sprite";
        use_texture_alpha = true;
		textures = {'sorcery_fireball.png'};

		visual_size = { x = 2, y = 2, z = 2 };
		physical = true;
		collide_with_objects = true;
		pointable = false;
		glow = 14;
		static_save = false;
		shaded = false;
	};
	on_activate = function(self)
		self.object:set_armor_groups{immortal = 1}
	end;
	on_step = function(self,dtime,collision)
		local pos = self.object:get_pos()
		if not self._meta then
			self._meta = { age = 0; lastemit = 0; emitters = {} }
			goto emit
		end

		self._meta.age = self._meta.age + dtime
		if self._meta.age >= 6 then
			goto destroy
		elseif (self._meta.age - self._meta.lastemit) < 3 then
			goto collcheck
		end

		-- fireballs dissipate when entering antimagic fields
		do local probe = sorcery.spell.probe(self.object:get_pos())
		if probe.disjunction and not self._meta.ignore_disjunction then
			sorcery.vfx.cast_sparkle(nil,sorcery.lib.color(255,90,10),3,0.3,self.object:get_pos())
			goto destroy
		end end
		
		::emit:: do
			self._meta.lastemit = self._meta.age
			local spawn = function(num, life_min, life_max, size_min, size_max, gl, speed, img)
				table.insert(self._meta.emitters, minetest.add_particlespawner {
					amount = num;
					minexptime = life_min;

Modified gems.lua from [d9755d7797] to [58885d2bb0].

56
57
58
59
60
61
62




63
64
65
66
67
68
69
70
71

72
73
74
75
76
77
78
		local img = sorcery.lib.image
		local img_stone = img('sorcery_amulet.png'):multiply(sorcery.lib.color(gem.tone))
		local img_sparkle = img('sorcery_amulet_sparkle.png')
		local useamulet = function(stack,user,target)
			local sp = sorcery.amulet.getspell(stack)
			if not sp or not sp.cast then return nil end
			local stats = sorcery.amulet.stats(stack)





			local ctx = {
				caster = user;
				target = target;
				stats = stats;
				amulet = stack;
				meta = stack:get_meta(); -- avoid spell boilerplate
				color = sorcery.lib.color(sp.tone);
				today = minetest.get_day_count();

				heading = {
					pos   = user:get_pos();
					yaw   = user:get_look_dir();
					pitch = user:get_look_vertical();
					angle = user:get_look_horizontal();
					eyeheight = user:get_properties().eye_height;
				};







>
>
>
>









>







56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
		local img = sorcery.lib.image
		local img_stone = img('sorcery_amulet.png'):multiply(sorcery.lib.color(gem.tone))
		local img_sparkle = img('sorcery_amulet_sparkle.png')
		local useamulet = function(stack,user,target)
			local sp = sorcery.amulet.getspell(stack)
			if not sp or not sp.cast then return nil end
			local stats = sorcery.amulet.stats(stack)
			local probe = sorcery.spell.probe(user:get_pos())
			-- amulets don't work in antimagic fields, though some may want to 
			-- implement this logic themselves (for instance to check a range)
			if (probe.disjunction and not sp.ignore_disjunction) then return nil end

			local ctx = {
				caster = user;
				target = target;
				stats = stats;
				amulet = stack;
				meta = stack:get_meta(); -- avoid spell boilerplate
				color = sorcery.lib.color(sp.tone);
				today = minetest.get_day_count();
				probe = probe;
				heading = {
					pos   = user:get_pos();
					yaw   = user:get_look_dir();
					pitch = user:get_look_vertical();
					angle = user:get_look_horizontal();
					eyeheight = user:get_properties().eye_height;
				};

Modified init.lua from [e9275c1995] to [c1be2dc670].

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
...
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
local data = sorcery.unit('data',nil,'lore')
local root = sorcery.unit()
sorcery.stage('bootstrap',data,root)

data {'ui'}
sorcery.unit('lib') {
	-- convenience
	'str';
	-- serialization
	'marshal', 'json';
	-- data structures
	'tbl', 'class';
	-- wrappers
	'color', 'image', 'ui';
	-- game
................................................................................
			sorcery.registry.mk(k,v)
		end
	end
end

sorcery.stage('startup',data)
for _,u in pairs {
	'vfx'; 'attunement'; 'context'; 'itemclass';
	'potions'; 'metal', 'gems'; 'leylines'; 'infuser';
	'altar'; 'wands'; 'tools', 'crafttools'; 'enchanter';
	'harvester'; 'metallurgy-hot', 'metallurgy-cold';
	'entities'; 'recipes'; 'coins'; 'interop';
	'tnodes'; 'forcefield'; 'farcaster'; 'portal';
	'cookbook', 'writing'; 'disassembly'; 'displacer';
	'gravitator'; 'precipitator'; 'calendar', 'astrolabe';







|







 







|







80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
...
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
local data = sorcery.unit('data',nil,'lore')
local root = sorcery.unit()
sorcery.stage('bootstrap',data,root)

data {'ui'}
sorcery.unit('lib') {
	-- convenience
	'str', 'math';
	-- serialization
	'marshal', 'json';
	-- data structures
	'tbl', 'class';
	-- wrappers
	'color', 'image', 'ui';
	-- game
................................................................................
			sorcery.registry.mk(k,v)
		end
	end
end

sorcery.stage('startup',data)
for _,u in pairs {
	'vfx'; 'attunement'; 'context'; 'itemclass'; 'spell';
	'potions'; 'metal', 'gems'; 'leylines'; 'infuser';
	'altar'; 'wands'; 'tools', 'crafttools'; 'enchanter';
	'harvester'; 'metallurgy-hot', 'metallurgy-cold';
	'entities'; 'recipes'; 'coins'; 'interop';
	'tnodes'; 'forcefield'; 'farcaster'; 'portal';
	'cookbook', 'writing'; 'disassembly'; 'displacer';
	'gravitator'; 'precipitator'; 'calendar', 'astrolabe';

Modified lib/tbl.lua from [eefda5e589] to [6f943d189b].

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
...
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
...
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
	local new = fn.copy(r1)
	for i=1,#r2 do
		new[#new + 1] = r2[i]
	end
	return new
end

fn.capitalize = function(str)
	return string.upper(string.sub(str, 1,1)) .. string.sub(str, 2)
end

fn.has = function(tbl,value,eqfn)
	for k,v in pairs(tbl) do
		if eqfn then
			if eqfn(v,value,tbl) then return true, k end
		else
			if value == v then return true, k end
		end
................................................................................
	table.sort(keys)
	return fn.each(keys, function(k,i)
		return f(tbl[k],k,i)
	end)
end

fn.iter = function(tbl,fn)
	for i=1,#tbl do
		fn(tbl[i], i)
	end
end

fn.map = function(tbl,fn)
	local new = {}
	for k,v in pairs(tbl) do
		local nv, nk = fn(v, k)
		new[nk or k] = nv
................................................................................

fn.fold = function(tbl,fn,acc)
	if #tbl == 0 then
		fn.each_o(tbl, function(v)
			acc = fn(acc, v, k)
		end)
	else
		for i=0,#tbl do
			acc = fn(acc,tbl[i],i)
		end
	end
	return acc
end

fn.walk = function(tbl,path)
	if type(path) == 'table' then







<
<
<
<







 







|
<
<







 







|
|







86
87
88
89
90
91
92




93
94
95
96
97
98
99
...
138
139
140
141
142
143
144
145


146
147
148
149
150
151
152
...
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
	local new = fn.copy(r1)
	for i=1,#r2 do
		new[#new + 1] = r2[i]
	end
	return new
end





fn.has = function(tbl,value,eqfn)
	for k,v in pairs(tbl) do
		if eqfn then
			if eqfn(v,value,tbl) then return true, k end
		else
			if value == v then return true, k end
		end
................................................................................
	table.sort(keys)
	return fn.each(keys, function(k,i)
		return f(tbl[k],k,i)
	end)
end

fn.iter = function(tbl,fn)
	for i,v in ipairs(tbl) do fn(v, i) end


end

fn.map = function(tbl,fn)
	local new = {}
	for k,v in pairs(tbl) do
		local nv, nk = fn(v, k)
		new[nk or k] = nv
................................................................................

fn.fold = function(tbl,fn,acc)
	if #tbl == 0 then
		fn.each_o(tbl, function(v)
			acc = fn(acc, v, k)
		end)
	else
		for i,v in ipairs(tbl) do
			acc = fn(acc,v,i)
		end
	end
	return acc
end

fn.walk = function(tbl,path)
	if type(path) == 'table' then

Modified sorcery.md from [246ca8c1c8] to [b729085cc9].

16
17
18
19
20
21
22



23
24
25
26
27
28
29
 * **xdecor** for various tools and ingredients, especially honey and the hammer
 * **basic_materials** for crafting ingredients
 * **instant_ores** for ore generation. temporary, will be removed and replaced with home-grown mechanism soon
 * **farming redo** for potion ingredients
 * **late** for spell, potion, and gravitator effects
   * **note**: in order for the gravitator to work, the late condition interval must be lowered from its default of 1.0 to 0.1. this currently can only be done by altering a variable at the top of `late/conditions.lua`, though a note in the source suggests a configuration option will be added eventually. hopefully this is so.




# interoperability
sorcery has special functionality to ensure it can cooperate with various other modules, although they are not necessarily required for it to function.

## xdecor
by default, `sorcery` disables the xdecor enchanter, since `sorcery` offers its own, much more sophisticated enchantment mechanism. however, the two can coexist if you really want; a configuration flag can be used to prevent `sorcery` disabling the xdecor enchanter.

## hopper







>
>
>







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 * **xdecor** for various tools and ingredients, especially honey and the hammer
 * **basic_materials** for crafting ingredients
 * **instant_ores** for ore generation. temporary, will be removed and replaced with home-grown mechanism soon
 * **farming redo** for potion ingredients
 * **late** for spell, potion, and gravitator effects
   * **note**: in order for the gravitator to work, the late condition interval must be lowered from its default of 1.0 to 0.1. this currently can only be done by altering a variable at the top of `late/conditions.lua`, though a note in the source suggests a configuration option will be added eventually. hopefully this is so.

## libraries
 * **luajit**, because `sorcery`'s code uses modern features not available in the ancient lua version bundled with minetest. alternately, it may be possible to build minetest against a more recent lua version if you're feeling masochistic; luajit will probably be faster tho and has first-party support

# interoperability
sorcery has special functionality to ensure it can cooperate with various other modules, although they are not necessarily required for it to function.

## xdecor
by default, `sorcery` disables the xdecor enchanter, since `sorcery` offers its own, much more sophisticated enchantment mechanism. however, the two can coexist if you really want; a configuration flag can be used to prevent `sorcery` disabling the xdecor enchanter.

## hopper

Modified vfx.lua from [343a5ccf55] to [aaa85eb70f].

1





2
3
4
5
6
7
8
sorcery.vfx = {}






sorcery.vfx.cast_sparkle = function(caster,color,strength,duration,pos)
	local ofs = pos
		and function(x) return vector.add(pos,x) end
		or  function(x) return x end
	local height = caster:get_properties().eye_height
	minetest.add_particlespawner {

>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
sorcery.vfx = {}

sorcery.vfx.glowspark = function(color)
	local spark = sorcery.lib.image('sorcery_spark.png')
	return spark:blit(spark:multiply(color))
end

sorcery.vfx.cast_sparkle = function(caster,color,strength,duration,pos)
	local ofs = pos
		and function(x) return vector.add(pos,x) end
		or  function(x) return x end
	local height = caster:get_properties().eye_height
	minetest.add_particlespawner {

Modified wands.lua from [b35b5eeee9] to [e661ef77a3].

207
208
209
210
211
212
213
214






215
216
217
218
219
220
221
...
253
254
255
256
257
258
259

260
261
262
263
264
265
266
end

local wand_cast = function(stack, user, target)
	local meta = stack:get_meta()
	local wand = sorcery.wands.util.getproto(stack)
	if meta:contains('sorcery_wand_spell') == false then return nil end
	local spell = meta:get_string('sorcery_wand_spell')
	local castfn = sorcery.data.spells[spell].cast






	if castfn == nil then return nil end
	local matprops = sorcery.wands.util.matprops(wand)
	if matprops.bond then
		local userct, found = 0, false
		for i=1,matprops.bond do
			local prop = 'bound_user_' .. tostring(i)
			if meta:contains(prop) then
................................................................................
	local context = {
		base = wand;
		stats = matprops;
		meta = meta;
		item = stack;
		caster = user;
		target = target;

		today = minetest.get_day_count();
		heading = {
			pos   = user:get_pos();
			yaw   = user:get_look_dir();
			pitch = user:get_look_vertical();
			angle = user:get_look_horizontal();
			eyeheight = uprops.eye_height;







|
>
>
>
>
>
>







 







>







207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
...
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
end

local wand_cast = function(stack, user, target)
	local meta = stack:get_meta()
	local wand = sorcery.wands.util.getproto(stack)
	if meta:contains('sorcery_wand_spell') == false then return nil end
	local spell = meta:get_string('sorcery_wand_spell')
	local spelldata = sorcery.data.spells[spell]

	-- wands don't work in anti-magic fields
	local probe = sorcery.spell.probe(user:get_pos())
	if probe.disjunction and not spelldata.ignore_disjunction then return nil end

	local castfn = spelldata.cast
	if castfn == nil then return nil end
	local matprops = sorcery.wands.util.matprops(wand)
	if matprops.bond then
		local userct, found = 0, false
		for i=1,matprops.bond do
			local prop = 'bound_user_' .. tostring(i)
			if meta:contains(prop) then
................................................................................
	local context = {
		base = wand;
		stats = matprops;
		meta = meta;
		item = stack;
		caster = user;
		target = target;
		probe = probe;
		today = minetest.get_day_count();
		heading = {
			pos   = user:get_pos();
			yaw   = user:get_look_dir();
			pitch = user:get_look_vertical();
			angle = user:get_look_horizontal();
			eyeheight = uprops.eye_height;