sorcery  Diff

Differences From Artifact [b4ccf2cc5a]:

  • File runeforge.lua — part of check-in [96c5289a2a] at 2020-10-21 03:35:35 on branch trunk — add rune forges, runes, amulet frames, write sacrifice spell, touch up amulet graphics, enable enchantment of amulets (though spells cannot yet be cast), defuckulate syncresis core icon, unfuckitize sneaky leycalc bug that's probably been the cause of some long-standing wackiness, add item classes, add some more textures, disbungle various other asstastrophes, remove sneaky old debug code, improve library code, add utility for uploading merge requests (user: lexi, size: 8121) [annotate] [blame] [check-ins using]

To Artifact [e97e283f3c]:


            1  +-- TODO make some kind of disposable "filter" tool that runeforges require 
            2  +-- to generate runes and that wears down over time, to make amulets more
            3  +-- expensive than they currently are? the existing system is neat but
            4  +-- i think amulets are a little overpowered for something that just
            5  +-- passively consumes ley-current
            6  +
     1      7   local constants = {
     2         -	rune_mine_interval = 90;
            8  +	rune_mine_interval = 250;
     3      9   	-- how often a powered forge rolls for new runes
     4     10   
     5     11   	rune_cache_max = 4;
     6     12   	-- how many runes a runeforge can hold at a time
     7     13   	
     8         -	rune_grades = {'Fragile', 'Shoddy', 'Ordinary', 'Pristine'};
           14  +	rune_grades = {'Fragile', 'Weak', 'Ordinary', 'Pristine', 'Sublime'};
     9     15   	-- how many grades of rune quality/power there are
           16  +	
           17  +	amulet_grades = {'Slight', 'Minor', 'Major', 'Grand', 'Ultimate' };
           18  +	-- what kind of amulet each rune grade translates to
           19  +	
           20  +	phial_kinds = {
           21  +		lesser   = {grade = 1; name = 'Lesser';   infusion = 'sorcery:powder_brass'};
           22  +		simple   = {grade = 2; name = 'Simple';   infusion = 'sorcery:powder_silver'};
           23  +		great    = {grade = 3; name = 'Great';    infusion = 'sorcery:powder_gold'};
           24  +		splendid = {grade = 4; name = 'Splendid'; infusion = 'sorcery:powder_electrum'};
           25  +		exalted  = {grade = 5; name = 'Exalted';  infusion = 'sorcery:powder_levitanium'};
           26  +		supreme  = {grade = 6; name = 'Supreme';  infusion = 'sorcery:essence_force'};
           27  +	};
    10     28   }
           29  +local calc_phial_props = function(phial) --> mine interval: float, time factor: float
           30  +	local g = phial:get_definition()._proto.grade
           31  +	local i = constants.rune_mine_interval 
           32  +	local fac = (g-1) / 5
           33  +	return i - ((i*0.5) * fac), 0.5 * fac
           34  +end
    11     35   sorcery.register.runes.foreach('sorcery:generate',{},function(name,rune)
    12     36   	local id = 'sorcery:rune_' .. name
    13     37   	rune.image = rune.image or string.format('sorcery_rune_%s.png',name)
    14     38   	rune.item = id
    15     39   	minetest.register_craftitem(id, {
    16     40   		description = sorcery.lib.color(rune.tone):readable():fmt(rune.name .. ' Rune');
    17     41   		short_description = rune.name .. ' Rune';
................................................................................
    20     44   		groups = {
    21     45   			sorcery_rune = 1;
    22     46   			not_in_creative_inventory = 1;
    23     47   		};
    24     48   		_proto = { id = name, data = rune; };
    25     49   	})
    26     50   end)
           51  +
           52  +for name,p in pairs(constants.phial_kinds) do
           53  +	local f = string.format
           54  +	local color = sorcery.lib.color(255,27,188)
           55  +	local fac = p.grade / 6
           56  +	local id = f('phial_%s', name);
           57  +	sorcery.register_potion_tbl {
           58  +		name = id;
           59  +		label = f('%s Phial',p.name);
           60  +		desc = "A powerful liquid consumed in the operation of a rune forge. Its quality determines how fast new runes can be constructed.";
           61  +		color = color:brighten(1 + fac*0.5);
           62  +		imgvariant = (fac >= 5) and 'sparkle' or 'dull';
           63  +		glow = 5+p.grade;
           64  +		extra = {
           65  +			groups = { sorcery_phial = p.grade };
           66  +			_proto = { id = name, data = p };
           67  +		};
           68  +	}
           69  +	sorcery.register.infusions.link {
           70  +		infuse = p.infusion;
           71  +		into = 'sorcery:potion_subtle';
           72  +		output = id;
           73  +	}
           74  +end
           75  +
           76  +local register_rune_wrench = function(w)
           77  +	local mp = sorcery.data.metals[w.metal].parts
           78  +	minetest.register_tool(w.name, {
           79  +		description = w.desc;
           80  +		inventory_image = w.img;
           81  +		groups = {
           82  +			sorcery_magitech = 1;
           83  +			sorcery_rune_wrench = 1;
           84  +			crafttool = 50;
           85  +		};
           86  +		_proto = w;
           87  +		_sorcery = {
           88  +			recipe = { note = w.note };
           89  +		};
           90  +	})
           91  +	minetest.register_craft {
           92  +		output = w.name;
           93  +		recipe = {
           94  +			{'',                        mp.fragment,''};
           95  +			{'',                        mp.ingot,   mp.fragment};
           96  +			{'sorcery:vidrium_fragment','',         ''};
           97  +		};
           98  +	}
           99  +end
          100  +
          101  +register_rune_wrench {
          102  +	name = 'sorcery:rune_wrench', desc = 'Rune Wrench';
          103  +	img = 'sorcery_rune_wrench.png', metal = 'brass';
          104  +	powers = { imbue = 30 };
          105  +	note = 'A runeworking tool used to imbue amulets with enchantments';
          106  +}
          107  +
          108  +register_rune_wrench {
          109  +	name = 'sorcery:rune_wrench_iridium', desc = 'Iridium Rune Wrench';
          110  +	img = 'sorcery_rune_wrench_iridium.png', metal = 'iridium';
          111  +	powers = { imbue = 80, extract = 40 };
          112  +	note = 'A rare and powerful runeworking tool used to imbue amulets with enchantments, or extract runes intact from enchanted amulets';
          113  +}
    27    114   
    28    115   local rune_set = function(stack,r)
    29    116   	local m = stack:get_meta()
    30    117   	local def = stack:get_definition()._proto.data
    31    118   	local grade
    32    119   	if r.grade then grade = r.grade
    33    120   	elseif m:contains('rune_grade') then grade = m:get_int('rune_grade') end
................................................................................
    36    123   	local title = sorcery.lib.color(def.tone):readable():fmt(string.format('%s %s Rune',qpfx,def.name))
    37    124   
    38    125   	m:set_int('rune_grade',grade)
    39    126   	m:set_string('description',title)
    40    127   end
    41    128   
    42    129   sorcery.amulet = {}
    43         -sorcery.amulet.setrune = function(stack,rune)
          130  +sorcery.amulet.setrune = function(stack,rune,user)
    44    131   	local m = stack:get_meta()
    45    132   	if rune then
    46    133   		local rp = rune:get_definition()._proto
    47    134   		local rg = rune:get_meta():get_int('rune_grade')
    48    135   		m:set_string('amulet_rune', rp.id)
    49    136   		m:set_int('amulet_rune_grade', rg)
    50    137   		local spell = sorcery.amulet.getspell(stack)
    51    138   		if not spell then return nil end
    52    139   
    53         -		local name = string.format('Amulet of %s', spell.name)
    54         -
          140  +		local name = string.format('Amulet of %s %s', constants.amulet_grades[rg], spell.name)
    55    141   		m:set_string('description', sorcery.lib.ui.tooltip {
    56    142   			title = name;
    57    143   			color = spell.tone;
    58    144   			desc = spell.desc;
    59    145   		})
          146  +
          147  +		if spell.apply then spell.apply {
          148  +			stack = stack;
          149  +			meta = m;
          150  +			user = user;
          151  +			self = spell;
          152  +		} end
    60    153   	else
          154  +		local spell = sorcery.amulet.getspell(stack)
    61    155   		m:set_string('description','')
    62    156   		m:set_string('amulet_rune','')
    63    157   		m:set_string('amulet_rune_grade','')
          158  +		if spell and spell.remove then spell.remove {
          159  +			stack = stack;
          160  +			meta = m;
          161  +			user = user;
          162  +			self = spell;
          163  +		} end
    64    164   	end
    65    165   	return stack
    66    166   end
          167  +
          168  +sorcery.amulet.stats = function(stack)
          169  +	local spell = sorcery.amulet.getspell(stack)
          170  +	if not spell then return nil end
          171  +	local power = spell.grade
          172  +	
          173  +	if spell.base_spell then
          174  +		-- only consider the default effect of the frame metal
          175  +		-- if the frame doesn't totally override the spell
          176  +		power = power * (spell.framestats and spell.framestats.power or 1)
          177  +	end
          178  +
          179  +	return {
          180  +		power = power;
          181  +	}
          182  +end
    67    183   
    68    184   sorcery.amulet.getrune = function(stack)
    69    185   	local m = stack:get_meta()
    70    186   	if not m:contains('amulet_rune') then return nil end
    71    187   	local rune = m:get_string('amulet_rune')
    72    188   	local grade = m:get_int('amulet_rune_grade')
    73    189   	local rs = ItemStack(sorcery.data.runes[rune].item)
................................................................................
    74    190   	rune_set(rs, {grade = grade})
    75    191   	return rs
    76    192   end
    77    193   
    78    194   sorcery.amulet.getspell = function(stack)
    79    195   	local m = stack:get_meta()
    80    196   	local proto = stack:get_definition()._sorcery.amulet
          197  +	if not m:contains('amulet_rune') then return nil end
    81    198   	local rune = m:get_string('amulet_rune')
          199  +	local rg = m:get_string('amulet_rune_grade')
    82    200   	local rd = sorcery.data.runes[rune]
    83    201   	local spell = rd.amulets[proto.base]
    84    202   	if not spell then return nil end
    85         -	local title,desc,cast = spell.name, spell.desc, spell.cast
          203  +	local title,desc,cast,apply,remove = spell.name, spell.desc, spell.cast, spell.apply, spell.remove -- FIXME in serious need of refactoring
          204  +	local base_spell = true
    86    205   
    87    206   	if proto.frame and spell.frame and spell.frame[proto.frame] then
    88    207   		local sp = spell.frame[proto.frame]
    89    208   		title = sp.name or title
    90    209   		desc = sp.desc or desc
    91    210   		cast = sp.desc or cast
          211  +		apply = sp.apply or apply
          212  +		remove = sp.remove or remove
          213  +		base_spell = false
    92    214   	end
    93    215   	
    94    216   	return {
    95    217   		rune = rune;
          218  +		grade = rg;
    96    219   		spell = spell;
    97         -		name = title;
    98         -		desc = desc;
    99         -		cast = cast;
          220  +		name = title, desc = desc;
          221  +		cast = cast, apply = apply, remove = remove;
          222  +		frame = proto.frame;
          223  +		framestats = proto.frame and sorcery.data.metals[proto.frame].amulet;
   100    224   		tone = sorcery.lib.color(rd.tone);
          225  +		base_spell = base_spell;
   101    226   	}
   102    227   end
   103    228   
   104    229   
   105    230   local runeforge_update = function(pos,time)
   106    231   	local m = minetest.get_meta(pos)
   107    232   	local i = m:get_inventory()
   108    233   	local l = sorcery.ley.netcaps(pos,time or 1)
   109    234   
   110    235   	local pow_min = l.self.powerdraw >= l.self.minpower
   111    236   	local pow_max = l.self.powerdraw >= l.self.maxpower
          237  +	local has_phial = function() return not i:is_empty('phial') end
   112    238   
   113         -	if time and pow_min then -- roll for runes
   114         -		local rolls = math.floor(time/constants.rune_mine_interval)
          239  +	if time and has_phial() and pow_min then -- roll for runes
          240  +		local rolls = math.floor(time/calc_phial_props(i:get_stack('phial',1)))
   115    241   		local newrunes = {}
   116    242   		for _=1,rolls do
   117    243   			local choices = {}
   118    244   			for name,rune in pairs(sorcery.data.runes) do
   119    245   				if rune.minpower*time <= l.self.powerdraw and math.random(rune.rarity) == 1 then
   120    246   					local n = ItemStack(rune.item)
   121    247   					choices[#choices + 1] = n
   122    248   				end
   123    249   			end
   124    250   			if #choices > 0 then newrunes[#newrunes + 1] = choices[math.random(#choices)] end
   125    251   		end
   126    252   
   127         -		print('rolled for runes, got', dump(newrunes))
   128    253   		for _,r in pairs(newrunes) do
   129         -			if i:room_for_item('cache',r) then
          254  +			if i:room_for_item('cache',r) and has_phial() then
   130    255   				local qual = math.random(#constants.rune_grades)
   131    256   				rune_set(r,{grade = qual})
   132    257   				i:add_item('cache',r)
   133         -			end
          258  +				-- consume a phial
          259  +				local ph = i:get_stack('phial',1)
          260  +				local n = ph:get_name()
          261  +				ph:take_item(1) i:set_stack('phial',1,ph)
          262  +				minetest.add_item(pos,i:add_item('refuse',ItemStack(sorcery.register.residue.db[n])))
          263  +			else break end
   134    264   		end
   135    265   	end
   136    266   
          267  +	has_phial = has_phial()
   137    268   	local spec = string.format([[
   138    269   		formspec_version[3] size[10.25,8] real_coordinates[true]
   139    270   		list[context;cache;%f,0.25;%u,1;]
   140    271   		list[context;amulet;3.40,1.50;1,1;]
   141    272   		list[context;active;5.90,1.50;1,1;]
          273  +
          274  +		list[context;wrench;1.25,1.75;1,1;]
          275  +		list[context;phial;7.25,1.75;1,1;]
          276  +		list[context;refuse;8.50,1.75;1,1;]
          277  +
   142    278   		list[current_player;main;0.25,3;8,4;]
   143    279   
   144    280   		image[0.25,0.50;1,1;sorcery_statlamp_%s.png]
   145    281   	]], (10.5 - constants.rune_cache_max*1.25)/2, constants.rune_cache_max,
   146         -	    pow_max and 'green' or (pow_min and 'yellow') or 'off')
          282  +	    ((has_phial and pow_max) and 'green' ) or
          283  +		((has_phial and pow_min) and 'yellow') or 'off')
          284  +
          285  +	local ghost = function(slot,x,y,img)
          286  +		if i:is_empty(slot) then spec = spec .. string.format([[
          287  +			image[%f,%f;1,1;%s.png]
          288  +		]], x,y,img) end
          289  +	end
          290  +
          291  +	ghost('active',5.90,1.50,'sorcery_ui_ghost_rune')
          292  +	ghost('amulet',3.40,1.50,'sorcery_ui_ghost_amulet')
          293  +	ghost('wrench',1.25,1.75,'sorcery_ui_ghost_rune_wrench')
          294  +	ghost('phial',7.25,1.75,'vessels_shelf_slot')
   147    295   	
   148    296   	m:set_string('formspec',spec)
          297  +
          298  +	if i:is_empty('phial') then return false end
   149    299   	return true
   150    300   end
   151    301   
   152    302   local rfbox = {
   153    303   	type = 'fixed';
   154    304   	fixed = {
   155    305   		-0.5, -0.5, -0.5;
................................................................................
   180    330   		'default_copper_block.png';
   181    331   	};
   182    332   	_sorcery = {
   183    333   		ley = {
   184    334   			mode = 'consume';
   185    335   			affinity = {'praxic'};
   186    336   			power = function(pos,time)
          337  +				local i = minetest.get_meta(pos):get_inventory()
          338  +				if i:is_empty('phial') then return 0 end
          339  +				local phial = i:get_stack('phial',1)
          340  +
   187    341   				local max,min = 0
   188    342   				for _,r in pairs(sorcery.data.runes) do
   189    343   					if r.minpower > max then max = r.minpower end
   190    344   					if min == nil or r.minpower < min then min = r.minpower end
   191    345   				end
          346  +				-- high-quality phials reduce power usage
          347  +				local fac = select(2, calc_phial_props(phial))
          348  +				min = min * fac  max = max * fac
   192    349   				return min*time,max*time
   193    350   			end;
   194    351   		};
   195    352   		on_leychange = runeforge_update;
   196    353   		recipe = {
   197    354   			note = 'Periodically creates runes when sufficiently powered and can be used to imbue them into an amulet, giving it a powerful magical effect';
   198    355   		};
   199    356   	};
   200    357   	on_construct = function(pos)
   201    358   		local m = minetest.get_meta(pos)
   202    359   		local i = m:get_inventory()
   203    360   		i:set_size('cache',constants.rune_cache_max)
   204         -		i:set_size('amulet',1)
   205         -		i:set_size('active',1)
          361  +		i:set_size('wrench',1) i:set_size('phial',1) i:set_size('refuse',1)
          362  +		i:set_size('amulet',1) i:set_size('active',1)
   206    363   		m:set_string('infotext','Rune Forge')
   207    364   		runeforge_update(pos)
   208         -		minetest.get_node_timer(pos):start(constants.rune_mine_interval)
   209    365   	end;
   210    366   	after_dig_node = sorcery.lib.node.purge_only {'amulet'};
   211    367   	on_timer = runeforge_update;
   212    368   	on_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
   213    369   		local inv = minetest.get_meta(pos):get_inventory()
          370  +		local wrench if not inv:is_empty('wrench') then
          371  +			wrench = inv:get_stack('wrench',1):get_definition()._proto
          372  +		end
          373  +		local wwear = function(cap)
          374  +			local s = inv:get_stack('wrench',1)
          375  +			local wear = 65535 / wrench.powers[cap]
          376  +			s:add_wear(wear)
          377  +			inv:set_stack('wrench',1,s)
          378  +		end
   214    379   		if fl == 'active' then
   215         -			inv:set_stack('amulet',1,sorcery.amulet.setrune(inv:get_stack('amulet',1)))
   216         -		elseif tl == 'active' then
   217         -			inv:set_stack('amulet',1,sorcery.amulet.setrune(inv:get_stack('amulet',1), inv:get_stack(tl,ti)))
          380  +			inv:set_stack('amulet',1,sorcery.amulet.setrune(inv:get_stack('amulet',1),nil,user))
          381  +			-- only special wrenches can extract runes intact
          382  +			if wrench.powers.extract then wwear('extract')
          383  +				minetest.sound_play('sorcery_chime', { pos = pos, gain = 0.5 })
          384  +			elseif wrench.powers.purge then wwear('purge')
          385  +				inv:set_stack(tl,ti,ItemStack(nil))
          386  +				minetest.sound_play('sorcery_disjoin', { pos = pos, gain = 0.5 })
          387  +			end
          388  +		elseif tl == 'active' and wrench.powers.imbue then
          389  +			local amulet = sorcery.amulet.setrune(inv:get_stack('amulet',1), inv:get_stack(tl,ti), user)
          390  +			local spell = sorcery.amulet.getspell(amulet)
          391  +			sorcery.vfx.enchantment_sparkle({
          392  +				under = pos;
          393  +				above = vector.add(pos,{x=0,y=1,z=0});
          394  +			}, spell.tone:brighten(1.2):hex())
          395  +			minetest.sound_play('xdecor_enchanting', { pos = pos, gain = 0.5 })
          396  +			inv:set_stack('amulet',1,amulet)
          397  +			wwear('imbue')
   218    398   		end
          399  +		-- trigger the update early to clean up the ghost image :/
          400  +		-- minetest needs a cleaner way to handle these
          401  +		runeforge_update(pos)
   219    402   	end;
   220    403   	on_metadata_inventory_put = function(pos, list, idx, stack, user)
          404  +		local inv = minetest.get_meta(pos):get_inventory()
   221    405   		if list == 'amulet' then
   222         -			local inv = minetest.get_meta(pos):get_inventory()
   223    406   			inv:set_stack('active',1,ItemStack(sorcery.amulet.getrune(stack)))
   224    407   		end
          408  +		runeforge_update(pos)
          409  +		if not inv:is_empty('phial') then
          410  +			minetest.get_node_timer(pos):start(calc_phial_props(inv:get_stack('phial',1)))
          411  +		end
   225    412   	end;
   226    413   	on_metadata_inventory_take = function(pos, list, idx, stack, user)
   227    414   		if list == 'amulet' then
   228    415   			minetest.get_meta(pos):get_inventory():set_stack('active',1,ItemStack())
   229    416   		end
          417  +		runeforge_update(pos)
   230    418   	end;
   231    419   	allow_metadata_inventory_put = function(pos,list,idx,stack,user)
   232    420   		if list == 'amulet' then
   233    421   			if minetest.get_item_group(stack:get_name(), 'sorcery_amulet') ~= 0 then
   234    422   				return 1
   235    423   			end
          424  +		end
          425  +		if list == 'phial' then
          426  +			if minetest.get_item_group(stack:get_name(), 'sorcery_phial') ~= 0 then
          427  +				return stack:get_count()
          428  +			end
          429  +		end
          430  +		if list == 'wrench' then
          431  +			if minetest.get_item_group(stack:get_name(), 'sorcery_rune_wrench') ~= 0 then
          432  +				return 1
          433  +			end
   236    434   		end
   237    435   		return 0
   238    436   	end;
   239    437   	allow_metadata_inventory_take = function(pos,list,idx,stack,user)
   240         -		if list == 'amulet' then return 1 end
          438  +		if list == 'amulet' or list == 'wrench' then return 1 end
          439  +		if list == 'phial' or list == 'refuse' then return stack:get_count() end
   241    440   		return 0
   242    441   	end;
   243    442   	allow_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
          443  +		local inv = minetest.get_meta(pos):get_inventory()
          444  +		local wrench if not inv:is_empty('wrench') then
          445  +			wrench = inv:get_stack('wrench',1):get_definition()._proto
          446  +		end
   244    447   		if fl == 'cache' then
   245    448   			if tl == 'cache' then return 1 end
   246    449   			if tl == 'active' then
   247         -				local inv = minetest.get_meta(pos):get_inventory()
   248         -				if not inv:is_empty('amulet') then
          450  +				print(dump(wrench))
          451  +				if wrench and wrench.powers.imbue and not inv:is_empty('amulet') then
   249    452   					local amulet = inv:get_stack('amulet',1)
   250    453   					local rune = inv:get_stack(fl,fi)
   251    454   					if sorcery.data.runes[rune:get_definition()._proto.id].amulets[amulet:get_definition()._sorcery.amulet.base] then
   252    455   						return 1
   253    456   					end
   254    457   				end
   255    458   			end
   256    459   		end
   257    460   		if fl == 'active' then
   258         -			if tl == 'cache' then return 1 end
          461  +			if tl == 'cache' and wrench and (wrench.powers.extract or wrench.powers.purge) then return 1 end
   259    462   		end
   260    463   		return 0
   261    464   	end;
   262    465   })
   263    466   
   264    467   do local m = sorcery.data.metals
   265    468   	-- temporary recipe until a fancier multi-part crafting path can be come up with