sorcery  Diff

Differences From Artifact [24333d0a0e]:

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

To Artifact [807df7a655]:

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

            1  +local target_node = function(ctx,tgt)
            2  +	if not ctx.target or ctx.target.type ~= 'node' then return false end
            3  +	local node = minetest.get_node(ctx.target.under)
            4  +	if node.name ~= tgt then return false end
            5  +	return node
            6  +end;
            7  +
            8  +local get_enchanter = function(ctx)
            9  +	local ench = target_node(ctx, 'sorcery:enchanter')
           10  +	if not ench then return false end
           11  +	return minetest.get_meta(ctx.target.under):get_inventory()
           12  +end
           13  +
     1     14   local cast_sparkle = function(ctx,color,strength,duration)
     2         -	minetest.add_particlespawner {
     3         -		amount = 70 * strength;
     4         -		time = duration or 1.5;
     5         -		attached = ctx.caster;
     6         -		texture = sorcery.lib.image('sorcery_spark.png'):multiply(color):render();
     7         -		minpos = { x = -0.1, z =  0.5, y =  1.2}; 
     8         -		maxpos = { x =  0.1, z =  0.3, y =  1.6}; 
     9         -		minvel = { x = -0.5, z = -0.5, y = -0.5};
    10         -		maxvel = { x =  0.5, z =  0.5, y =  0.5};
    11         -		minacc = { x =  0.0, z =  0.0, y =  0.5};
    12         -		maxacc = { x =  0.0, z =  0.0, y =  0.5};
    13         -		minsize = 0.4, maxsize = 0.8;
    14         -		minexptime = 1, maxexptime = 1;
    15         -		glow = 14;
    16         -		animation = {
    17         -			type = 'vertical_frames';
    18         -			aspect_w = 16;
    19         -			aspect_h = 16;
    20         -			length = 1.1;
    21         -		};
    22         -	}
           15  +	sorcery.vfx.cast_sparkle(ctx.caster,color,strength,duration)
           16  +end
           17  +
           18  +local enchantment_sparkle = function(ctx,color)
           19  +	sorcery.vfx.enchantment_sparkle(ctx.target,color)
    23     20   end
    24         -local enchantment_sparkle = function(ctx,color)
    25         -	local minvel, maxvel
    26         -	if minetest.get_node(vector.add(ctx.target.under,{y=1,z=0,x=0})).name == 'air' then
    27         -		minvel = {x=0,z=0,y= 0.3}  maxvel = {x=0,z=0,y= 1.5};
    28         -	else
    29         -		local dir = vector.subtract(ctx.target.under,ctx.target.above)
    30         -		minvel = vector.multiply(dir, 0.3)
    31         -		maxvel = vector.multiply(dir, 1.2)
    32         -	end
    33         -	return minetest.add_particlespawner {
    34         -		amount = 50;
    35         -		time = 0.5;
    36         -		minpos = vector.subtract(ctx.target.under, 0.5);
    37         -		maxpos = vector.add(ctx.target.under, 0.5);
    38         -		minvel = minvel, maxvel = maxvel;
    39         -		minexptime = 1, maxexptime = 2;
    40         -		minsize = 0.5, maxsize = 2;
    41         -		texture = sorcery.lib.image('sorcery_spark.png'):multiply(color):render();
    42         -		animation = {
    43         -			type = 'vertical_frames';
    44         -			aspect_w = 16, aspect_h = 16;
    45         -			length = 2;
    46         -		};
    47         -		glow = 14;
    48         -	}
    49         -end
           21  +
    50     22   local anchorwand = function(aff,uses,recipe)
    51     23   	local affcolor = sorcery.lib.color(sorcery.data.affinities[aff].color)
    52     24   	return {
    53     25   		name = aff .. ' anchor';
    54     26   		desc = 'With an enchanter, anchor ' .. aff .. ' spells into an object to enable it to produce preternatural effects';
    55     27   		uses = uses;
    56     28   		affinity = recipe;
    57     29   		color = affcolor;
    58     30   		sound = 'xdecor_enchanting'; -- FIXME make own
    59     31   		cast = function(ctx)
    60         -			if (not ctx.target) or ctx.target.type ~= 'node' then return false end
    61         -			local node = minetest.get_node(ctx.target.under)
    62         -			if node.name ~= 'sorcery:enchanter' then return false end
           32  +			local node = target_node(ctx, 'sorcery:enchanter')
           33  +			if not node then return false end
    63     34   
    64     35   			local inv = minetest.get_meta(ctx.target.under):get_inventory()
    65     36   			if inv:is_empty('item') then return false end
    66     37   			local subj = inv:get_stack('item',1)
    67     38   
    68     39   			-- now we have everything we need. this part is complex.
    69     40   			-- first, we need to check the item to see if it is in a
................................................................................
   193    164   -- note: this was written before terminology was standardized,
   194    165   -- and "leytype" corresponds to what is otherwise known as an
   195    166   -- "affinity"; "affinity" after this comment is widely misused
   196    167   return {
   197    168   	flame = {
   198    169   		name = 'flamebolt';
   199    170   		color = {255,89,16};
   200         -		uses = 64;
          171  +		uses = 32;
   201    172   		affinity = {'acacia','blazing'};
   202    173   		leytype = 'praxic';
   203    174   		desc = 'Conjure a gout of fire to scorch your foes with a flick of this wand';
   204    175   		cast = function(ctx)
   205    176   			local speed = 30 -- TODO maybe amethyst tip increases speed?
          177  +			local radius 
          178  +			if ctx.base.gem == 'sapphire'
          179  +				then radius = math.random(2,3)
          180  +				else radius = math.random(1,2)
          181  +			end
   206    182   			local heading = ctx.heading
   207         -			heading.pos.y = heading.pos.y + 1.5 -- TODO maths
          183  +			heading.pos.y = heading.pos.y + heading.eyeheight*0.9
   208    184   			local bolt = minetest.add_entity(heading.pos,'sorcery:spell_projectile_flamebolt')
   209    185   			bolt:set_rotation(heading.yaw)
   210    186   			local vel = {
   211    187   				x = heading.yaw.x * speed;
   212    188   				y = heading.yaw.y * speed;
   213    189   				z = heading.yaw.z * speed;
   214    190   			};
          191  +			bolt:get_luaentity()._blastradius = radius
   215    192   			bolt:set_velocity(vel)
   216    193   		end;
   217    194   	};
   218    195   	seal = {
   219    196   		name = 'sealing';
   220    197   		color = {255,238,16};
   221    198   		uses = 32;
................................................................................
   223    200   		leytype = 'imperic';
   224    201   		affinity = {'pine','dark'};
   225    202   		cast = function(ctx)
   226    203   			if ctx.target == nil or ctx.target.type ~= 'node' then return false end
   227    204   			local meta = minetest.get_meta(ctx.target.under)
   228    205   			-- first we need to check if the wand has an identifying 'key' yet,
   229    206   			-- and set one if not.
   230         -			local wandmode = ctx.base.gem == 'sapphire'
          207  +			local modes = {
          208  +				sapphire = 'lockdown';
          209  +				diamond = 'steal';
          210  +			}
          211  +			local wandmode = modes[ctx.base.gem] or 'seal'
   231    212   			local keycode
   232    213   			if ctx.meta:contains('sorcery_wand_key') then
   233    214   				keycode = ctx.meta:get_string('sorcery_wand_key')
   234    215   			else
   235    216   				keycode = sorcery.lib.str.rand(32)
   236    217   				ctx.meta:set_string('sorcery_wand_key', keycode)
   237    218   				-- ctx.meta:mark_as_private('sorcery_wand_key')
   238    219   			end
   239    220   			if meta:contains('owner') then
   240    221   				-- owner is already set -- can we break the enchantment?
          222  +				if wandmode == 'steal' then
          223  +					if meta:get_string('owner') ~= ctx.caster:get_player_name() then
          224  +						meta:set_string('owner',ctx.caster:get_player_name())
          225  +						enchantment_sparkle(ctx,sorcery.lib.color(101,255,238))
          226  +					end
          227  +					return
          228  +				end
          229  +
   241    230   				if meta:get_string('sorcery_wand_key') == keycode then
   242    231   					meta:set_string('owner','')
   243    232   					meta:set_string('sorcery_wand_key','')
   244    233   					meta:set_string('sorcery_seal_mode','')
   245    234   					enchantment_sparkle(ctx,sorcery.lib.color(101,255,142))
   246    235   				else return false end
   247    236   			else
   248    237   				meta:set_string('sorcery_wand_key',keycode)
   249    238   				meta:mark_as_private('sorcery_wand_key')
   250    239   				meta:set_string('owner',ctx.caster:get_player_name())
   251         -				if wandmode then
          240  +				if wandmode == 'lockdown' then
   252    241   					meta:set_string('sorcery_seal_mode','wand')
   253    242   				end
   254    243   				enchantment_sparkle(ctx,sorcery.lib.color(255,201,27))
   255    244   			end
   256    245   		end;
   257    246   	};
   258    247   	leyspark = {
................................................................................
   374    363   	};
   375    364   	meld = {
   376    365   		name = 'melding';
   377    366   		uses = 48;
   378    367   		leytype = 'syncretic';
   379    368   		color = {172,65,255};
   380    369   		affinity = {'apple','verdant'};
   381         -		desc = 'Meld the properties of three balanced items on an enchanter to create a new one with special properties, but destroying the old ones and losing two thirds of the mass in the process. The precise outcome is not always predictable.';
          370  +		desc = 'Meld the properties of three balanced items on an enchanter to create a new one with special properties, but destroying the old ones and losing two thirds of the mass in the process. The precise outcome is not always predictable, and may vary with the moons and the stars.';
          371  +		cast = function(ctx)
          372  +			local e = get_enchanter(ctx)
          373  +			if not e then return false end
          374  +
          375  +			for _,m in pairs(sorcery.data.resonance.meld) do
          376  +				if m.restrict and not m.restrict(ctx) 
          377  +					then goto next_meld end
          378  +
          379  +				local g = {}
          380  +				for i,set in ipairs(m.set) do
          381  +					if type(set) == 'table' then
          382  +						g[i] = set
          383  +					else g[i] = { take = set; } end
          384  +
          385  +					local found = false
          386  +					for j=1,e:get_size('foci') do
          387  +						local match,res = sorcery.lib.item.groupmatch(g[i].take, e:get_stack('foci',j),false)
          388  +						if match then
          389  +							found = true
          390  +							g[i].slot = j
          391  +							g[i].leftover = res
          392  +							break
          393  +						end
          394  +					end
          395  +					if not found then goto next_meld end
          396  +				end
          397  +				-- we've made it past the tests; this meld
          398  +				-- matches the spec
          399  +
          400  +				for _,t in pairs(g) do
          401  +					if t.leftover and t.leftover:get_count() > 0 then
          402  +						e:set_stack('foci',t.slot,t.leftover)
          403  +						if t.replacement then
          404  +							minetest.add_item(ctx.target.above, ItemStack(t.replacement))
          405  +						end
          406  +					else
          407  +						e:set_stack('foci',t.slot,ItemStack(t.replacement))
          408  +					end
          409  +				end
          410  +				
          411  +				local res
          412  +				if type(m.results) == 'function' then
          413  +					res = m.results(ctx)
          414  +				elseif type(m.results) == 'table' and m.results[1] then -- haaaack
          415  +					res = select(2,sorcery.lib.tbl.pick(m.results))
          416  +				else
          417  +					res = m.results
          418  +				end
          419  +
          420  +				e:set_stack('item',1,ItemStack(res))
          421  +				enchantment_sparkle(ctx,sorcery.lib.color(228,4,201))
          422  +			::next_meld::end
          423  +		end;
   382    424   	};
   383    425   	divide = {
   384    426   		name = 'division';
   385    427   		uses = 19;
   386    428   		leytype = 'syncretic';
   387    429   		color = {255,65,121};
   388    430   		affinity = {'apple','shimmering'};
   389         -		desc = 'Shatter an item on an enchanter, dividing its essence equally into three parts and precipitating it into new items embodying various properties of the destroyed item. The outcome is not always predictable.';
          431  +		desc = 'Shatter an item on an enchanter, dividing its essence equally into three parts and precipitating it into new items embodying various properties of the destroyed item. The outcome is not always predictable, and may vary with the moons and the stars.';
          432  +		cast = function(ctx)
          433  +			local e = get_enchanter(ctx)
          434  +			if not e then return false end
          435  +
          436  +			local orig = e:get_stack('item',1)
          437  +			local div = sorcery.data.resonance.divide[orig:get_name()]
          438  +			if not div then return false end
          439  +
          440  +			local bitch = function(err)
          441  +				sorcery.log('data/spells(divide)', err .. ' for ' .. orig:get_name())
          442  +				return false
          443  +			end
          444  +
          445  +			if not (div.mode and div.give) then
          446  +				return bitch('improperly specified division')
          447  +			end
          448  +			
          449  +			if div.restrict and not div.restrict(ctx) then
          450  +				return false
          451  +			end
          452  +
          453  +			local dst
          454  +			if div.mode == 'any' then
          455  +				local lst = sorcery.lib.tbl.cshuf(div.give)
          456  +				dst = function(i) return lst[i] end
          457  +			elseif div.mode == 'random' then
          458  +				dst = function() return sorcery.lib.tbl.pick(div.give) end
          459  +			elseif div.mode == 'set' then
          460  +				dst = function(i) return div.give[i] end
          461  +			elseif div.mode == 'all' then
          462  +				dst = function() return div.give end
          463  +			elseif div.mode == 'fn' then
          464  +				dst = function(i) return div.give(i,ctx) end
          465  +			else return bitch('invalid division mode') end
          466  +			for i=1,e:get_size('foci') do
          467  +				e:set_stack('foci',i,ItemStack(dst(i)))
          468  +			end
          469  +			e:set_stack('item',1,ItemStack(div.replacement))
          470  +
          471  +			for _,color in pairs{{245,63,63},{63,245,178}} do
          472  +				enchantment_sparkle(ctx, sorcery.lib.color(color))
          473  +			end
          474  +		end;
   390    475   	};
   391    476   	obliterate = {
   392    477   		name = 'obliteration';
   393    478   		uses = 129;
   394    479   		color = {175,6,212};
   395    480   		affinity = {'aspen','dark'};
   396    481   		leytype = 'occlutic';
   397         -		desc = 'Totally and irreversibly obliterate all items on an enchanter.';
          482  +		desc = 'Incinerate all items on an enchanter, rendering them down to ash or obliterating them entirely.';
   398    483   		cast = function(ctx)
   399         -			if not ctx.target or ctx.target.type ~= 'node' then return false end
   400         -			local tgt = minetest.get_node(ctx.target.under)
   401         -			if tgt.name ~= 'sorcery:enchanter' then return false end
          484  +			local tgt = target_node(ctx, 'sorcery:enchanter')
          485  +			if not tgt then return false end
   402    486   
   403    487   			local inv = minetest.get_meta(ctx.target.under):get_inventory()
   404    488   			for _,name in pairs{'foci','item'} do
   405    489   				for i=1,inv:get_size(name) do
   406         -					inv:set_stack(name,i,ItemStack(nil))
          490  +					local stack = 'sorcery:ash'
          491  +					if ctx.base.gem == 'sapphire' then
          492  +						stack = nil
          493  +					end
          494  +					inv:set_stack(name,i,ItemStack(stack))
   407    495   				end
   408    496   			end
   409    497   
   410    498   			enchantment_sparkle(ctx,sorcery.lib.color(255,12,0))
   411    499   			enchantment_sparkle(ctx,sorcery.lib.color(85,18,35))
   412    500   			enchantment_sparkle(ctx,sorcery.lib.color(0,0,0))
   413    501   		end
................................................................................
   430    518   	};
   431    519   	transmute = {
   432    520   		name = 'transmutation';
   433    521   		uses = 7;
   434    522   		color = {255,90,18};
   435    523   		leytype = 'imperic';
   436    524   		affinity = {'aspen','shimmering','dark','blazing'};
   437         -		desc = 'Transmute three ingots into one of a different metal, determined by chance and influenced by configuration of the wand';
          525  +		desc = 'Transmute three ingots into one of a different metal, determined by chance, and influenced by configuration of the wand as well as the stars and the phase of the moon';
          526  +		-- diamond = quantity varies between 1-3
   438    527   	};
   439    528   	disjoin = {
   440    529   		name = 'disjunction';
   441    530   		uses = 32;
   442    531   		color = {17,6,212};
   443    532   		leytype = 'occlutic';
   444    533   		affinity = {'jungle','silent'};
   445    534   		desc = 'With an enchanter, disjoin the anchor holding a spell into an object so a new spell can instead be bound in';
          535  +		cast = function(ctx)
          536  +			local ench = target_node(ctx, 'sorcery:enchanter')
          537  +			if not ench then return false end
          538  +			local ei = minetest.get_meta(ctx.target.under):get_inventory()
          539  +			local item = ei:get_stack('item',1)
          540  +			local e = sorcery.enchant.get(item)
          541  +			if next(e.spells) == nil then return false end
          542  +			if #e.spells == 1 then e = nil else
          543  +				if ctx.base.gem == 'sapphire'
          544  +					then e.spells = {} e.energy = 0
          545  +					else table.remove(e.spells, math.random(#e.spells))
          546  +				end
          547  +			end
          548  +			sorcery.enchant.set(item,e)
          549  +			ei:set_stack('item',1,item)
          550  +			enchantment_sparkle(ctx,sorcery.lib.color(255,154,44))
          551  +			enchantment_sparkle(ctx,sorcery.lib.color(226,44,255))
          552  +		end;
   446    553   	};
   447    554   	divine = {
   448    555   		name = 'divining';
   449    556   		desc = 'Steal away the secrets of the cosmos';
   450    557   		uses = 16;
   451    558   		color= {97,97,255};
   452    559   		sound = 'xdecor:enchanting';
................................................................................
   455    562   		cast = function(ctx)
   456    563   			local inks = {'black','red','white','violet','blue','green'}
   457    564   			local getcolor = function(stack)
   458    565   				 if stack:is_empty() then return nil end
   459    566   				 if minetest.get_item_group(stack:get_name(), 'dye') == 0 then return nil end
   460    567   				 for _,ink in pairs(inks) do
   461    568   					 if minetest.get_item_group(stack:get_name(), 'color_' ..ink) ~= 0
   462         -						 then print('found',ink,'ink') return ink end
          569  +						 then return ink end
   463    570   				 end
   464    571   			end
   465    572   			if not ctx.target or ctx.target.type ~= 'node' then return false end
   466    573   			local tgt = minetest.get_node(ctx.target.under)
   467    574   			if tgt.name == 'sorcery:enchanter' then
   468    575   				local meta = minetest.get_meta(ctx.target.under)
   469    576   				local inv = meta:get_inventory()
................................................................................
   472    579   					and not inv:is_empty('foci') then
   473    580   				   local ink1 = getcolor(inv:get_stack('foci',2))
   474    581   				   local ink2 = getcolor(inv:get_stack('foci',3))
   475    582   				   local restrict, kind, mod = {} do
   476    583   					   local ms = inv:get_stack('foci',1)
   477    584   					   if not ms:is_empty() then mod = ms:get_name() end
   478    585   				   end
   479         -				   print(ink1,ink2,mod)
   480    586   				   if ink1 == 'black' and ink2 == 'black' then kind = 'craft'
   481    587   					   if mod then
   482    588   						   if mod == sorcery.data.metals.cobalt.parts.powder then
   483    589   							   restrict.group = 'sorcery_magitech'
   484    590   						   elseif mod == sorcery.data.metals.vidrium.parts.powder then
   485    591   							   restrict.group = 'sorcery_ley_device'
   486    592   						   elseif mod == sorcery.data.metals.aluminum.parts.powder then
................................................................................
   516    622   						   if mod == sorcery.data.metals.cobalt.parts.powder then
   517    623   							   restrict.aff = 'praxic'
   518    624   						   elseif mod == sorcery.data.metals.tungsten.parts.powder then
   519    625   							   restrict.aff = 'counterpraxic'
   520    626   						   elseif mod == sorcery.data.metals.aluminum.parts.powder then
   521    627   							   restrict.aff = 'syncretic'
   522    628   						   elseif mod == sorcery.data.metals.lithium.parts.powder then
   523         -							   -- restrict.aff = 'mandatic' -- no enchants yet, will cause infinite loop
          629  +							   -- restrict.aff = 'mandatic' -- no enchants yet, will cause infinite loop 🙃
   524    630   						   elseif mod == sorcery.data.metals.iridium.parts.powder then
   525    631   							   restrict.aff = 'entropic'
   526    632   						   elseif mod == sorcery.data.metals.gold.parts.powder then
   527    633   							   restrict.aff = 'cognic'
   528    634   						   elseif mod == sorcery.data.metals.silver.parts.powder then
   529    635   							   -- restrict.aff = 'occlutic'
   530    636   						   elseif mod == sorcery.data.metals.electrum.parts.powder then
   531    637   							   -- restrict.aff = 'imperic'
   532    638   						   else return false end
   533    639   					   end
   534    640   				   elseif ink1 == 'red' and ink2 == 'yellow' then kind = 'cook';
   535    641   				   -- elseif ink1 == 'red' and ink2 == 'orange' then kind = 'smelt';
   536    642   				   end
   537         -				   print('result',kind,dump(restrict))
   538    643   				   if kind then
   539         -					   print('found kind')
   540    644   					   local rec = ItemStack('sorcery:recipe')
   541    645   					   local m = rec:get_meta()
   542    646   					   if ctx.base.gem == 'diamond' then
   543    647   						   -- make recipe for thing in slot 1
   544    648   					   else
   545    649   						   sorcery.cookbook.setrecipe(rec,kind,nil,restrict)
   546    650   					   end
................................................................................
   574    678   		color = {244,255,157};
   575    679   		affinity = {'acacia','shimmering','blazing'};
   576    680   		leytype = 'cognic';
   577    681   		cast = function(ctx)
   578    682   			local center = ctx.heading.pos
   579    683   			local maxpower = 20
   580    684   			local power = (ctx.base.gem == 'sapphire' and maxpower) or maxpower/2
   581         -			local range = (ctx.base.gem == 'emerald' and 10) or 5
          685  +			local range = (ctx.base.gem == 'emerald' and 6) or 3
   582    686   			local duration = (ctx.base.gem == 'amethyst' and 60) or 30
   583    687   			if ctx.base.gem == 'diamond' then
   584    688   				power = power * (math.random()*2)
   585    689   				range = range * (math.random()*2)
   586    690   				duration = duration * (math.random()*2)
   587    691   			end
   588    692   			local lum = math.ceil((power/maxpower) * minetest.LIGHT_MAX)