Comment: | 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 |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
147592b8e9972e05b003073cb2fde9b6 |
User & Date: | lexi on 2020-10-26 03:58:08 |
Other Links: | manifest | tags |
2020-10-30
| ||
18:47 | add duplicate and elevate spells, add more sfx, various tweaks and bugfixes, add object handle class check-in: 6e106c135c user: lexi tags: trunk | |
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 | |
Modified data/runes.lua from [4908250df2] to [d5ad4a6740].
1 1 -- a rune is an abstract object created by a runeforge, which can be 2 2 -- applied to an amulet in order to imbue that amulet with unique 3 3 -- and fearsome powers. the specific spell depends on the stone the 4 4 -- rune is applied to, and not all runes can necessarily be applied 5 5 -- to all stones. 6 6 7 -local sparkle = function(color, spell, amt,time,minsize,maxsize,sh) 8 - spell.visual_subjects { 9 - amount = amt, time = time, -- attached = s; 10 - minpos = { x = -0.3, y = -0.5, z = -0.3 }; 11 - maxpos = { x = 0.3, y = sh*1.1, z = 0.3 }; 7 +local sparkle_region = function(s) 8 + s.spell.visual_subjects { 9 + amount = s.amt, time = s.time, -- attached = s; 10 + minpos = s.minpos; 11 + maxpos = s.maxpos; 12 12 minvel = { x = -0.4, y = -0.2, z = -0.4 }; 13 13 maxvel = { x = 0.4, y = 0.2, z = 0.4 }; 14 14 minacc = { x = -0.5, y = -0.4, z = -0.5 }; 15 15 maxacc = { x = 0.5, y = 0.4, z = 0.5 }; 16 - minexptime = 1.0, maxexptime = 2.0; 17 - minsize = minsize, maxsize = maxsize, glow = 14; 18 - texture = sorcery.vfx.glowspark(color):render(); 16 + minexptime = 1.0*(s.length or 1), maxexptime = 2.0 * (s.length or 1); 17 + minsize = s.minsize, maxsize = s.maxsize, glow = 14; 18 + texture = (s.img or sorcery.vfx.glowspark(s.color)):render(); 19 19 animation = { 20 20 type = 'vertical_frames'; 21 21 aspect_w = 16, aspect_h = 16; 22 + length = 0.1 + (s.length or 1)*2; 22 23 }; 23 24 } 24 25 end 25 -local sparktrail = function(fn,tgt,color) 26 +local sparkle = function(color, spell, amt,time,minsize,maxsize,sh) 27 + sparkle_region { spell = spell; 28 + amt = amt, time = time, color = color; 29 + minsize = minsize, maxsize = maxsize; 30 + minpos = { x = -0.3, y = -0.5, z = -0.3 }; 31 + maxpos = { x = 0.3, y = sh*1.1, z = 0.3 }; 32 + } 33 +end 34 +local sparktrail = function(fn,tgt,color,time) 26 35 return (fn or minetest.add_particlespawner)({ 27 - amount = 240, time = 1, attached = tgt; 36 + amount = 240, time = time or 1, attached = tgt; 28 37 minpos = {x = -0.4, y = -0.5, z = -0.4}; 29 38 maxpos = {x = 0.4, y = tgt:get_properties().eye_height or 0.5, z = 0.4}; 30 39 minacc = {x = 0.0, y = 0.05, z = 0.0}; 31 40 maxacc = {x = 0.0, y = 0.15, z = 0.0}; 32 41 minexptime = 1.5, maxexptime = 5; 33 42 minsize = 0.5, maxsize = 2.6, glow = 14; 34 43 texture = sorcery.vfx.glowspark(color):render(); ................................................................................ 39 48 }); 40 49 end 41 50 return { 42 51 translocate = { 43 52 name = 'Translocate'; 44 53 tone = {0,235,233}; 45 54 minpower = 3; 46 - rarity = 15; 55 + rarity = 10; 47 56 amulets = { 48 57 amethyst = { 49 58 name = 'Joining'; 50 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'; 51 60 apply = function(ctx) 52 61 local maker = ctx.user:get_player_name() 53 62 ctx.meta:set_string('rune_join_target',maker) ................................................................................ 82 91 else 83 92 local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest')) 84 93 ctx.meta:set_string('rune_return_dest','') 85 94 local subjects = { ctx.caster } 86 95 local center = ctx.caster:get_pos() 87 96 ctx.sparkle = false 88 97 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 89 99 for _,s in pairs(subjects) do 90 100 local offset = vector.subtract(s:get_pos(), center) 91 101 local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset)) 92 102 if pt then 93 103 -- minetest.sound_play('sorcery_stutter', { 94 104 -- object = s, gain = 0.8; 95 105 -- },true) ................................................................................ 123 133 minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true) 124 134 sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos()) 125 135 s:set_pos(pt) 126 136 sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2) 127 137 end; 128 138 }; 129 139 sounds = { 130 - [0] = { 131 - pos = 'subjects'; 132 - sound = 'sorcery_stutter'; 133 - }; 140 + [0] = { sound = 'sorcery_stutter', pos = 'subjects' }; 134 141 }; 135 142 } 136 143 end 137 144 end 138 145 end 139 146 end; 140 147 frame = { 148 + tungsten = { 149 + name = 'Quick Return'; 150 + 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 + }; 141 152 iridium = { 142 153 name = 'Mass Return'; 143 154 desc = 'Use this amulet once to bind it to a particular place, then carry yourself and everyone around you back to that point in a flash simply by using it again'; 144 155 }; 145 156 }; 146 157 }; 147 158 emerald = { ................................................................................ 191 202 }; 192 203 }; 193 204 }; 194 205 disjoin = { 195 206 name = 'Disjoin'; 196 207 tone = {159,235,0}; 197 208 minpower = 4; 198 - rarity = 20; 209 + rarity = 40; 199 210 amulets = { 200 211 sapphire = { 201 212 name = 'Unsealing'; 202 213 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'; 203 214 }; 204 215 amethyst = { 205 216 name = 'Purging'; ................................................................................ 236 247 name = 'Nullification'; 237 248 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'; 238 249 }; 239 250 }; 240 251 }; 241 252 luxite = { 242 253 name = 'Disjunctive Aura'; 243 - desc = 'For a time, all magic undertaken in your vicinity will fail totally'; 254 + desc = 'For a time, all magic undertaken in your vicinity will fail totally — including your own'; 244 255 cast = function(ctx) 256 + local h = ctx.heading.eyeheight*1.1 245 257 sorcery.spell.cast { 246 258 caster = ctx.caster, attach = 'caster'; 259 + subjects = {{player=ctx.caster}}; 247 260 disjunction = true, range = 4 + ctx.stats.power; 248 261 duration = 10 + ctx.stats.power * 3; 249 262 timeline = { 250 263 [0] = function(s,_,tl) 251 - sparkle(sorcery.lib.color(120,255,30), s, 252 - 30 * tl, tl, 0.3,1.4, ctx.heading.eyeheight*1.1) 253 - end 264 + local ttns = 0.8 265 + local vel = s.range / ttns 266 + s.visual_caster { 267 + amount = 300, time = ttns, glow = 14; 268 + texture = sorcery.lib.image('sorcery_sputter.png'):glow(sorcery.lib.color(160,255,80)):render(); 269 + minpos = { x = -0.0, y = h*0.5,z = -0.0 }; 270 + maxpos = { x = 0.0, y = h*0.5,z = 0.0 }; 271 + minvel = { x = -vel, y = -0.0, z = -vel }; 272 + maxvel = { x = vel, y = 0.0, z = vel }; 273 + minacc = { x = -0.2, y = -0.0, z = -0.2 }; 274 + maxacc = { x = 0.2, y = 0.0, z = 0.2 }; 275 + minexptime = ttns, maxexptime = ttns * 2; 276 + minsize = 0.2, maxsize = 4.5; 277 + animation = { 278 + type = 'vertical_frames', length = 0.1 + ttns*2; 279 + aspect_w = 16, aspect_h = 16; 280 + } 281 + } 282 + end; 283 + [{whence=0,secs=0.8}] = function(s,te,tl) 284 + local range = s.range 285 + sparkle_region { 286 + spell = s, amt = 150*tl, time = tl; 287 + minsize = 1, maxsize = 8.4; 288 + minpos = { x = 0-range, y = -0.5, z = 0-range }; 289 + maxpos = { x = range, y = h, z = range }; 290 + img = sorcery.lib.image('sorcery_flicker.png'):glow(sorcery.lib.color(120,255,30)); 291 + } 292 + end; 254 293 }; 255 294 sounds = { 256 - [0] = { sound = 'sorcery_disjoin', pos = 'caster' }; 257 - [1] = { sound = 'sorcery_powerdown', pos = 'caster' }; 295 + [0.00] = {sound='sorcery_disjoin', where='caster'}; 296 + [{whence=0,secs=0.8}] = { 297 + sound='sorcery_disjoin_bg', where='subjects'; 298 + gain=0.5, stop = {whence=1,secs=-1.5} 299 + }; 300 + [1.00] = {sound='sorcery_powerdown', where='caster'}; 258 301 }; 259 302 } 260 303 end 261 304 }; 262 305 diamond = { 263 306 name = 'Mundanity'; 264 307 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'; ................................................................................ 271 314 }; 272 315 } 273 316 }; 274 317 repulse = { 275 318 name = 'Repulse'; 276 319 tone = {0,180,235}; 277 320 minpower = 1; 278 - rarity = 7; 321 + rarity = 5; 279 322 amulets = { 280 323 amethyst = { 281 324 name = 'Hurling'; 282 325 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'; 283 326 cast = function(ctx) 284 327 if not (ctx.target and ctx.target.type == 'object') then return false end 285 328 local tgt = ctx.target.ref 286 329 local line = vector.subtract(ctx.caster:get_pos(), tgt:get_pos()) 287 330 -- direction vector from target to caster 288 - print('line',dump(line)) 289 331 local dir,mag = sorcery.lib.math.vsep(line) 290 332 if mag > 6 then return false end -- no cheating! 291 333 local force = 20 + (ctx.stats.power * 2.5) 292 334 minetest.sound_play('sorcery_hurl',{pos=tgt:get_pos()},true) 293 335 local immortal = tgt:get_luaentity():get_armor_groups().immortal or 0 294 336 if minetest.is_player(tgt) or immortal == 0 then 295 337 tgt:punch(ctx.caster, 1, { ................................................................................ 309 351 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'; 310 352 cast = function(ctx) 311 353 local power = 14 * (1+(ctx.stats.power * 0.2)) 312 354 minetest.sound_play('sorcery_hurl',{object=ctx.caster},true) 313 355 sorcery.spell.cast { 314 356 caster = ctx.caster; 315 357 subjects = {{player=ctx.caster}}; 316 - duration = power * 0.25; 358 + duration = power * 0.30; 317 359 timeline = { 318 360 [0] = function(s,_,tl) 319 361 sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93)) 320 362 ctx.caster:add_velocity{y=power;x=0,z=0} 321 363 s.affect { 322 - duration = power * 0.25; 364 + duration = power * 0.50; 323 365 raise = 2; 324 - fall = (power * 0.25) * 0.3; 366 + -- fall = (power * 0.25) * 0.3; 325 367 impacts = { 326 368 gravity = 0.1; 327 369 }; 328 370 } 329 371 end; 372 + }; 373 + intervals = { 374 + {period = 0.2, after = {whence=0, secs=2}; fn = function(c) 375 + -- return gravity to normal once they touch down 376 + for si,sub in pairs(c.spell.subjects) do 377 + local p = sub.player:get_pos() 378 + for i=1,3 do 379 + local sum = vector.offset(p,0,-i,0) 380 + if not sorcery.lib.node.is_air(sum) then 381 + c.spell.release_subject(si) 382 + if #c.spell.subjects == 0 then 383 + return false 384 + end 385 + break 386 + end 387 + end 388 + end 389 + end}; 330 390 }; 331 391 } 332 392 end; 333 393 }; 334 394 sapphire = { 335 395 name = 'Flinging'; 336 396 desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you'; 397 + cast = function(ctx) 398 + if not (ctx.target and ctx.target.type == 'object') then return false end 399 + local tgt = ctx.target.ref 400 + local power = 16 * (1+(ctx.stats.power * 0.2)) 401 + minetest.sound_play('sorcery_hurl',{object=ctx.caster},true) 402 + sorcery.spell.cast { 403 + caster = ctx.caster; 404 + subjects = {{player=tgt}}; 405 + duration = 4; 406 + timeline = { 407 + [0] = function(s,_,tl) 408 + 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 + }; 436 + } 437 + end 438 + end 439 + end; 440 + [0.3] = function(s,te,tl) 441 + sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93)) 442 + for _,sub in pairs(s.subjects) do 443 + sub.player:add_velocity{y=power;x=0,z=0} 444 + end 445 + end; 446 + [1] = (ctx.amulet.frame == 'cobalt') and function(s,te,tl) 447 + -- TODO add visuals 448 + for _,sub in pairs(s.subjects) do 449 + sub.player:add_velocity{y=-power*2;x=0,z=0} 450 + end 451 + end or nil; 452 + }; 453 + sounds = { 454 + [0.3] = { 455 + sound = 'sorcery_hurl'; 456 + where = 'subjects'; 457 + ephemeral = true; 458 + }; 459 + [1] = (ctx.amulet.frame == 'cobalt') and { 460 + sound = 'sorcery_hurl'; 461 + where = 'subjects'; 462 + ephemeral = true; 463 + } or nil; 464 + }; 465 + }; 466 + end; 467 + frame = { 468 + cobalt = { 469 + name = 'Crushing'; 470 + desc = 'Toss an enemy violently into the air, then bring them crashing down to earth with bone-shattering force'; 471 + }; 472 + iridium = { 473 + name = 'Mass Flinging'; 474 + desc = 'Send everyone around you hurtling into the sky, and allow the inevitable impact to do your dirty work for you'; 475 + }; 476 + }; 337 477 }; 338 478 emerald = { 339 479 name = 'Shockwave'; 340 480 desc = 'Let loose a stream of concussive force that slams into everything in your path and sends them hurtling away from you'; 341 481 }; 342 482 luxite = { 343 483 name = 'Repulsive Aura'; ................................................................................ 349 489 }; 350 490 }; 351 491 }; 352 492 obliterate = { 353 493 name = 'Obliterate'; 354 494 tone = {255,0,10}; 355 495 minpower = 5; 356 - rarity = 30; 496 + rarity = 35; 357 497 amulets = { 358 498 amethyst = { 359 499 name = 'Sapping'; 360 500 desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention'; 361 501 }; 362 502 ruby = { 363 503 name = 'Shattering'; ................................................................................ 375 515 local bolt = minetest.add_entity(vector.add(heading.pos,vector.multiply(heading.yaw,2.5)),'sorcery:spell_projectile_flamebolt') 376 516 bolt:set_rotation(heading.yaw) 377 517 bolt:get_luaentity()._blastradius = radius 378 518 bolt:set_velocity(vel) 379 519 end; 380 520 }; 381 521 luxite = { 382 - name = 'Lethal Aura'; 383 - 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'; 522 + name = 'Cataclysmic Aura'; 523 + desc = 'A storm of destructive force rages about you as you stand untouched, the master of its voracious dark energies'; 384 524 }; 385 525 mese = { 386 526 name = 'Cataclysm'; 387 - desc = 'Use this amulet once to pick a target, then visit devastation upon it from afar with a mere snap of your fingers'; 527 + desc = 'Use this amulet once to pick a target, then visit devastation upon it from afar whenever you so will with a mere snap of your fingers'; 388 528 }; 389 529 diamond = { 390 530 name = 'Killing'; 391 531 mingrade = 4; 392 532 desc = 'Wield this amulet against a foe to instantly snuff the life out of their mortal form, regardless of their physical protections.'; 393 533 cast = function(ctx) 394 534 if not (ctx.target and ctx.target.type == 'object') then return false end ................................................................................ 408 548 }; 409 549 }; 410 550 }; 411 551 excavate = { 412 552 name = 'Excavate'; 413 553 tone = {0,68,235}; 414 554 minpower = 3; 415 - rarity = 60; 555 + rarity = 30; 416 556 amulets = { 557 + luxite = { 558 + name = 'Stonestride'; 559 + 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 + }; 417 561 sapphire = { 418 562 name = 'Tunnelling'; 419 - desc = 'Carve a long tunnel ahead of you into the rock'; 563 + desc = 'Carve a long tunnel ahead of you into the rock and dirt'; 564 + cast = function(ctx) 565 + if ctx.target.type ~= 'node' then return false end 566 + local allowed = { 567 + ['default:stone'] = true; 568 + ['default:desert_stone'] = true; 569 + ['default:dirt'] = true; 570 + ['default:gravel'] = true; 571 + } 572 + if allowed[minetest.get_node(ctx.target.under).name] ~= true then 573 + return false 574 + end 575 + local timeline,sounds = {}, {} 576 + local tunnel_depth = math.random(5,9) * ctx.stats.power 577 + local cname = ctx.caster:get_player_name() 578 + local cut = function(step,s,te,tl) 579 + local smash = function(pos) 580 + if not allowed[minetest.get_node(pos).name] then return end 581 + if minetest.is_protected(pos, cname) then return end 582 + s.visual { 583 + amount = math.random(32,48), time = 0.2, glow = 14; 584 + texture = sorcery.lib.image('sorcery_spark.png'):glow(sorcery.lib.color(10,20,255)):render(); 585 + minpos = vector.subtract(pos, {x=0.5,y=0.5,z=0.5}); 586 + maxpos = vector.add (pos, {x=0.5,y=0.5,z=0.5}); 587 + minvel = {x = -0.3, y = -0.3, z = -0.3}; 588 + maxvel = {x = 0.3, y = 0.3, z = 0.3}; 589 + minacc = {x = -0.6, y = -0.6, z = -0.6}; 590 + maxacc = {x = 0.6, y = 0.6, z = 0.6}; 591 + minexptime = 0.4, maxexptime = 1.2; 592 + minsize = 0.3, maxsize = 1.2; 593 + animation = { 594 + type = 'vertical_frames', length = 1.3; 595 + aspect_w = 16, aspect_h = 16; 596 + }; 597 + } 598 + minetest.dig_node(pos) 599 + if math.random(5) == 1 then 600 + minetest.set_node(pos, {name='sorcery:air_flash_' .. tostring(math.random(10))}) 601 + end 602 + -- TODO visuals 603 + end 604 + local r = s.tunnel_radius 605 + local yaw = {x=0,y=s.tunnel_angle,z=0} 606 + s.visual { 607 + amount = 16, time = 3, glow = 14; 608 + texture = sorcery.lib.image('sorcery_sparking.png'):glow(sorcery.lib.color(20,60,255)):render(); 609 + minpos = vector.subtract(s.anchor, {x=r,y=r,z=r}); 610 + maxpos = vector.add (s.anchor, {x=r,y=r,z=r}); 611 + minvel = {x = -0.1, y = -0.1, z = -0.1}; 612 + maxvel = {x = 0.1, y = 0.1, z = 0.1}; 613 + minexptime = 1.0, maxexptime = 1.4; 614 + minsize = 1.5, maxsize = 4; 615 + animation = { 616 + type = 'vertical_frames', length = 1.5; 617 + aspect_w = 64, aspect_h = 64; 618 + }; 619 + } 620 + for x=-r,r do for y=-r,r do 621 + local xs = x < 0 and -1 or 1 622 + local ys = y < 0 and -1 or 1 623 + if x^2 + y^2 <= r^2 then 624 + if (x+xs)^2 + y^2 > r^2 or 625 + (y+ys)^2 + x^2 > r^2 then 626 + -- we're right at the edge - make a mess 627 + if math.random(5) == 1 then goto skip end 628 + end 629 + local p = vector.add(s.anchor,vector.rotate({x=x,y=y,z=0},yaw)) 630 + smash(p) 631 + end 632 + ::skip::end end 633 + -- if math.random(1,10) == 1 then 634 + -- s.tunnel_angle = s.tunnel_angle + math.random(-0.05,0.05) 635 + -- yaw.y = s.tunnel_angle 636 + -- end 637 + if math.random(1,21) == 1 then 638 + s.tunnel_radius = math.min(6,math.max(3,s.tunnel_radius + math.random(-1,1))) 639 + end 640 + local dir = vector.rotate({x=0,y=0,z=1},yaw) 641 + if sorcery.lib.math.vdcomp(1, dir) < 1 then 642 + dir = vector.normalize(dir) 643 + end 644 + s.anchor = vector.add(s.anchor,dir) 645 + end 646 + local tp = 0 647 + for i=1,tunnel_depth do 648 + local now = {whence=0,secs=tp} 649 + timeline[now] = function(...) cut(i,...) end 650 + sounds[now] = { 651 + sound='sorcery_crunch', where='pos'; 652 + ephemeral=true, gain = math.random(3,10) * 0.1; 653 + } 654 + tp = tp + (math.random(2,5) * 0.1) 655 + end 656 + sounds[1] = {sound='sorcery_powerdown', where='pos'} 657 + sorcery.spell.cast { 658 + caster = ctx.caster; 659 + duration = tp; 660 + timeline = timeline, sounds = sounds; 661 + -- spell state 662 + anchor = ctx.target.under; 663 + tunnel_angle = ctx.caster:get_look_horizontal(); 664 + tunnel_radius = math.floor(math.random(3,5) * (ctx.stats.power * 0.1)); 665 + } 666 + end; 420 667 }; 421 668 emerald = { 422 669 name = 'Boring'; 423 670 desc = 'Release the force of this amulet to punch a deep borehole down into the earth below'; 424 - } 671 + }; 672 + amethyst = { 673 + name = 'Shaftcutting'; 674 + desc = 'Cut a wide shaft up into the ceiling of a cavern'; 675 + }; 425 676 }; 426 677 }; 427 678 genesis = { 428 679 name = 'Genesis'; 429 680 tone = {235,0,175}; 430 681 minpower = 5; 431 - rarity = 50; 682 + rarity = 25; 432 683 amulets = { 433 684 mese = { 685 + mingrade = 4; 434 686 name = 'Duplication'; 435 687 desc = 'Generate a copy of any object or item, no matter how common or rare'; 436 688 }; 437 689 }; 438 690 }; 439 691 luminate = { 440 692 name = 'Luminate'; 441 693 tone = {255,194,0}; 442 694 minpower = 1; 443 - rarity = 25; 695 + rarity = 5; 444 696 amulets = { 445 697 luxite = { 446 698 name = 'Glow'; 447 699 desc = 'Swathe yourself in an aura of sparkling radiance, casting light upon all the dark places where you voyage'; 448 700 }; 449 701 diamond = { 450 702 name = 'Radiance'; ................................................................................ 459 711 }; 460 712 }; 461 713 }; 462 714 dominate = { 463 715 name = 'Dominate'; 464 716 tone = {235,0,228}; 465 717 minpower = 4; 466 - rarity = 40; 718 + rarity = 20; 467 719 amulets = { 468 720 amethyst = { 469 721 name = 'Suffocation'; 470 722 desc = 'Wrap this spell tightly around your victim\'s throat, cutting off their oxygen until you release them.'; 471 723 }; 472 724 emerald = { 473 725 name = 'Caging'; 474 726 desc = 'Trap your victim in an impenetrable field of force, leaving them with no way out but translocation or waiting for the field to release them'; 475 727 }; 728 + luxite = { 729 + name = 'Vampiric Aura'; 730 + desc = 'For a time, anyone who approaches you, whether friend or foe, will suffer immediate retaliation as they are quickly sapped of their vital force in order to replenish your own'; 731 + }; 476 732 ruby = { 477 733 name = 'Exsanguination'; 478 734 desc = 'Rip the life force out of another, leaving them on the brink of death, and use it to mend your own wounds and invigorate your being'; 479 735 cast = function(ctx) 480 736 if not (ctx.target and ctx.target.type == 'object') then return false end 481 737 local tgt = ctx.target.ref 482 738 local takefac = math.min(99,50 + (ctx.stats.power * 5)) / 100 483 739 local dmg = tgt:get_hp() * takefac 484 - print("!!! dmg calc",takefac,dmg,tgt:get_hp()) 485 740 486 741 local numhits = math.random(6,10+ctx.stats.power/2) 487 742 local function dohit(hitsleft) 488 743 if tgt == nil or tgt:get_properties() == nil then return end 489 744 tgt:punch(ctx.caster, 1, { 490 745 full_punch_interval = 1; 491 746 damage_groups = { fleshy = dmg / numhits }
Modified displacer.lua from [e06af858b3] to [ec3123df49].
120 120 on_metadata_inventory_put = function(pos) 121 121 minetest.get_node_timer(pos):start(1) 122 122 end; 123 123 on_timer = function(pos,delta) 124 124 local meta = minetest.get_meta(pos) 125 125 if not meta:contains('active-device') then return false end 126 126 127 + local probe = sorcery.spell.probe(pos) 128 + if probe.disjunction then return true end 129 + 127 130 local inv = meta:get_inventory() 128 131 if inv:is_empty('cache') then return false end 129 132 130 133 local dev = gettxr(pos) 131 134 local active = minetest.string_to_pos(meta:get_string('active-device')) 132 135 133 136 local ad ................................................................................ 143 146 if ad.partner then 144 147 remote = gettxr(ad.partner) 145 148 elseif ad.code then 146 149 local net = sorcery.farcaster.junction(pos,constants.xmit_wattage) 147 150 for _,n in pairs(net) do 148 151 for _,d in pairs(n.caps.net.devices.consume) do 149 152 if d.id == 'sorcery:displacer' then 150 - local t = gettxr(d.pos) 151 - for _,d in pairs(t.connections) do 152 - if d.mode == 'receive' and d.code then 153 - local match = true 154 - for i=1,#d.code do 155 - if d.code[i] ~= ad.code[i] then 156 - match = false break 153 + local dp = sorcery.spell.probe(d.pos) 154 + if not dp.disjunction then 155 + local t = gettxr(d.pos) 156 + for _,d in pairs(t.connections) do 157 + if d.mode == 'receive' and d.code then 158 + local match = true 159 + for i=1,#d.code do 160 + if d.code[i] ~= ad.code[i] then 161 + match = false break 162 + end 157 163 end 158 - end 159 - if match then 160 - remote = t 161 - break 164 + if match then 165 + remote = t 166 + break 167 + end 162 168 end 163 169 end 164 170 end 165 171 end 166 172 if remote then break end 167 173 end 168 174 if remote then break end
Modified enchanter.lua from [b41a4d81d6] to [5ad2e81269].
400 400 if puncher == nil then return end -- i don't know why 401 401 -- this is necessary but you get rare crashes without it 402 402 403 403 -- perform leyline checks and call notify if necessary 404 404 if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then 405 405 sorcery.lib.node.notifyneighbors(pos) 406 406 end 407 + 408 + -- is there an active disjunction in effect here? 409 + -- if so, return immediately and perform no magic 410 + local probe = sorcery.spell.probe(pos) 411 + if probe.disjunction then return end 407 412 408 413 -- we're goint to do something VERY evil here and 409 414 -- replace the air with a "glow-air" that removes 410 415 -- itself after a short period of time, to create 411 416 -- a flash of light when an enchanted tool's used 412 417 -- to dig out a node 413 418 local tool = puncher:get_wielded_item()
Modified forcefield.lua from [a330742505] to [2ecc37ddb4].
123 123 }; 124 124 on_construct = function(pos) 125 125 minetest.get_node_timer(pos):start(1) 126 126 end; 127 127 on_timer = function(pos,delta) 128 128 local orientation = math.floor(minetest.get_node(pos).param2 / 4) 129 129 local costs = calc_cost(pos,delta) 130 + local probe = sorcery.spell.probe(pos) 131 + if probe.disjunction then return true end 130 132 local l = sorcery.ley.netcaps(pos,delta) 131 133 if l.self.powerdraw >= costs.mincost then 132 134 local dist = l.self.powerdraw / (constants.cost_per_barrier * delta) 133 135 for i=1,math.floor(dist) do 134 136 local t = costs.targets[i] 135 137 local str = math.min(0xFF,t[2] + 50*delta); 136 - minetest.swap_node(t[1], { 137 - name = 'sorcery:air_barrier_' .. math.max(1, math.floor(10*(str/0xFF))); 138 - param2 = str; 139 - }) 140 - minetest.get_node_timer(t[1]):start(1) 138 + local fprobe = sorcery.spell.probe(t[1]) 139 + if not fprobe.disjunction then 140 + minetest.swap_node(t[1], { 141 + name = 'sorcery:air_barrier_' .. math.max(1, math.floor(10*(str/0xFF))); 142 + param2 = str; 143 + }) 144 + minetest.get_node_timer(t[1]):start(1) 145 + end 141 146 end 142 147 143 148 local pn = vector.add(pos, vector.divide(costs.aim,2)); 144 149 local pp = vector.add(pn, pofstbl[orientation]) 145 150 pn = vector.subtract(pn, pofstbl[orientation]) 146 151 147 152 minetest.add_particlespawner {
Modified gems.lua from [58885d2bb0] to [42c4d86138].
65 65 -- implement this logic themselves (for instance to check a range) 66 66 if (probe.disjunction and not sp.ignore_disjunction) then return nil end 67 67 68 68 local ctx = { 69 69 caster = user; 70 70 target = target; 71 71 stats = stats; 72 - amulet = stack; 72 + wield = stack; 73 + amulet = stack:get_definition()._sorcery.amulet; 73 74 meta = stack:get_meta(); -- avoid spell boilerplate 74 75 color = sorcery.lib.color(sp.tone); 75 76 today = minetest.get_day_count(); 76 77 probe = probe; 77 78 heading = { 78 79 pos = user:get_pos(); 79 80 yaw = user:get_look_dir(); ................................................................................ 81 82 angle = user:get_look_horizontal(); 82 83 eyeheight = user:get_properties().eye_height; 83 84 }; 84 85 85 86 sound = "xdecor_enchanting"; --FIXME make own sounds 86 87 sparkle = true; 87 88 } 88 - print('casting') 89 89 local res = sp.cast(ctx) 90 90 91 91 if res == nil or res == true then 92 92 minetest.sound_play(ctx.sound, { 93 93 pos = user:get_pos(); 94 94 gain = 1; 95 95 }) ................................................................................ 99 99 end 100 100 if res == nil then 101 101 if not minetest.check_player_privs(user, 'sorcery:infinirune') then 102 102 sorcery.amulet.setrune(stack) 103 103 end 104 104 end 105 105 106 - return ctx.amulet 106 + return ctx.wield 107 107 end; 108 108 minetest.register_craftitem(amuletname, { 109 109 description = sorcery.lib.str.capitalize(name) .. ' amulet'; 110 110 inventory_image = img_sparkle:blit(img_stone):render(); 111 111 wield_scale = { x = 0.6, y = 0.6, z = 0.6 }; 112 112 groups = { sorcery_amulet = 1 }; 113 113 on_use = useamulet;
Modified gravitator.lua from [8ccd951dd6] to [64c5633477].
77 77 }; 78 78 on_construct = function(pos) 79 79 setmeta(pos,'off') 80 80 end; 81 81 on_timer = function(pos) 82 82 if p.color == nil then return false end 83 83 84 - local vee = {x=0,y=-1,z=0}; 84 + local probe = sorcery.spell.probe(pos) 85 + if probe.disjunction then return true end 86 + 87 + local vee = {x=0,y=-1,z=0} 85 88 minetest.add_particlespawner { 86 89 amount = 128; 87 90 time = 4; 88 91 minpos = vector.subtract(pos,radius); 89 92 maxpos = vector.add(pos,radius); 90 93 minvel = vector.multiply(vee, p.factor*0.5); 91 94 maxvel = vector.multiply(vee, p.factor);
Modified harvester.lua from [ae53588867] to [da59e16e73].
34 34 }; 35 35 36 36 on_timer = function(pos,elapse) 37 37 local meta = minetest.get_meta(pos) 38 38 local inv = meta:get_inventory() 39 39 if inv:is_empty('charge') then return false end 40 40 41 + local probe = sorcery.spell.probe(pos) 42 + if probe.disjunction then return true end 43 + 41 44 local put_in_hopper = sorcery.lib.node.discharger(pos) 42 45 local discharge = function(item,idx) 43 46 inv:set_stack('charge',idx,put_in_hopper(item)) 44 47 end 45 48 46 49 local ley = sorcery.ley.estimate(pos) 47 50 local charged = false
Modified infuser.lua from [13d8509748] to [c4c14b0c08].
74 74 local infuser_timer = function(pos, elapsed) 75 75 local meta = minetest.get_meta(pos) 76 76 77 77 local inv = meta:get_inventory() 78 78 local infusion = inv:get_list('infusion') 79 79 local potions = inv:get_list('potions') 80 80 local elixir = infusion[1]:get_definition() 81 + local probe = sorcery.spell.probe(pos) 82 + if probe.disjunction then return true end 83 + 81 84 local potionct = 0 82 85 83 86 do 84 87 local ingredient -- *eyeroll* 85 88 if infusion[1]:is_empty() then goto cancel end 86 89 ingredient = infusion[1]:get_name() 87 90 for i = 1,#potions do
Modified lib/image.lua from [680d9162e2] to [32e08434b2].
51 51 end; 52 52 53 53 transform = function(self, kind) 54 54 return image.change(self, { 55 55 fx = sorcery.lib.tbl.append(self.fx, {'transform' .. tostring(kind)}) 56 56 }) 57 57 end; 58 + 59 + glow = function(self,color) return self:blit(self:multiply(color)) end; 58 60 } end; 59 61 } 60 62 return image
Added lib/math.lua version [80f52c4aaf].
1 +local fn = {} 2 + 3 +fn.vsep = function(vec) -- separate a vector into a direction + magnitude 4 + local magnitude = math.max(math.abs(vec.x), math.abs(vec.y), math.abs(vec.z)) 5 + local inv = 1 / magnitude 6 + return vector.multiply(vec,inv), magnitude 7 +end 8 + 9 +fn.vdcomp = function(dist,v1,v2) -- compare the distance between two points 10 + -- (cheaper than calculating distance outright) 11 + local d if v2 12 + then d = vector.subtract(v1,v2) 13 + else d = v1 14 + end 15 + local dsq = (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2) 16 + return dsq / (dist^2) 17 + -- [0,1) == less then 18 + -- 1 == equal 19 + -- >1 == greater than 20 +end 21 + 22 +return fn
Modified portal.lua from [daf126a7a3] to [12b64fac56].
274 274 local dev = portal_composition(pos) 275 275 if not dev then return false end 276 276 local dsp = portal_disposition(dev) 277 277 local crc = portal_circuit(pos) 278 278 local cap = sorcery.ley.netcaps(pos,delta) 279 279 local tune = sorcery.attunement.verify(pos) 280 280 local partner -- raw position of partner node, if any 281 + local probe = sorcery.spell.probe(pos) 281 282 if tune and tune.partner then 282 283 minetest.load_area(tune.partner) 283 284 -- we are attuned to a partner, but is it in the circuit? 284 285 for _,v in pairs(crc) do 285 286 if vector.equals(v.pos,tune.partner) then 286 287 partner = tune.partner 287 288 break ................................................................................ 291 292 292 293 if cap.self.minpower ~= cap.self.powerdraw then return true end 293 294 294 295 -- clean out user table 295 296 for name,user in pairs(portal_context.users) do 296 297 if user and vector.equals(user.portal, pos) then 297 298 local found = false 298 - for _,u in pairs(dsp.users) do 299 - if u.object:get_player_name() == name then 300 - found = true 299 + if not probe.disjunction then 300 + for _,u in pairs(dsp.users) do 301 + if u.object:get_player_name() == name then 302 + found = true 303 + end 301 304 end 302 305 end 303 306 if not found then 304 307 if user.sound then minetest.sound_fade(user.sound,1,0) end 305 308 portal_context.users[name] = nil 306 309 end 307 310 end 308 311 end 312 + if probe.disjunction then return true end 309 313 310 314 -- one user per pad only! 311 315 for _,n in pairs(dev.nodes) do 312 316 for _,u in pairs(dsp.users) do 313 317 if u.slot == n then 314 318 local pname = u.object:get_player_name() 315 319 if not portal_context.users[pname] then
Modified runeforge.lua from [c8e0c3ac03] to [f2cf52d41a].
47 47 }; 48 48 _proto = { id = name, data = rune; }; 49 49 }) 50 50 end) 51 51 52 52 for name,p in pairs(constants.phial_kinds) do 53 53 local f = string.format 54 - local color = sorcery.lib.color(204,38,235) 54 + local color = sorcery.lib.color(142,232,0) 55 55 local fac = p.grade / 6 56 56 local id = f('phial_%s', name); 57 57 sorcery.register_potion_tbl { 58 58 name = id; 59 59 label = f('%s Phial',p.name); 60 60 desc = "A powerful liquid consumed in the operation of a rune forge. Its quality determines how fast new runes can be constructed and how much energy is required by the process."; 61 61 color = color:brighten(1 + fac*0.5); ................................................................................ 226 226 frame = proto.frame; 227 227 framestats = proto.frame and sorcery.data.metals[proto.frame].amulet; 228 228 tone = sorcery.lib.color(rd.tone); 229 229 base_spell = base_spell; 230 230 } 231 231 end 232 232 233 - 234 233 local runeforge_update = function(pos,time) 235 234 local m = minetest.get_meta(pos) 236 235 local i = m:get_inventory() 237 236 local l = sorcery.ley.netcaps(pos,time or 1) 237 + local probe = sorcery.spell.probe(pos) 238 238 239 239 local pow_min = l.self.powerdraw >= l.self.minpower 240 240 local pow_max = l.self.powerdraw >= l.self.maxpower 241 241 local has_phial = function() return not i:is_empty('phial') end 242 242 243 - if time and has_phial() and pow_min then -- roll for runes 243 + if time and has_phial() and pow_min and not probe.disjunction then -- roll for runes 244 244 local int, powerfac = calc_phial_props(i:get_stack('phial',1)) 245 245 local rolls = math.floor(time/int) 246 246 local newrunes = {} 247 247 for _=1,rolls do 248 248 local choices = {} 249 249 for name,rune in pairs(sorcery.data.runes) do 250 - print('considering',name) 251 - print('-- power',rune.minpower,(rune.minpower*powerfac)*time,'//',l.self.powerdraw,l.self.powerdraw/time,'free',l.freepower,'max',l.maxpower) 250 + -- print('considering',name) 251 + -- print('-- power',rune.minpower,(rune.minpower*powerfac)*time,'//',l.self.powerdraw,l.self.powerdraw/time,'free',l.freepower,'max',l.maxpower) 252 252 if (rune.minpower*powerfac)*time <= l.self.powerdraw and math.random(rune.rarity) == 1 then 253 - local n = ItemStack(rune.item) 254 - choices[#choices + 1] = n 253 + choices[#choices + 1] = rune 254 + end 255 + end 256 + if #choices > 0 then 257 + -- if multiple runes were rolled up, be nice to the player 258 + -- and pick the rarest one to give them 259 + local rare, choice = 0 260 + for i,c in pairs(choices) do 261 + if c.rarity > rare then 262 + rare = c.rarity 263 + choice = c 264 + end 255 265 end 266 + newrunes[#newrunes + 1] = ItemStack(choice.item) 256 267 end 257 - if #choices > 0 then newrunes[#newrunes + 1] = choices[math.random(#choices)] end 258 - print('rune choices:',dump(choices)) 259 - print('me',dump(l.self)) 268 + -- print('rune choices:',dump(choices)) 269 + -- print('me',dump(l.self)) 260 270 end 261 271 262 272 for _,r in pairs(newrunes) do 263 273 if i:room_for_item('cache',r) and has_phial() then 264 274 local qual = math.random(#constants.rune_grades) 265 275 rune_set(r,{grade = qual}) 266 276 i:add_item('cache',r) ................................................................................ 284 294 list[context;phial;7.25,1.75;1,1;] 285 295 list[context;refuse;8.50,1.75;1,1;] 286 296 287 297 list[current_player;main;0.25,3;8,4;] 288 298 289 299 image[0.25,0.50;1,1;sorcery_statlamp_%s.png] 290 300 ]], (10.5 - constants.rune_cache_max*1.25)/2, constants.rune_cache_max, 291 - ((has_phial and pow_max) and 'green' ) or 292 - ((has_phial and pow_min) and 'yellow') or 'off') 301 + ((not (has_phial and pow_min)) and 'off' ) or 302 + ( probe.disjunction and 'blue' ) or 303 + ((has_phial and pow_max) and 'green') or 'yellow') 293 304 294 305 local ghost = function(slot,x,y,img) 295 306 if i:is_empty(slot) then spec = spec .. string.format([[ 296 307 image[%f,%f;1,1;%s.png] 297 308 ]], x,y,img) end 298 309 end 299 310 ................................................................................ 446 457 allow_metadata_inventory_take = function(pos,list,idx,stack,user) 447 458 if list == 'amulet' or list == 'wrench' then return 1 end 448 459 if list == 'phial' or list == 'refuse' then return stack:get_count() end 449 460 return 0 450 461 end; 451 462 allow_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user) 452 463 local inv = minetest.get_meta(pos):get_inventory() 464 + local probe = sorcery.spell.probe(pos) 453 465 local wrench if not inv:is_empty('wrench') then 454 466 wrench = inv:get_stack('wrench',1):get_definition()._proto 455 467 end 456 468 if fl == 'cache' then 469 + if probe.disjunction then return 0 end 457 470 if tl == 'cache' then return 1 end 458 471 if tl == 'active' and inv:is_empty('active') then 459 472 print(dump(wrench)) 460 473 if wrench and wrench.powers.imbue and not inv:is_empty('amulet') then 461 474 local amulet = inv:get_stack('amulet',1) 462 475 local rune = inv:get_stack(fl,fi) 463 476 local runeid = rune:get_definition()._proto.id ................................................................................ 474 487 return 1 475 488 end 476 489 end 477 490 end 478 491 end 479 492 end 480 493 if fl == 'active' then 494 + if probe.disjunction then return 0 end 481 495 if tl == 'cache' and wrench and (wrench.powers.extract or wrench.powers.purge) then return 1 end 482 496 end 483 497 return 0 484 498 end; 485 499 }) 486 500 487 501 do local m = sorcery.data.metals
Added sounds/sorcery_disjoin_bg.ogg version [b5199cb6ff].
cannot compute difference between binary files
Added sounds/sorcery_hurl.ogg version [36a1bec900].
cannot compute difference between binary files
Added sounds/sorcery_powerdown.ogg version [12cb1c05f3].
cannot compute difference between binary files
Added spell.lua version [cfeba0d2de].
1 +-- this file is used to track active spells, for the purposes of metamagic 2 +-- like disjunction. a "spell" is a table consisting of several properties: 3 +-- a "disjoin" function that, if present, is called when the spell is 4 +-- abnormally interrupted, a "terminate" function that calls when the spell 5 +-- completes, a "duration" property specifying how long the spell lasts in 6 +-- seconds, and a "timeline" table that maps floats to functions called at 7 +-- specific points during the function's activity. it can also have a 8 +-- 'delay' property that specifies how long to wait until the spell sequence 9 +-- starts; the spell is however still vulnerable to disjunction during this 10 +-- period. there can also be a sounds table that maps timepoints to sounds 11 +-- the same way timeline does. each value should be a table of form {sound, 12 +-- where}. the `where` field may contain one of 'pos', 'caster', 'subjects', or 13 +-- a vector specifying a position in the world, and indicate where the sound 14 +-- should be played. by default 'caster' and 'subjects' sounds will be attached 15 +-- to the objects they reference; 'attach=false' can be added to prevent this. 16 +-- by default sounds will be faded out quickly when disjunction occurs; this 17 +-- can be controlled by the fade parameter. 18 +-- 19 +-- spells can have various other properties, for instance 'disjunction', which 20 +-- when true prevents other spells from being cast in its radius while it is 21 +-- still in effect. disjunction is absolute; there is no way to overwhelm it. 22 +-- 23 +-- the spell also needs at least one of "anchor", "subjects", or "caster". 24 +-- * an anchor is a position that, in combination with 'range', specifies the area 25 +-- where a spell is in effect; this is used for determining whether it 26 +-- is affected by a disjunction that incorporates part of that position 27 +-- * subjects is an array of individuals affected by the spell. when 28 +-- disjunction is cast on one of them, they will be removed from the 29 +-- table. each entry should have at least a 'player' field; they can 30 +-- also contain any other data useful to the spell. if a subject has 31 +-- a 'disjoin' field it must be a function called when they are removed 32 +-- from the list of spell targets. 33 +-- * caster is the individual who cast the spell, if any. a disjunction 34 +-- against their person will totally disrupt the spell. 35 +local log = function(...) sorcery.log('spell',...) end 36 + 37 +-- FIXME saving object refs is iffy, find a better alternative 38 +sorcery.spell = { 39 + active = {} 40 +} 41 + 42 +local get_spell_positions = function(spell) 43 + local spellpos 44 + if spell.anchor then 45 + spellpos = {spell.anchor} 46 + elseif spell.attach then 47 + if spell.attach == 'caster' then 48 + spellpos = {spell.caster:get_pos()} 49 + elseif spell.attach == 'subjects' or spell.attach == 'both' then 50 + if spell.attach == 'both' then 51 + spellpos = {spell.caster:get_pos()} 52 + else spellpos = {} end 53 + for _,s in pairs(spell.subjects) do 54 + spellpos[#spellpos+1] = s.player:get_pos() 55 + end 56 + else spellpos = {spell.attach:get_pos()} end 57 + else assert(false) end 58 + return spellpos 59 +end 60 + 61 +local inspellrange = function(spell,pos,range) 62 + local spellpos = get_spell_positions(spell) 63 + 64 + for _,p in pairs(spellpos) do 65 + if vector.equals(pos,p) or 66 + (range and sorcery.lib.math.vdcomp(range, pos,p)<=1) or 67 + (spell.range and sorcery.lib.math.vdcomp(spell.range,p,pos)<=1) then 68 + return true 69 + end 70 + end 71 + return false 72 +end 73 + 74 +sorcery.spell.probe = function(pos,range) 75 + -- this should be called before any magical effects are performed. 76 + -- other mods can overlay their own functions to e.g. protect areas 77 + -- from magic 78 + local result = {} 79 + 80 + -- first we need to check if any active injunctions are in effect 81 + -- injunctions are registered as spells with a 'disjunction = true' 82 + -- property 83 + for id,spell in pairs(sorcery.spell.active) do 84 + if not (spell.disjunction and (spell.anchor or spell.attach)) then goto skip end 85 + if inspellrange(spell,pos,range) then 86 + result.disjunction = true 87 + break 88 + end 89 + ::skip::end 90 + 91 + -- at some point we might also check to see if certain anti-magic 92 + -- blocks are nearby or suchlike. there should also be regions where 93 + -- perhaps certain kinds of magic are unusually empowered or weak 94 + -- (perhaps drawing on leyline affinity) 95 + return result 96 +end 97 +sorcery.spell.disjoin = function(d) 98 + local spells,targets = {},{} 99 + if d.spell then spells = {{v=d.spell}} 100 + elseif d.target then targets = {d.target} 101 + elseif d.pos then -- find spells anchored here and people in range 102 + for id,spell in pairs(sorcery.spell.active) do 103 + if not spell.anchor then goto skip end -- this intentionally excludes attached spells 104 + if inspellrange(spell,d.pos,d.range) then 105 + spells[#spells+1] = {v=spell,i=id} 106 + end 107 + ::skip::end 108 + local ppl = minetest.get_objects_inside_radius(d.pos,d.range) 109 + if #targets == 0 then targets = ppl else 110 + for _,p in pairs(ppl) do targets[#targets+1] = p end 111 + end 112 + end 113 + 114 + -- iterate over targets to remove from any spell's influence 115 + for _,t in pairs(targets) do 116 + for id,spell in pairs(sorcery.spell.active) do 117 + if spell.caster == t then spells[#spells+1] = {v=spell,i=id} else 118 + for si, sub in pairs(spell.subjects) do 119 + if sub.player == t then 120 + if sub.disjoin then sub:disjoin(spell) end 121 + spell.release_subject(si) 122 + break 123 + end 124 + end 125 + end 126 + end 127 + end 128 + 129 + -- spells to disjoin entirely 130 + for _,s in pairs(spells) do local spell = s.v 131 + if spell.disjoin then spell:disjoin() end 132 + spell.abort() 133 + if s.i then sorcery.spell.active[s.i] = nil else 134 + for k,v in pairs(sorcery.spell.active) do 135 + if v == spell then sorcery.spell.active[k] = nil break end 136 + end 137 + end 138 + end 139 +end 140 + 141 +-- when a new spell is created, we analyze it and make the appropriate calls 142 +-- to minetest.after to queue up the events. each job returned needs to be 143 +-- saved in 'jobs' so they can be canceled if the spell is disjoined. no polling 144 +-- necessary :D 145 + 146 +sorcery.spell.cast = function(proto) 147 + local s = table.copy(proto) 148 + s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {} 149 + s.impacts = s.impacts or {} s.subjects = s.subjects or {} 150 + s.delay = s.delay or 0 151 + s.visual = function(def,subj) 152 + s.vfx[#s.vfx + 1] = { 153 + handle = minetest.add_particlespawner(def); 154 + subject = subj; 155 + } 156 + end 157 + s.visual_caster = function(def) -- convenience function 158 + local d = table.copy(def) 159 + d.attached = s.caster 160 + s.visual(d) 161 + end 162 + s.visual_subjects = function(def) 163 + for _,sub in pairs(s.subjects) do 164 + local d = table.copy(def) 165 + d.attached = sub.player 166 + s.visual(d,sub) 167 + end 168 + end 169 + s.affect = function(i) 170 + local etbl = {} 171 + for _,sub in pairs(s.subjects) do 172 + local eff = late.new_effect(sub.player, i) 173 + local rec = { 174 + effect = eff; 175 + subject = sub; 176 + } 177 + s.impacts[#s.impacts+1] = rec 178 + etbl[#etbl+1] = rec 179 + end 180 + return etbl 181 + end 182 + s.abort = function() 183 + for _,j in ipairs(s.jobs) do j:cancel() end 184 + for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end 185 + for _,i in ipairs(s.sfx) do s.silence(i) end 186 + for _,i in ipairs(s.impacts) do i.effect:stop() end 187 + end 188 + s.release_subject = function(si) 189 + local t = s.subjects[si] 190 + print('releasing against',si,t) 191 + for _,f in pairs(s.sfx) do if f.subject == t then s.silence(f) end end 192 + for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end 193 + for _,f in pairs(s.vfx) do 194 + if f.subject == t then minetest.delete_particlespawner(f.handle) end 195 + end 196 + s.subjects[si] = nil 197 + end 198 + local interpret_timespec = function(when) 199 + local t if type(when) == 'number' then 200 + t = s.duration * when 201 + else 202 + t = (s.duration * (when.whence or 0)) + when.secs 203 + end 204 + if t then return math.min(s.duration,math.max(0,t)) end 205 + 206 + log('invalid timespec ' .. dump(when)) 207 + return 0 208 + end 209 + s.queue = function(when,fn) 210 + local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0 211 + local timepast = interpret_timespec(when) 212 + if not timepast then timepast = 0 end 213 + local timeleft = s.duration - timepast 214 + local howlong = (s.delay + timepast) - elapsed 215 + if howlong < 0 then 216 + log('cannot time-travel! queue() called with `when` specifying timepoint that has already passed') 217 + howlong = 0 218 + end 219 + s.jobs[#s.jobs+1] = minetest.after(howlong, function() 220 + -- this is somewhat awkward. since we're using a non-polling approach, we 221 + -- need to find a way to account for a caster or subject walking into an 222 + -- existing antimagic field, or someone with an existing antimagic aura 223 + -- walking into range of the anchor. so every time a spell effect would 224 + -- take place, we first check to see if it's in range of something nasty 225 + if not s.disjunction and -- avoid self-disjunction 226 + (s.caster and sorcery.spell.probe(s.caster:get_pos()).disjunction) or 227 + (s.anchor and sorcery.spell.probe(s.anchor,s.range).disjunction) then 228 + sorcery.spell.disjoin{spell=s} 229 + else 230 + if not s.disjunction then for _,sub in pairs(s.subjects) do 231 + local sp = sub.player:get_pos() 232 + if sorcery.spell.probe(sp).disjunction then 233 + sorcery.spell.disjoin{pos=sp} 234 + end 235 + end end 236 + -- spell still exists and we've removed any subjects who have been 237 + -- affected by a disjunction spell, it's now time to actually perform 238 + -- the queued-up action 239 + fn(s,timepast,timeleft) 240 + end 241 + end) 242 + end 243 + s.play_now = function(spec) 244 + local specs, stbl = {}, {} 245 + local addobj = function(obj,sub) 246 + if spec.attach == false then specs[#specs+1] = { 247 + spec = { pos = obj:get_pos() }; 248 + obj = obj, subject = sub; 249 + } else specs[#specs+1] = { 250 + spec = { object = obj }; 251 + obj = obj, subject = sub; 252 + } end 253 + end 254 + 255 + if spec.where == 'caster' then addobj(s.caster) 256 + elseif spec.where == 'subjects' then 257 + for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end 258 + elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} } 259 + else specs[#specs+1] = { spec = {pos = spec.where} } end 260 + 261 + for _,sp in pairs(specs) do 262 + sp.spec.gain = spec.gain 263 + local so = { 264 + handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral); 265 + ctl = spec; 266 + -- object = sp.obj; 267 + subject = sp.subject; 268 + } 269 + stbl[#stbl+1] = so 270 + s.sfx[#s.sfx+1] = so 271 + end 272 + return stbl 273 + end 274 + s.play = function(when,spec) 275 + s.queue(when, function() 276 + local snds = s.play_now(spec) 277 + if spec.stop then 278 + s.queue(spec.stop, function() 279 + for _,snd in pairs(snds) do s.silence(snd) end 280 + end) 281 + end 282 + end) 283 + end 284 + s.silence = function(sound) 285 + if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle) 286 + else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end 287 + end 288 + local startqueued, termqueued = false, false 289 + local myid = #sorcery.spell.active+1 290 + s.cancel = function() 291 + s.abort() 292 + sorcery.spell.active[myid] = nil 293 + end 294 + local perform_disjunction_calls = function() 295 + local positions = get_spell_positions(s) 296 + for _,p in pairs(positions) do 297 + sorcery.spell.disjoin{pos = p, range = s.range} 298 + end 299 + end 300 + if s.timeline then 301 + for when_raw,what in pairs(s.timeline) do 302 + local when = interpret_timespec(when_raw) 303 + if s.delay == 0 and when == 0 then 304 + startqueued = true 305 + if s.disjunction then perform_disjunction_calls() end 306 + what(s,0,s.duration) 307 + elseif when_raw == 1 or when >= s.duration then -- avoid race conditions 308 + if not termqueued then 309 + termqueued = true 310 + s.queue(1,function(s,...) 311 + what(s,...) 312 + if s.terminate then s:terminate() end 313 + sorcery.spell.active[myid] = nil 314 + end) 315 + else 316 + log('multiple final timeline events not possible, ignoring') 317 + end 318 + elseif when == 0 and s.disjunction then 319 + startqueued = true 320 + s.queue(when_raw,function(...) 321 + perform_disjunction_calls() 322 + what(...) 323 + end) 324 + else s.queue(when_raw,what) end 325 + end 326 + end 327 + if s.intervals then 328 + for _,int in pairs(s.intervals) do 329 + local timeleft = s.duration - interpret_timespec(int.after) 330 + local iteration, itercount = 0, timeleft / int.period 331 + local function iterate(lastreturn) 332 + iteration = iteration + 1 333 + local nr = int.fn { 334 + spell = s; 335 + iteration = iteration; 336 + iterationcount = itercount; 337 + timeleft = timeleft; 338 + timeelapsed = s.duration - timeleft; 339 + lastreturn = lastreturn; 340 + } 341 + if nr ~= false and iteration < itercount then 342 + s.jobs[#s.jobs+1] = minetest.after(int.period, 343 + function() iterate(nr) end) 344 + end 345 + end 346 + if int.after 347 + then s.queue(int.after, iterate) 348 + else s.queue({whence=0, secs=s.period}, iterate) 349 + end 350 + end 351 + end 352 + if s.disjunction and not startqueued then 353 + if s.delay == 0 then perform_disjunction_calls() else 354 + s.queue(0, function() perform_disjunction_calls() end) 355 + end 356 + end 357 + if s.sounds then 358 + for when,what in pairs(s.sounds) do s.play(when,what) end 359 + end 360 + sorcery.spell.active[myid] = s 361 + if not termqueued then 362 + s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function() 363 + if s.terminate then s:terminate() end 364 + sorcery.spell.active[myid] = nil 365 + end) 366 + end 367 + s.starttime = minetest.get_server_uptime() 368 + return s 369 +end
Modified textures/sorcery_crackle.png from [faf84cf96c] to [58d455dd12].
cannot compute difference between binary files
Added textures/sorcery_flicker.png version [d2be38b136].
cannot compute difference between binary files
Added textures/sorcery_fog.png version [f33fe7d904].
cannot compute difference between binary files
Added textures/sorcery_glitter.png version [e4de7bf843].
cannot compute difference between binary files
Added textures/sorcery_poof.png version [01b7e56ad7].
cannot compute difference between binary files
Added textures/sorcery_sparking.png version [6a1bae8d93].
cannot compute difference between binary files
Added textures/sorcery_sputter.png version [3a75e25024].
cannot compute difference between binary files
Modified tnodes.lua from [4064266f0d] to [46cef4271e].
18 18 minetest.get_node_timer(pos):start(1) 19 19 end; 20 20 on_timer = function(pos,dtime) 21 21 local meta = minetest.get_meta(pos) 22 22 local elapsed = dtime + meta:get_float('duration') - meta:get_float('timeleft') 23 23 local level = 1 - (elapsed / meta:get_float('duration')) 24 24 local lum = math.ceil(level*meta:get_int('power')) 25 + local probe = sorcery.spell.probe(pos) 26 + if probe.disjunction then 27 + minetest.remove_node(pos) 28 + return false 29 + end 25 30 if lum ~= i then 26 31 if lum <= 0 then 27 32 minetest.remove_node(pos) 28 33 return false 29 34 else 30 35 minetest.swap_node(pos,{name='sorcery:air_glimmer_'..tostring(lum)}) 31 36 end