Overview
Comment: | add duplicate and elevate spells, add more sfx, various tweaks and bugfixes, add object handle class |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
6e106c135c5d0f1556f06759dba2b0f0 |
User & Date: | lexi on 2020-10-30 18:47:34 |
Other Links: | manifest | tags |
Context
2020-10-30
| ||
19:03 | squish oversized sound effect check-in: b96185e88b user: lexi tags: trunk | |
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 | |
Changes
Modified data/runes.lua from [d5ad4a6740] to [3a290dc995].
43 43 texture = sorcery.vfx.glowspark(color):render(); 44 44 animation = { 45 45 type = 'vertical_frames', length = 5.1; 46 46 aspect_w = 16, aspect_h = 16; 47 47 }; 48 48 }); 49 49 end 50 + 51 +local teleport = function(ctx,subjects,delay,pos,color) 52 + if ctx.amulet.frame == 'tungsten' then delay = delay * 0.5 end 53 + color = color or sorcery.lib.color(29,205,247) 54 + local center = ctx.caster:get_pos() 55 + for _,sub in pairs(subjects) do 56 + local s = sub.ref 57 + local offset = vector.subtract(s:get_pos(), center) 58 + local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset)) 59 + if pt then 60 + -- minetest.sound_play('sorcery_stutter', { 61 + -- object = s, gain = 0.8; 62 + -- },true) 63 + local mydelay = sub.delay or (delay + math.random(-10,10)*.1); 64 + local sh = s:get_properties().eye_height 65 + local color = sub.color or color 66 + sorcery.lib.node.preload(pt,s) 67 + sorcery.spell.cast { 68 + name = 'sorcery:translocate'; 69 + duration = mydelay; 70 + caster = ctx.caster; 71 + subjects = {{player=s,dest=sub.dest or pt}}; 72 + timeline = { 73 + [0] = function(sp,_,timeleft) 74 + sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh) 75 + sp.windup = (sp.play_now{ 76 + sound = 'sorcery_windup'; 77 + where = 'subjects'; 78 + gain = 0.4; 79 + fade = 1.5; 80 + })[1] 81 + end; 82 + [0.4] = function(sp,_,timeleft) 83 + sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh) 84 + end; 85 + [0.7] = function(sp,_,timeleft) 86 + sparkle(color,sp,timeleft*80, timeleft, 2,4, sh) 87 + end; 88 + [1] = function(sp) 89 + sp.silence(sp.windup) 90 + minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true) 91 + minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true) 92 + sorcery.vfx.body_sparkle(nil,color:brighten(1.3),2,s:get_pos()) 93 + s:set_pos(pt) 94 + sorcery.vfx.body_sparkle(s,color:darken(0.3),2) 95 + end; 96 + }; 97 + sounds = { 98 + [0] = { sound = 'sorcery_stutter', pos = 'subjects' }; 99 + }; 100 + } 101 + end 102 + end 103 +end 50 104 return { 51 105 translocate = { 52 106 name = 'Translocate'; 53 107 tone = {0,235,233}; 54 108 minpower = 3; 55 - rarity = 10; 109 + rarity = 7; 56 110 amulets = { 57 111 amethyst = { 58 112 name = 'Joining'; 59 - desc = 'Give this amulet to another and they can arrive safely at your side in a flash from anywhere in the world — though returning whence they came may be a more difficult matter'; 113 + desc = 'Give this amulet to another and with a snap of their fingers they can arrive safely at your side from anywhere in the world — though returning whence they came may be a more difficult matter'; 60 114 apply = function(ctx) 61 115 local maker = ctx.user:get_player_name() 62 116 ctx.meta:set_string('rune_join_target',maker) 63 117 end; 64 118 remove = function(ctx) ctx.meta:set_string('rune_join_target','') end; 119 + cast = function(ctx) 120 + local target = minetest.get_player_by_name(ctx.meta:get_string('rune_join_target')) 121 + if not target then return false end 122 + 123 + local subjects if ctx.amulet.frame == 'cobalt' then 124 + if ctx.target.type ~= 'object' then return false end 125 + subjects = {{ref=ctx.target.ref}} 126 + else subjects = {{ref=ctx.caster}} end 127 + 128 + local delay = math.max(5,11 - ctx.stats.power) + 2.3*(math.random()*2-1) 129 + local color = sorcery.lib.color(117,38,237) 130 + teleport(ctx,subjects,delay,target:get_pos(),color) 131 + if ctx.amulet.frame == 'gold' then 132 + teleport(ctx,{{ref=target}},delay,ctx.caster:get_pos()) 133 + else 134 + ctx.sparkle = false 135 + end 136 + end; 65 137 frame = { 138 + tungsten = { 139 + name = 'Quick Joining'; 140 + desc = 'Give this amulet to another and they can arrive safely at your side almost instantaneously from anywhere in the world — though returning whence they came may be a more difficult matter'; 141 + }; 66 142 gold = { 67 143 name = 'Exchange'; 68 144 desc = 'Give this amulet to another and they will be able to trade places with you no matter where in the world each of you might be.'; 69 145 }; 70 146 cobalt = { 71 147 name = 'Sending'; 72 148 desc = 'Give this amulet to another and by wielding this amulet against another they will be able to transport them instantly to your side'; ................................................................................ 91 167 else 92 168 local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest')) 93 169 ctx.meta:set_string('rune_return_dest','') 94 170 local subjects = { ctx.caster } 95 171 local center = ctx.caster:get_pos() 96 172 ctx.sparkle = false 97 173 local delay = math.max(3,10 - ctx.stats.power) + 3*(math.random()*2-1) 98 - if ctx.amulet.frame == 'tungsten' then delay = delay * 0.5 end 99 - for _,s in pairs(subjects) do 100 - local offset = vector.subtract(s:get_pos(), center) 101 - local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset)) 102 - if pt then 103 - -- minetest.sound_play('sorcery_stutter', { 104 - -- object = s, gain = 0.8; 105 - -- },true) 106 - local mydelay = delay + math.random(-10,10)*.1; 107 - local sh = s:get_properties().eye_height 108 - local color = sorcery.lib.color(29,205,247) 109 - sorcery.lib.node.preload(pt,s) 110 - sorcery.spell.cast { 111 - duration = mydelay; 112 - caster = ctx.caster; 113 - subjects = {{player=s,dest=pt}}; 114 - timeline = { 115 - [0] = function(sp,_,timeleft) 116 - sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh) 117 - sp.windup = (sp.play_now{ 118 - sound = 'sorcery_windup'; 119 - where = 'subjects'; 120 - gain = 0.4; 121 - fade = 1.5; 122 - })[1] 123 - end; 124 - [0.4] = function(sp,_,timeleft) 125 - sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh) 126 - end; 127 - [0.7] = function(sp,_,timeleft) 128 - sparkle(color,sp,timeleft*80, timeleft, 2,4, sh) 129 - end; 130 - [1] = function(sp) 131 - sp.silence(sp.windup) 132 - minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true) 133 - minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true) 134 - sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos()) 135 - s:set_pos(pt) 136 - sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2) 137 - end; 138 - }; 139 - sounds = { 140 - [0] = { sound = 'sorcery_stutter', pos = 'subjects' }; 141 - }; 142 - } 143 - end 144 - end 174 + teleport(ctx,{{ref=ctx.caster}},delay,pos) 145 175 end 146 176 end; 147 177 frame = { 148 178 tungsten = { 149 179 name = 'Quick Return'; 150 180 desc = 'Use this amulet once to bind it to a particular place, then discharge its spell to translocate yourself rapidly back to that point from anywhere in the world.'; 151 181 }; ................................................................................ 163 193 name = 'Mass Banishment'; 164 194 desc = 'Use this amulet once to bind it to a particular point in the world, then use it again to seize up everyone surrounding you in the grip of a fearsome magic that will deport them all in the blink of an eye to whatever destination you have chosen'; 165 195 }; 166 196 }; 167 197 }; 168 198 ruby = minetest.get_modpath('beds') and { 169 199 name = 'Escape'; 170 - desc = 'Immediately transport yourself out of a dangerous situation back to the last place you slept'; 200 + desc = 'Immediately transport yourself out of a dangerous situation back to the last place you slept, before anyone has time to net you in a disjunction'; 171 201 cast = function(ctx) 172 202 -- if not beds.spawns then beds.read_spawns() end 173 203 local subjects = {ctx.caster} 174 204 for _,s in pairs(subjects) do 175 205 local spp = beds.spawn[ctx.caster:get_player_name()] 176 206 if spp then 177 207 local oldpos = s:get_pos() 178 - minetest.sound_play('sorcery_splunch', {pos=oldpos}, true) 179 - sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,131),2,oldpos) 180 - s:set_pos(spp) 181 - minetest.sound_play('sorcery_splunch', {pos=spp}, true) 182 - sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,89),2,spp) 208 + local jump = function() 209 + minetest.sound_play('sorcery_splunch', {pos=oldpos}, true) 210 + sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,131),2,oldpos) 211 + s:set_pos(spp) 212 + minetest.sound_play('sorcery_splunch', {pos=spp}, true) 213 + sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,89),2,spp) 214 + end 215 + if ctx.amulet.frame == 'cobalt' then 216 + sorcery.spell.cast { 217 + name = 'sorcery:escape'; 218 + caster = s; 219 + duration = random() * 0.4 + 0.3; 220 + timeline = { 221 + [0] = function() 222 + sorcery.vfx.imbue(sorcery.lib.color(244,38,131), s, 1.3) 223 + end; 224 + [1] = function(sp) 225 + local radius = 6 * ctx.stats.power 226 + local center = sp.caster:get_pos() 227 + local targets = minetest.get_objects_inside_radius(center, radius) 228 + jump() 229 + -- TODO: shockwave visuals 230 + for _,o in pairs(targets) do 231 + if not o:get_armor_groups().immortal then 232 + local distance = vector.distance(o:get_pos(), center) 233 + local dmg = (7 * ctx.stats.power) * (distance / radius) 234 + minetest.punch(ctx.caster, 1.0, { 235 + full_punch_interval = 1.0; 236 + damage_groups = { fleshy = dmg }; 237 + }, vector.direction(o:get_pos(), center)); 238 + end 239 + end 240 + end; 241 + } 242 + } 243 + else jump() end 183 244 end 184 245 -- TODO decide what happens to the people who don't have 185 246 -- respawn points already set 186 247 end 187 248 end; 188 249 frame = { 189 250 cobalt = { ................................................................................ 195 256 desc = 'Break up even the fiercest of quarrels by transporting yourself and everyone around you out of harms\' way and immediately back to the last place each slept'; 196 257 }; 197 258 }; 198 259 }; 199 260 diamond = { 200 261 name = 'Elevation'; 201 262 desc = 'Lift yourself and everything around you high up into the sky'; 263 + cast = function(ctx) 264 + local center = ctx.caster:get_pos() 265 + local up = ((ctx.stats.power * 7) + math.random(6,17)) * (math.random() * 0.4 + 0.4) 266 + if center.y > 0 then up = up + center.y end 267 + local newcenter = vector.new(center.x,up,center.z) 268 + if not sorcery.lib.node.get_arrival_point(newcenter) then return false end 269 + sorcery.lib.node.preload(newcenter,ctx.caster) 270 + local jmpcolor = sorcery.lib.color(0,255,144) 271 + 272 + if not ctx.amulet.frame == 'iridium' then 273 + local where = vector.offset(center,0,1,0) 274 + repeat local ok, nx = minetest.line_of_sight(where, newcenter) 275 + if ok then break end 276 + if minetest.get_node_or_nil(nx) == nil then 277 + minetest.load_area(nx) 278 + where = nx -- save some time 279 + else return false end 280 + until false 281 + end 282 + local lift = function(n) 283 + local dest = vector.new(n.pos.x, up + n.h, n.pos.z) 284 + if sorcery.lib.node.is_clear(dest) then 285 + minetest.set_node(dest, minetest.get_node(n.pos)) 286 + minetest.get_meta(dest):from_table(minetest.get_meta(n.pos):to_table()) 287 + if math.random(5) == 1 then 288 + minetest.set_node(n.pos, {name='sorcery:air_flash_' .. tostring(math.random(10))}) 289 + else minetest.remove_node(n.pos) end 290 + local obs = minetest.get_objects_inside_radius(n.pos, 1.5) 291 + if obs then for _,o in pairs(obs) do 292 + local pt = sorcery.lib.node.get_arrival_point(vector.add(dest, vector.subtract(o:get_pos(),n.pos))) 293 + if pt then 294 + o:set_pos(pt) 295 + sorcery.vfx.body_sparkle(o,jmpcolor:darken(0.3),2) 296 + end 297 + end end 298 + return true 299 + else 300 + return false 301 + end 302 + end 303 + local nodes,sparkles,tmap = {},{},{} 304 + local r = math.ceil((ctx.stats.power * 0.1) * 8 + 3) 305 + for x = -r,r do -- lazy hack to select a sphere 306 + for z = -r,r do 307 + local col = {} 308 + for y = -r,r do 309 + local ofs = vector.new(x,y,z) 310 + if sorcery.lib.math.vdcomp(r,ofs) <= 1 then 311 + local pos = vector.add(center, ofs) 312 + if sorcery.lib.node.is_air(pos) then 313 + if y > 0 then 314 + sparkles[#sparkles+1] = pos 315 + break -- levitation is a sin 316 + end 317 + else 318 + nodes[#nodes+1] = {pos=pos, h=y} 319 + col[#col+1] = {pos=pos, h=y} 320 + end 321 + end 322 + end 323 + if #col > 0 then 324 + local seq = math.floor(math.sqrt((x^2) + (z^2))) 325 + -- TODO find a way to optimise this shitshow 326 + if tmap[seq] 327 + then tmap[seq][#(tmap[seq])+1] = col 328 + else tmap[seq] = {col} 329 + end 330 + end 331 + end end 332 + 333 + -- for _,n in pairs(nodes) do 334 + -- local dest = vector.new(n.pos.x, up + n.h, n.pos.z) 335 + -- if sorcery.lib.node.is_clear(dest) then 336 + -- minetest.set_node(dest, minetest.get_node(n.pos)) 337 + -- minetest.get_meta(dest):from_table(minetest.get_meta(n.pos):to_table()) 338 + -- if math.random(5) == 1 then 339 + -- minetest.set_node(n.pos, {name='sorcery:air_flash_' .. tostring(math.random(10))}) 340 + -- else minetest.remove_node(n.pos) end 341 + -- end 342 + -- end 343 + local timeline, sounds = { 344 + [0] = function(s) 345 + -- sorcery.vfx.imbue(jmpcolor,s.caster,1) 346 + end; 347 + }, {}; 348 + local time = 0; 349 + for i=0,#tmap do 350 + local cols = tmap[i] 351 + if cols ~= nil then 352 + time = time + math.random()*0.2 + 0.1 353 + local wh = {whence=0,secs=2+time} 354 + timeline[wh] = function(sp) 355 + for _,col in pairs(cols) do 356 + for _,n in pairs(col) do lift(n) end 357 + end 358 + end 359 + sounds[wh] = { 360 + sound = 'sorcery_zap'; 361 + gain = math.random() + 0.1; 362 + where = cols[1][1].pos; 363 + } 364 + end 365 + end 366 + sorcery.spell.cast { 367 + name = 'sorcery:elevate'; 368 + caster = ctx.caster; 369 + anchor = center, radius = r; 370 + duration = 2 + time; 371 + timeline = timeline, sounds = sounds; 372 + } 373 + end; 374 + frame = { 375 + iridium = { 376 + name = 'Ascension'; 377 + desc = 'Transport yourself and your surroundings high into the heavens, even if you are deep in the bowels of the earth'; 378 + }; 379 + }; 202 380 }; 203 381 }; 204 382 }; 205 383 disjoin = { 206 384 name = 'Disjoin'; 207 385 tone = {159,235,0}; 208 386 minpower = 4; 209 - rarity = 40; 387 + rarity = 34; 210 388 amulets = { 211 389 sapphire = { 212 390 name = 'Unsealing'; 213 391 desc = 'Wielding this amulet, a touch of your hand will unravel even the mightiest protective magics, leaving doors unsealed and walls free to tear down'; 214 392 }; 215 393 amethyst = { 216 394 name = 'Purging'; ................................................................................ 251 429 }; 252 430 luxite = { 253 431 name = 'Disjunctive Aura'; 254 432 desc = 'For a time, all magic undertaken in your vicinity will fail totally — including your own'; 255 433 cast = function(ctx) 256 434 local h = ctx.heading.eyeheight*1.1 257 435 sorcery.spell.cast { 436 + name = 'sorcery:disjunctive-aura'; 258 437 caster = ctx.caster, attach = 'caster'; 259 438 subjects = {{player=ctx.caster}}; 260 439 disjunction = true, range = 4 + ctx.stats.power; 261 440 duration = 10 + ctx.stats.power * 3; 262 441 timeline = { 263 442 [0] = function(s,_,tl) 264 443 local ttns = 0.8 ................................................................................ 348 527 }; 349 528 ruby = { 350 529 name = 'Liftoff'; 351 530 desc = 'Lift yourself high into the air with a blast of violent repulsive force against the ground, and drift down safely to a position of your choice'; 352 531 cast = function(ctx) 353 532 local power = 14 * (1+(ctx.stats.power * 0.2)) 354 533 minetest.sound_play('sorcery_hurl',{object=ctx.caster},true) 534 + 535 + local oldsp = sorcery.spell.ensorcelled(ctx.caster, 'sorcery:liftoff') 536 + if oldsp then oldsp:cancel() end 537 + 355 538 sorcery.spell.cast { 539 + name = 'sorcery:liftoff'; 356 540 caster = ctx.caster; 357 541 subjects = {{player=ctx.caster}}; 358 542 duration = power * 0.30; 359 543 timeline = { 360 544 [0] = function(s,_,tl) 361 545 sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93)) 362 - ctx.caster:add_velocity{y=power;x=0,z=0} 546 + ctx.caster:add_velocity{y=power*1.2;x=0,z=0} 547 + end; 548 + [{whence=0, secs=1}] = function(s) 363 549 s.affect { 364 550 duration = power * 0.50; 365 - raise = 2; 551 + raise = 0.5; 366 552 -- fall = (power * 0.25) * 0.3; 367 553 impacts = { 368 554 gravity = 0.1; 369 555 }; 370 556 } 371 557 end; 372 558 }; ................................................................................ 396 582 desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you'; 397 583 cast = function(ctx) 398 584 if not (ctx.target and ctx.target.type == 'object') then return false end 399 585 local tgt = ctx.target.ref 400 586 local power = 16 * (1+(ctx.stats.power * 0.2)) 401 587 minetest.sound_play('sorcery_hurl',{object=ctx.caster},true) 402 588 sorcery.spell.cast { 589 + name = 'sorcery:flinging'; 403 590 caster = ctx.caster; 404 591 subjects = {{player=tgt}}; 405 592 duration = 4; 406 593 timeline = { 407 594 [0] = function(s,_,tl) 408 595 for _,sub in pairs(s.subjects) do 409 - local height = (sub.player:get_properties().eye_height or 1)*1.3 410 - local scenter = vector.add(sub.player:get_pos(), {x=0,y=height/2,z=0}) 411 - for i=1,math.random(64,128) do 412 - local high = (height+0.8)*math.random() - 0.8 413 - local far = (high >= -0.5 and high <= height) and 414 - (math.random() * 0.3 + 0.4) or 415 - (math.random() * 0.5) 416 - local yaw = {x=0, y = math.random()*100, z=0} 417 - local po = vector.rotate({x=far,y=high,z=0}, yaw) 418 - local ppos = vector.add(po,sub.player:get_pos()) 419 - local dir = vector.direction(ppos,scenter) 420 - local vel = math.random() * 0.8 + 0.4 421 - minetest.add_particle { 422 - pos = ppos; 423 - velocity = vector.multiply(dir,vel); 424 - expirationtime = far / vel; 425 - size = math.random()*2.4 + 0.6; 426 - texture = sorcery.lib.image('sorcery_sputter.png'):glow(sorcery.lib.color{ 427 - hue = math.random(41,63); 428 - saturation = 100; 429 - luminosity = 0.5 + math.random()*0.3; 430 - }):render(); 431 - glow = 14; 432 - animation = { 433 - type = 'vertical_frames', length = far/vel; 434 - aspect_w = 16, aspect_h = 16; 435 - }; 596 + sorcery.vfx.imbue(function() return 597 + sorcery.lib.color { 598 + hue = math.random(41,63); 599 + saturation = 100; 600 + luminosity = 0.5 + math.random()*0.3; 436 601 } 437 - end 602 + end, sub.player) 438 603 end 439 604 end; 440 - [0.3] = function(s,te,tl) 605 + [{whence=0, secs=1}] = function(s,te,tl) 441 606 sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93)) 442 607 for _,sub in pairs(s.subjects) do 443 608 sub.player:add_velocity{y=power;x=0,z=0} 444 609 end 445 610 end; 446 611 [1] = (ctx.amulet.frame == 'cobalt') and function(s,te,tl) 447 612 -- TODO add visuals ................................................................................ 489 654 }; 490 655 }; 491 656 }; 492 657 obliterate = { 493 658 name = 'Obliterate'; 494 659 tone = {255,0,10}; 495 660 minpower = 5; 496 - rarity = 35; 661 + rarity = 30; 497 662 amulets = { 498 663 amethyst = { 499 664 name = 'Sapping'; 500 665 desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention'; 501 666 }; 502 667 ruby = { 503 668 name = 'Shattering'; ................................................................................ 548 713 }; 549 714 }; 550 715 }; 551 716 excavate = { 552 717 name = 'Excavate'; 553 718 tone = {0,68,235}; 554 719 minpower = 3; 555 - rarity = 30; 720 + rarity = 17; 556 721 amulets = { 557 722 luxite = { 558 723 name = 'Stonestride'; 559 724 desc = 'Rock walls will open up before you when you brandish this amulet before them, closing up again behind you without leaving a trace of your passage'; 560 725 }; 561 726 sapphire = { 562 727 name = 'Tunnelling'; ................................................................................ 651 816 sound='sorcery_crunch', where='pos'; 652 817 ephemeral=true, gain = math.random(3,10) * 0.1; 653 818 } 654 819 tp = tp + (math.random(2,5) * 0.1) 655 820 end 656 821 sounds[1] = {sound='sorcery_powerdown', where='pos'} 657 822 sorcery.spell.cast { 823 + name = 'sorcery:excavate'; 658 824 caster = ctx.caster; 659 825 duration = tp; 660 826 timeline = timeline, sounds = sounds; 661 827 -- spell state 662 828 anchor = ctx.target.under; 663 829 tunnel_angle = ctx.caster:get_look_horizontal(); 664 830 tunnel_radius = math.floor(math.random(3,5) * (ctx.stats.power * 0.1)); ................................................................................ 675 841 }; 676 842 }; 677 843 }; 678 844 genesis = { 679 845 name = 'Genesis'; 680 846 tone = {235,0,175}; 681 847 minpower = 5; 682 - rarity = 25; 848 + rarity = 23; 683 849 amulets = { 684 850 mese = { 685 851 mingrade = 4; 686 852 name = 'Duplication'; 687 - desc = 'Generate a copy of any object or item, no matter how common or rare'; 853 + desc = 'Bring an exact twin of any object or item into existence, no matter how common or rare it might be'; 854 + cast = function(ctx) 855 + local color = sorcery.lib.color(255,61,205) 856 + local dup, sndpos, anchor, sbj, ty 857 + if ctx.target.type == 'object' and ctx.target.ref:get_luaentity().name == '__builtin:item' then 858 + sorcery.vfx.imbue(color, ctx.target.ref) 859 + sndpos = 'subjects' 860 + sbj = {{player = ctx.target.ref}} 861 + local item = ItemStack(ctx.target.ref:get_luaentity().itemstring) 862 + local r = function() return math.random() * 2 - 1 end 863 + local putpos = vector.offset(ctx.target.ref:get_pos(), r(), 1, r()) 864 + dup = function() 865 + item:set_count(1) -- nice try bouge-san 866 + return minetest.add_item(putpos, item), false 867 + end 868 + elseif ctx.target.type == 'node' then 869 + ty = minetest.get_node(ctx.target.under).name 870 + sorcery.vfx.imbue(color, ctx.target.under) 871 + sndpos = 'pos'; 872 + anchor = ctx.target.under; 873 + dup = function() 874 + local origmeta = minetest.get_meta(ctx.target.under):to_table() 875 + origmeta.inventory = nil 876 + local npos 877 + do local vp = {} 878 + for _, of in pairs(sorcery.lib.node.offsets.neighbors) do 879 + local sum = vector.add(ctx.target.under, of) 880 + if sorcery.lib.node.is_clear(sum) then 881 + vp[#vp+1] = sum 882 + end 883 + end 884 + if #vp > 0 then npos=vp[math.random(#vp)] end 885 + end 886 + if npos then 887 + minetest.set_node(npos, minetest.get_node(ctx.target.under)) 888 + minetest.get_meta(npos):from_table(origmeta) 889 + return npos, true 890 + else 891 + local nstack = ItemStack(ty) 892 + nstack:get_meta():from_table(origmeta) 893 + local leftover = ctx.caster:get_inventory():add_item('main',nstack) 894 + if leftover and not leftover.is_empty() then 895 + minetest.add_item(ctx.caster:get_pos(), leftover) 896 + end 897 + end 898 + end 899 + else 900 + return false 901 + end 902 + if minetest.get_item_group(ty,'do_not_duplicate') ~= 0 then 903 + return true 904 + end 905 + 906 + sorcery.spell.cast { 907 + name = 'sorcery:duplicate'; 908 + caster = ctx.caster; 909 + duration = math.random(10,20) * ((10 - ctx.stats.power)*0.1); 910 + anchor = anchor; 911 + timeline = { 912 + [{whence=0, secs=1}] = function(s,te,tl) 913 + local mag = sbj and 0.5 or 0.7 914 + local pv = sbj and vector.new(0,0,0) or ctx.target.under 915 + local vfn = (sbj and s.visual_subjects or s.visual) 916 + vfn { 917 + amount = tl * 30, time = tl; 918 + minpos = vector.offset(pv,-mag,-mag,-mag); 919 + maxpos = vector.offset(pv, mag, mag, mag); 920 + minsize = 0.5, maxsize = 2.3; 921 + minexptime = 1.0, maxexptime = 1.5; 922 + texture = sorcery.lib.image('sorcery_sputter.png'):glow(color):render(); 923 + animation = { 924 + type = 'vertical_frames', length = 1.6; 925 + aspect_w = 16, aspect_h = 16; 926 + }; 927 + } 928 + end; 929 + [1] = function(s,te) 930 + local where, node = dup() 931 + if where == nil then return end 932 + local pv = node and where or vector.new(0,0,0) 933 + local mp = (not node) and vector.new(0,0,0) or { 934 + x = 0.5, y = 0.5, z = 0.5 935 + } 936 + minetest.add_particlespawner { 937 + amount = 170, time = 0.2; 938 + minpos = vector.subtract(pv,mp); 939 + maxpos = vector.add(pv,mp); 940 + attached = (not node) and where or nil; 941 + minvel = {x = -2.0, y = -1.8, z = -2.0}; 942 + maxvel = {x = 2.0, y = 0.2, z = 2.0}; 943 + minacc = {x = -0.0, y = -0.1, z = -0.0}; 944 + maxacc = {x = 0.0, y = -0.3, z = 0.0}; 945 + minsize = 0.3, maxsize = 2; 946 + minexptime = 1, maxexptime = 3.0; 947 + texture = sorcery.lib.image('sorcery_spark.png'):glow(color):render(); 948 + animation = { 949 + type = 'vertical_frames', length = 3.1; 950 + aspect_w = 16, aspect_h = 16; 951 + }; 952 + } 953 + end; 954 + }; 955 + sounds = { 956 + [0] = { 957 + sound = 'sorcery_duplicate_bg'; 958 + where = sndpos, stop = 1, fade = 2; 959 + }; 960 + [1] = { 961 + sound = 'sorcery_genesis'; 962 + where = sndpos, ephemeral = true; 963 + }; 964 + }; 965 + } 966 + end; 688 967 }; 689 968 }; 690 969 }; 691 970 luminate = { 692 971 name = 'Luminate'; 693 972 tone = {255,194,0}; 694 973 minpower = 1; 695 974 rarity = 5; 696 975 amulets = { 697 976 luxite = { 698 977 name = 'Glow'; 699 978 desc = 'Swathe yourself in an aura of sparkling radiance, casting light upon all the dark places where you voyage'; 979 + iridium = { 980 + name = 'Aura'; 981 + desc = 'Dazzling golden luminance emanates from the bodies of all those around you, and you walk in light even amid the darkest depths of the earth'; 982 + }; 700 983 }; 701 984 diamond = { 702 985 name = 'Radiance'; 703 986 desc = 'Set the air around you alight with a mystic luminance, letting you see clearly a great distance in every direction for several minutes'; 704 987 frame = { 705 988 iridium = { 706 989 name = 'Sunshine'; ................................................................................ 711 994 }; 712 995 }; 713 996 }; 714 997 dominate = { 715 998 name = 'Dominate'; 716 999 tone = {235,0,228}; 717 1000 minpower = 4; 718 - rarity = 20; 1001 + rarity = 13; 719 1002 amulets = { 720 1003 amethyst = { 721 1004 name = 'Suffocation'; 722 1005 desc = 'Wrap this spell tightly around your victim\'s throat, cutting off their oxygen until you release them.'; 723 1006 }; 724 1007 emerald = { 725 1008 name = 'Caging';
Modified gems.lua from [42c4d86138] to [68440cd05e].
55 55 if not gem.foreign_amulet then 56 56 local img = sorcery.lib.image 57 57 local img_stone = img('sorcery_amulet.png'):multiply(sorcery.lib.color(gem.tone)) 58 58 local img_sparkle = img('sorcery_amulet_sparkle.png') 59 59 local useamulet = function(stack,user,target) 60 60 local sp = sorcery.amulet.getspell(stack) 61 61 if not sp or not sp.cast then return nil end 62 - local stats = sorcery.amulet.stats(stack) 62 + 63 + local usedamulet if stack:get_count() == 1 then 64 + usedamulet = stack 65 + else 66 + usedamulet = ItemStack(stack) 67 + usedamulet:set_count(1) 68 + end 63 69 local probe = sorcery.spell.probe(user:get_pos()) 64 70 -- amulets don't work in antimagic fields, though some may want to 65 71 -- implement this logic themselves (for instance to check a range) 66 72 if (probe.disjunction and not sp.ignore_disjunction) then return nil end 73 + local stats = sorcery.amulet.stats(usedamulet) 67 74 68 75 local ctx = { 69 76 caster = user; 70 77 target = target; 71 78 stats = stats; 72 - wield = stack; 79 + wield = usedamulet; 73 80 amulet = stack:get_definition()._sorcery.amulet; 74 81 meta = stack:get_meta(); -- avoid spell boilerplate 75 82 color = sorcery.lib.color(sp.tone); 76 83 today = minetest.get_day_count(); 77 84 probe = probe; 78 85 heading = { 79 86 pos = user:get_pos(); ................................................................................ 93 100 pos = user:get_pos(); 94 101 gain = 1; 95 102 }) 96 103 end 97 104 if ctx.sparkle then 98 105 sorcery.vfx.cast_sparkle(user, ctx.color, stats.power,0.5) 99 106 end 107 + local infinirune = minetest.check_player_privs(user, 'sorcery:infinirune') 100 108 if res == nil then 101 - if not minetest.check_player_privs(user, 'sorcery:infinirune') then 102 - sorcery.amulet.setrune(stack) 103 - end 109 + if not infinirune then sorcery.amulet.setrune(usedamulet) end 104 110 end 105 111 106 - return ctx.wield 112 + if stack:get_count() == 1 then 113 + return ctx.wield 114 + else 115 + if not infinirune then 116 + stack:take_item(1) 117 + local leftover = user:get_inventory():add_item('main',usedamulet) 118 + if leftover and leftover:get_count() > 0 then 119 + minetest.add_item(user:get_pos(), leftover) 120 + end 121 + end 122 + return stack 123 + end 107 124 end; 108 125 minetest.register_craftitem(amuletname, { 109 126 description = sorcery.lib.str.capitalize(name) .. ' amulet'; 110 127 inventory_image = img_sparkle:blit(img_stone):render(); 111 128 wield_scale = { x = 0.6, y = 0.6, z = 0.6 }; 112 129 groups = { sorcery_amulet = 1 }; 113 130 on_use = useamulet;
Modified init.lua from [c1be2dc670] to [98e23bb4d1].
86 86 -- convenience 87 87 'str', 'math'; 88 88 -- serialization 89 89 'marshal', 'json'; 90 90 -- data structures 91 91 'tbl', 'class'; 92 92 -- wrappers 93 - 'color', 'image', 'ui'; 93 + 'color', 'image', 'ui', 'obj'; 94 94 -- game 95 95 'node', 'item'; 96 96 } 97 97 98 98 sorcery.stage('worldbuilding',data,root) 99 99 root {'compat','matreg'} 100 100 if not sorcery.stage('loadlore', data, root) then
Modified interop.lua from [913cc0d6da] to [66e6db7c2d].
41 41 42 42 {'top', 'sorcery:mill', 'output'}; 43 43 {'side', 'sorcery:mill', 'grinder'}; 44 44 {'bottom', 'sorcery:mill', 'input'}; 45 45 46 46 {'bottom', 'sorcery:harvester', 'charge'}; 47 47 -- output handled on our side 48 + 49 + {'bottom', 'sorcery:runeforge', 'amulet'}; 50 + -- output handled on our side 48 51 } 49 52 end 53 + 54 +if minetest.get_modpath('mtg_craftguide') and minetest.get_modpath('sfinv') then 55 +-- the craft guide is handy, but not only is it glitched to the point of enabling 56 +-- trivial denial of service attacks against a server, it breaks some of the most 57 +-- basic mechanics of the sorcery mod. we disable it except for players with a 58 +-- specific debugging privilege. i suppose we could also add a 'potion of 59 +-- omniscience' that allows brief access, but i'm disinclined to; it feels gross. 60 + local pg = sfinv.pages['mtg_craftguide:craftguide'] 61 + local cb = pg.is_in_nav 62 + -- currently this isn't used by mtgcg, but doing this gives us some future- 63 + -- proofing, and keeps us from fucking up any competing access control that 64 + -- might be in use. 65 + pg.is_in_nav = function(self,player, ...) 66 + -- unfortunately, this is a purely cosmetic "access control" mechanism; 67 + -- sfinv doesn't actually check if a page is available to a player before 68 + -- showing it to them. ironic, given how the author specifically warns 69 + -- people in his modding tutorial that the client can submit any form it 70 + -- wants at any time… 🙄 71 + if not minetest.check_player_privs(player, 'sorcery:omniscience') then 72 + return false 73 + end 74 + if cb 75 + then return cb(self,player,...) 76 + else return true 77 + end 78 + end 79 +end
Modified lib/node.lua from [5b83bf6142] to [4b89fedafd].
71 71 is_air = function(pos) 72 72 local n = sorcery.lib.node.force(pos) 73 73 if n.name == 'air' then return true end 74 74 local d = minetest.registered_nodes[n.name] 75 75 if not d then return false end 76 76 return not d.walkable 77 77 end; 78 + 79 + is_clear = function(pos) 80 + if not sorcery.lib.node.is_air(pos) then return false end 81 + local ents = minetest.get_objects_inside_radius(pos,0.5) 82 + if #ents > 0 then return false end 83 + return true 84 + end; 78 85 79 86 get_arrival_point = function(pos) 80 - local air = sorcery.lib.node.is_air 81 - if air(pos) then 82 - local n = {x=0,y=1,z=0} 83 - if air(vector.add(pos,n)) then return pos end 84 - local down = vector.subtract(pos,n) 85 - if air(down) then return down end 86 - else return nil end 87 + local try = function(p) 88 + local air = sorcery.lib.node.is_clear 89 + if air(p) then 90 + if air(vector.offset(p,0,1,0)) then return p end 91 + if air(vector.offset(p,0,-1,0)) then return vector.offset(p,0,-1,0) end 92 + end 93 + return false 94 + end 95 + 96 + do local t = try(pos) if t then return t end end 97 + for _,o in pairs(ofs.neighbors) do 98 + local p = vector.add(pos, o) 99 + do local t = try(p) if t then return t end end 100 + end 87 101 end; 88 102 89 103 amass = function(startpoint,names,directions) 90 104 if not directions then directions = ofs.neighbors end 91 105 local nodes, positions, checked = {},{},{} 92 106 local checkedp = function(pos) 93 107 for _,v in pairs(checked) do
Added lib/obj.lua version [42345da92a].
1 +-- functions for working with entities inexplicably missing 2 +-- from the game API 3 + 4 +local fn = {} 5 + 6 +-- WARNING: INEFFICIENT AS FUCK 7 +fn.identify = function(objref) --> objectid 8 + for _, o in pairs(minetest.get_connected_players()) do 9 + if objref == o then return o:get_player_name(), 'player' end 10 + end 11 + for id, le in pairs(minetest.luaentities) do 12 + if le.object == objref then return id, 'entity' end 13 + end 14 +end 15 + 16 +fn.handle = sorcery.lib.class { 17 + __newindex = function(self,key,newval) 18 + local hnd if self.player 19 + then hnd = minetest.get_player_by_name(self._id) 20 + else hnd = minetest.luaentities[self._id] 21 + end 22 + if key == 'id' then 23 + if type(newval) == 'string' then 24 + local p = minetest.get_player_by_name(newval) 25 + if p then 26 + self._id = newval 27 + self.player = true 28 + return 29 + end 30 + end 31 + if minetest.luaentities[newval] then 32 + self._id = newval 33 + self.player = false 34 + else error('attempted to assign invalid ID to entity handle') end 35 + elseif key == 'obj' then 36 + local no, kind = fn.identify(newval) 37 + if no then 38 + self._id = no 39 + if kind == 'player' 40 + then self.player = true 41 + else self.player = false 42 + end 43 + else error('attempted to assign invalid ObjectRef to entity handle') end 44 + elseif key == 'stack' and self.kind == 'item' then 45 + hnd:set_item(newval) 46 + end 47 + end; 48 + __index = function(self,key) 49 + local hnd if self.player then 50 + hnd = minetest.get_player_by_name(self._id) 51 + else 52 + hnd = minetest.luaentities[self._id] 53 + end 54 + if key == 'online' then 55 + return hnd ~= nil 56 + elseif key == 'id' then 57 + if self.player then return nil 58 + else return self._id end 59 + elseif key == 'obj' then 60 + if self.player 61 + then return hnd 62 + else return hnd.object 63 + end 64 + elseif key == 'kind' then 65 + if self.player then return 'player' 66 + elseif hnd.name == '__builtin:item' then return 'item' 67 + else return 'object' end 68 + elseif key == 'name' then 69 + if self.player then return self._id 70 + elseif self.kind == 'item' then 71 + return ItemStack(hnd.itemstring):get_name() 72 + else return hnd.name end 73 + elseif key == 'stack' and self.kind == 'item' then 74 + return ItemStack(hnd.itemstring) 75 + elseif key == 'height' then 76 + if kind == 'item' then return 0.5 77 + elseif kind == 'player' then 78 + local eh = hnd.object:get_properties().eye_height 79 + return eh and (eh*1.2) or 1 80 + else 81 + local box = hnd.object:get_properties().collisionbox 82 + if box then 83 + local miny,maxy = box[2], box[5] 84 + return maxy-miny, miny 85 + else return 0 end 86 + end 87 + end 88 + end; 89 + construct = function(h) 90 + local kind, id 91 + if type(h) == 'string' and minetest.get_player_by_name(h) ~= nil then 92 + kind = 'player'; 93 + id = h 94 + elseif minetest.luaentities[h] then 95 + kind = 'entity'; 96 + id = h 97 + else id, kind = fn.identify(h) end 98 + 99 + if not id then 100 + error('attempted to construct object handle from invalid value') 101 + end 102 + 103 + return { 104 + player = kind == 'player'; 105 + _id = id; 106 + } 107 + end; 108 +} 109 + 110 +return fn
Modified privs.lua from [62e9e10513] to [7ff60372f5].
1 1 minetest.register_privilege('sorcery:infinirune', { 2 2 description = "runes don't discharge upon use, for debugging use only"; 3 3 give_to_singleplayer = false; 4 4 give_to_admin = false; 5 5 }) 6 + 7 +if minetest.get_modpath('mtg_craftguide') then 8 + minetest.register_privilege('sorcery:omniscience', { 9 + description = "access the all-knowing crafting guide"; 10 + give_to_singleplayer = false; 11 + give_to_admin = false; 12 + }) 13 +end
Added sounds/sorcery_duplicate_bg.ogg version [].
Added sounds/sorcery_genesis.ogg version [8332db2096].
cannot compute difference between binary files
Modified spell.lua from [cfeba0d2de] to [92125c4f2c].
133 133 if s.i then sorcery.spell.active[s.i] = nil else 134 134 for k,v in pairs(sorcery.spell.active) do 135 135 if v == spell then sorcery.spell.active[k] = nil break end 136 136 end 137 137 end 138 138 end 139 139 end 140 + 141 +sorcery.spell.ensorcelled = function(player,spell) 142 + if type(player) == 'string' then player = minetest.get_player_by_name(player) end 143 + for _,s in pairs(sorcery.spell.active) do 144 + if spell and (s.name ~= spell) then goto skip end 145 + for _,sub in pairs(s.subjects) do 146 + if sub.player == player then return s end 147 + end 148 + ::skip::end 149 + return false 150 +end 151 + 152 +sorcery.spell.each = function(player,spell) 153 + local idx = 0 154 + return function() 155 + repeat idx = idx + 1 156 + local sp = sorcery.spell.active[idx] 157 + if sp == nil then return nil end 158 + if spell == nil or sp.name == spell then 159 + for _,sub in pairs(sp.subjects) do 160 + if sub.player == player then return sp end 161 + end 162 + end 163 + until idx >= #sorcery.spell.active 164 + end 165 +end 140 166 141 167 -- when a new spell is created, we analyze it and make the appropriate calls 142 168 -- to minetest.after to queue up the events. each job returned needs to be 143 169 -- saved in 'jobs' so they can be canceled if the spell is disjoined. no polling 144 170 -- necessary :D 145 171 146 172 sorcery.spell.cast = function(proto) ................................................................................ 183 209 for _,j in ipairs(s.jobs) do j:cancel() end 184 210 for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end 185 211 for _,i in ipairs(s.sfx) do s.silence(i) end 186 212 for _,i in ipairs(s.impacts) do i.effect:stop() end 187 213 end 188 214 s.release_subject = function(si) 189 215 local t = s.subjects[si] 190 - print('releasing against',si,t) 191 216 for _,f in pairs(s.sfx) do if f.subject == t then s.silence(f) end end 192 217 for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end 193 218 for _,f in pairs(s.vfx) do 194 219 if f.subject == t then minetest.delete_particlespawner(f.handle) end 195 220 end 196 221 s.subjects[si] = nil 197 222 end ................................................................................ 363 388 if s.terminate then s:terminate() end 364 389 sorcery.spell.active[myid] = nil 365 390 end) 366 391 end 367 392 s.starttime = minetest.get_server_uptime() 368 393 return s 369 394 end 395 + 396 +minetest.register_on_dieplayer(function(player) 397 + sorcery.spell.disjoin{target=player} 398 +end)
Modified vfx.lua from [aaa85eb70f] to [423ee7c3cf].
104 104 }, 105 105 acceleration = { 106 106 x = 0, 107 107 y = -1, 108 108 z = 0 109 109 } 110 110 } 111 + end 112 +end 113 + 114 +-- target can be an entity or a pos vector 115 +sorcery.vfx.imbue = function(color, target, strength, height) 116 + local tpos if target.get_pos then 117 + tpos = target:get_pos() 118 + if target.get_properties then 119 + height = height or ((target:get_properties().eye_height or 1)*1.3) 120 + end 121 + else 122 + tpos = target 123 + end 124 + height = height or 1 125 + local scenter = vector.add(tpos, {x=0,y=height/2,z=0}) 126 + for i=1,math.random(64*(strength or 1),128*(strength or 1)) do 127 + local high = (height+0.8)*math.random() - 0.8 128 + local far = (high >= -0.5 and high <= height) and 129 + (math.random() * 0.3 + 0.4) or 130 + (math.random() * 0.5) 131 + local yaw = {x=0, y = math.random()*(2*math.pi), z=0} 132 + local po = vector.rotate({x=far,y=high,z=0}, yaw) 133 + local ppos = vector.add(po,tpos) 134 + local dir = vector.direction(ppos,scenter) 135 + local vel = math.random() * 0.8 + 0.4 136 + local col if type(color) == 'function' 137 + then col = color(i, {high = high, far = far, dir = dir, vel = vel, pos = po}) 138 + else col = color 139 + end 140 + minetest.add_particle { 141 + pos = ppos; 142 + velocity = vector.multiply(dir,vel); 143 + expirationtime = far / vel; 144 + size = math.random()*2.4 + 0.6; 145 + texture = sorcery.lib.image('sorcery_sputter.png'):glow(col):render(); 146 + glow = 14; 147 + animation = { 148 + type = 'vertical_frames', length = far/vel; 149 + aspect_w = 16, aspect_h = 16; 150 + }; 151 + } 111 152 end 112 153 end