-- TODO make some kind of disposable "filter" tool that runeforges require
-- to generate runes and that wears down over time, to make amulets more
-- expensive than they currently are? the existing system is neat but
-- i think amulets are a little overpowered for something that just
-- passively consumes ley-current
-- -- are phials & rune-wrenches enough for this now?
local constants = {
rune_mine_interval = 180;
-- how often a powered forge rolls for new runes
rune_cache_max = 6;
-- how many runes a runeforge can hold at a time
rune_grades = {'Fragile', 'Weak', 'Ordinary', 'Pristine', 'Sublime'};
-- how many grades of rune quality/power there are
amulet_grades = {'Slight', 'Minor', 'Major', 'Grand', 'Ultimate' };
-- what kind of amulet each rune grade translates to
phial_kinds = {
lesser = {grade = 1, name = 'Lesser'; infusion = 'sorcery:powder_brass';
dist = { Fragile = 1, Weak = 0.7, Ordinary = 0.1, Pristine = 0.05, Sublime = 0.01 };
};
simple = {grade = 2, name = 'Simple'; infusion = 'sorcery:powder_silver';
dist = { Fragile = 1, Weak = 0.8, Ordinary = 0.2, Pristine = 0.07, Sublime = 0.015 };
};
great = {grade = 3, name = 'Great'; infusion = 'sorcery:powder_gold';
dist = { Fragile = 1, Weak = 0.9, Ordinary = 0.5, Pristine = 0.1, Sublime = 0.05 };
};
splendid = {grade = 4, name = 'Splendid'; infusion = 'sorcery:powder_electrum';
dist = { Fragile = 1, Weak = 0.95, Ordinary = 0.7, Pristine = 0.3, Sublime = 0.1 };
};
exalted = {grade = 5, name = 'Exalted'; infusion = 'sorcery:powder_iridium';
dist = { Fragile = 0, Weak = 1, Ordinary = 0.9, Pristine = 0.5, Sublime = 0.25 };
};
supreme = {grade = 6, name = 'Supreme'; infusion = 'sorcery:powder_levitanium';
dist = { Fragile = 0, Weak = 0, Ordinary = 1, Pristine = 0.7, Sublime = 0.4 };
};
};
}
local calc_phial_props = function(phial) --> mine interval: float, power factor: float
local m = phial:get_meta()
local g = phial:get_definition()._proto.data.grade
local i = constants.rune_mine_interval
local fac = (g-1) / 5
fac = fac + 0.1 * m:get_int('speed')
return math.max(3,i - ((i*0.5) * fac)), 0.5 * fac
end
sorcery.register.runes.foreach('sorcery:generate',{},function(name,rune)
local id = 'sorcery:rune_' .. name
rune.image = rune.image or string.format('sorcery_rune_%s.png',name)
rune.item = id
local c = sorcery.lib.color(rune.tone)
minetest.register_craftitem(id, {
description = c:darken(0.7):bg(c:readable():fmt(rune.name .. ' Rune'));
short_description = rune.name .. ' Rune';
inventory_image = rune.image;
stack_max = 1;
groups = {
sorcery_rune = 1;
not_in_creative_inventory = 1;
};
_proto = { id = name, data = rune };
})
end)
local phkind = {
label = 'Phial';
desc = 'An alchemical substance which rune forges consume while coalescing new runes';
}
for name,p in pairs(constants.phial_kinds) do
local f = string.format
local color = sorcery.lib.color(142,232,0)
local fac = p.grade / 6
local id = f('phial_%s', name);
local fname = f('%s Phial',p.name);
local 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, and affects your odds of getting a high-quality rune."
sorcery.register_potion_tbl {
name = id;
label = fname;
desc = desc;
color = color:brighten(1 + fac*0.5);
imgvariant = (fac >= 5) and 'sparkle' or 'dull';
glow = 5+p.grade;
extra = {
groups = { sorcery_phial = p.grade };
_proto = { id = name, desc = desc, name = p.name, kind = phkind, data = p, quals = {force = true, speed = true}, color = color };
};
}
sorcery.register.infusions.link {
infuse = p.infusion;
into = 'sorcery:potion_subtle';
output = 'sorcery:'..id;
_proto = {
data = { color = color };
};
}
end
local register_rune_wrench = function(w)
local mp = sorcery.data.metals[w.metal].parts
minetest.register_tool(w.name, {
description = w.desc;
inventory_image = w.img;
groups = {
sorcery_magitech = 1;
sorcery_rune_wrench = 1;
crafttool = 50;
};
_proto = w;
_sorcery = {
recipe = { note = w.note };
};
})
minetest.register_craft {
output = w.name;
recipe = {
{'', mp.fragment,''};
{'', mp.ingot, mp.fragment};
{'sorcery:fragment_vidrium','', ''};
};
}
end
register_rune_wrench {
name = 'sorcery:rune_wrench', desc = 'Rune Wrench';
img = 'sorcery_rune_wrench.png', metal = 'brass';
powers = { imbue = 30 };
note = 'A runeworking tool used to imbue amulets with enchantments';
}
register_rune_wrench {
name = 'sorcery:rune_wrench_iridium', desc = 'Iridium Rune Wrench';
img = 'sorcery_rune_wrench_iridium.png', metal = 'iridium';
powers = { imbue = 80, extract = 40 };
note = 'A rare and powerful runeworking tool used to imbue amulets with enchantments, or extract runes intact from enchanted amulets';
}
local rune_set = function(stack,r)
local m = stack:get_meta()
local def = stack:get_definition()._proto.data
local grade
if r.grade then grade = r.grade
elseif m:contains 'rune_grade' then grade = m:get_int 'rune_grade' end
local qpfx = constants.rune_grades[grade]
local title = sorcery.lib.color(def.tone):readable():fmt(string.format('%s %s Rune',qpfx,def.name))
m:set_int('rune_grade',grade)
m:set_string('description',title)
end
sorcery.amulet = {}
sorcery.amulet.setrune = function(stack,rune,user)
local m = stack:get_meta()
if rune then
local rp = rune:get_definition()._proto
local rg = rune:get_meta():get_int 'rune_grade'
m:set_string('amulet_rune', rp.id)
m:set_int('amulet_rune_grade', rg)
local spell = sorcery.amulet.getspell(stack)
if not spell then return nil end
local name
if spell.mingrade and spell.mingrade > 0 then -- indicating quality makes less sense if it's restricted
name = string.format('Amulet of %s', spell.name)
else
name = string.format('Amulet of %s %s', constants.amulet_grades[rg], spell.name)
end
m:set_string('description', sorcery.lib.ui.tooltip {
title = name;
color = spell.tone;
desc = spell.desc;
})
if spell.apply then spell.apply {
stack = stack;
meta = m;
user = user;
self = spell;
} end
else
local spell = sorcery.amulet.getspell(stack)
m:set_string('description','')
m:set_string('amulet_rune','')
m:set_string('amulet_rune_grade','')
if spell and spell.remove then spell.remove {
stack = stack;
meta = m;
user = user;
self = spell;
} end
end
return stack
end
sorcery.amulet.stats = function(stack)
local spell = sorcery.amulet.getspell(stack)
if not spell then return nil end
local power = spell.grade
if spell.base_spell then
-- only consider the default effect of the frame metal
-- if the frame doesn't totally override the spell
power = power * (spell.framestats and spell.framestats.power or 1)
end
return {
power = power;
}
end
sorcery.amulet.getrune = function(stack)
local m = stack:get_meta()
if not m:contains('amulet_rune') then return nil end
local rune = m:get_string('amulet_rune')
local grade = m:get_int('amulet_rune_grade')
local rs = ItemStack(sorcery.data.runes[rune].item)
rune_set(rs, {grade = grade})
return rs
end
sorcery.amulet.getspell = function(stack)
local m = stack:get_meta()
local proto = stack:get_definition()._sorcery.amulet
if not m:contains('amulet_rune') then return nil end
local rune = m:get_string('amulet_rune')
local rg = m:get_int('amulet_rune_grade')
local rd = sorcery.data.runes[rune]
local spell = rd.amulets[proto.base]
if not spell then return nil end
local title,desc,cast,apply,remove,mingrade,sound = spell.name, spell.desc, spell.cast, spell.apply, spell.remove, spell.mingrade,spell.sound -- FIXME in serious need of refactoring
local base_spell = true
if proto.frame and spell.frame and spell.frame[proto.frame] then
local sp = spell.frame[proto.frame]
if not sp.mingrade or rg >= sp.mingrade then
title = sp.name or title
desc = sp.desc or desc
cast = sp.cast or cast
apply = sp.apply or apply
remove = sp.remove or remove
mingrade = sp.mingrade or mingrade
sound = sp.sound or sound
base_spell = false
end
end
-- PLEASE, GOD, REFACTOR ME
return {
rune = rune, grade = rg;
spell = spell, mingrade = mingrade;
name = title, desc = desc, sound = sound;
cast = cast, apply = apply, remove = remove;
frame = proto.frame;
framestats = proto.frame and sorcery.data.metals[proto.frame].amulet;
tone = sorcery.lib.color(rd.tone);
base_spell = base_spell;
}
end
local runeforge_update = function(pos,time)
local m = minetest.get_meta(pos)
local i = m:get_inventory()
local l = sorcery.ley.netcaps(pos,time or 1)
local probe = sorcery.spell.probe(pos)
local pow_min = l.self.powerdraw >= l.self.minpower
local pow_max = l.self.powerdraw >= l.self.maxpower
local has_phial = function() return not i:is_empty('phial') end
if time and has_phial() and pow_min and not probe.disjunction then -- roll for runes
local phial = i:get_stack('phial',1)
local int, powerfac = calc_phial_props(phial)
local pf = phial:get_meta():get_int('force')
local rolls = math.floor(time/int)
local newrunes = {}
for _=1,rolls do
local choices = {}
for name,rune in pairs(sorcery.data.runes) do
local powreq = (rune.minpower*powerfac)*time
if powreq <= l.self.powerdraw then
choices[#choices + 1] = rune
end
end
for k,v in pairs(choices) do print(' * choice',k,v.item) end
if #choices > 0 then
for try = 1,#choices do
local _, choice = sorcery.lib.tbl.pick(choices)
local adjrare = math.max(2, choice.rarity - pf)
if math.random(adjrare) == 1 then
newrunes[#newrunes + 1] = ItemStack(choice.item)
break
end
end
end
-- print('rune choices:',dump(choices))
-- print('me',dump(l.self))
end
for _,r in pairs(newrunes) do
if i:room_for_item('cache',r) and has_phial() then
local qual
-- iterate through qualities from highest to lowest, rolling against the phial's
-- distribution for each, and stopping when we find one
local qdist = phial:get_definition()._proto.data.dist
for i=#constants.rune_grades,1,-1 do
local chance = qdist[constants.rune_grades[i]]
if chance == 1 or math.random() <= chance then
qual = i
break
end
end
rune_set(r,{grade = qual})
i:add_item('cache',r)
-- consume a phial
local ph = i:get_stack('phial',1)
ph:take_item(1) i:set_stack('phial',1,ph)
minetest.add_item(pos,i:add_item('refuse',ItemStack(sorcery.register.residue.db[ph:get_name()])))
else break end
end
end
has_phial = has_phial()
local spec = string.format([[
formspec_version[3] size[10.25,8] real_coordinates[true]
list[context;amulet;3.40,1.50;1,1;]
list[context;active;5.90,1.50;1,1;]
list[context;wrench;1.25,1.75;1,1;]
list[context;phial;7.25,1.75;1,1;]
list[context;refuse;8.50,1.75;1,1;]
list[current_player;main;0.25,3;8,4;]
style_type[list;size=0.8]
list[context;cache;%f,0.25;%u,1;]
image[0.25,0.50;1,1;sorcery_statlamp_%s.png]
]], (10.5 - 0.8*(constants.rune_cache_max*1.25))/2, constants.rune_cache_max,
((not (has_phial and pow_min)) and 'off' ) or
( probe.disjunction and 'blue' ) or
((has_phial and pow_max) and 'green') or 'yellow')
local ghost = function(slot,x,y,img)
if i:is_empty(slot) then spec = spec .. string.format([[
image[%f,%f;1,1;%s.png]
]], x,y,img) end
end
ghost('active',5.90,1.50,'sorcery_ui_ghost_rune')
ghost('amulet',3.40,1.50,'sorcery_ui_ghost_amulet')
ghost('wrench',1.25,1.75,'sorcery_ui_ghost_rune_wrench')
ghost('phial',7.25,1.75,'vessels_shelf_slot')
m:set_string('formspec',spec)
if i:is_empty('phial') then return false end
return true
end
local rfbox = {
type = 'fixed';
fixed = {
-0.5, -0.5, -0.5;
0.5, 0.1, 0.5;
};
};
minetest.register_node('sorcery:runeforge', {
description = 'Rune Forge';
drawtype = 'mesh';
mesh = 'sorcery-runeforge.obj';
sunlight_propagates = true;
paramtype = 'light';
paramtype2 = 'facedir';
selection_box = rfbox;
collision_box = rfbox;
groups = {
choppy = 2;
dig_immediate = 2;
sorcery_magitech = 1;
sorcery_tech = 1;
sorcery_ley_device = 1;
};
tiles = {
'default_diamond_block.png';
'default_tin_block.png';
'sorcery_metal_iridium_shiny.png';
'sorcery_metal_vidrium_shiny.png';
'default_copper_block.png';
};
_sorcery = {
ley = {
mode = 'consume';
affinity = {'praxic'};
power = function(pos,time)
local i = minetest.get_meta(pos):get_inventory()
if i:is_empty('phial') then return 0 end
local phial = i:get_stack('phial',1)
local max,min = 0
for _,r in pairs(sorcery.data.runes) do
if r.minpower > max then max = r.minpower end
if min == nil or r.minpower < min then min = r.minpower end
end
-- high-quality phials reduce power usage
local fac = select(2, calc_phial_props(phial))
min = min / fac max = max / fac
return min*time,max*time
end;
};
on_leychange = runeforge_update;
recipe = {
note = 'Periodically creates runes when sufficiently powered and can be used to imbue them into an amulet, giving it a powerful magical effect';
};
};
on_construct = function(pos)
local m = minetest.get_meta(pos)
local i = m:get_inventory()
i:set_size('cache',constants.rune_cache_max)
i:set_size('wrench',1) i:set_size('phial',1) i:set_size('refuse',1)
i:set_size('amulet',1) i:set_size('active',1)
m:set_string('infotext','Rune Forge')
runeforge_update(pos)
end;
after_dig_node = sorcery.lib.node.purge_only {'amulet','wrench','refuse','phial'};
on_timer = runeforge_update;
on_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
local inv = minetest.get_meta(pos):get_inventory()
local wrench if not inv:is_empty('wrench') then
wrench = inv:get_stack('wrench',1):get_definition()._proto
end
local wwear = function(cap)
local s = inv:get_stack('wrench',1)
local wear = 65535 / wrench.powers[cap]
s:add_wear(wear)
inv:set_stack('wrench',1,s)
end
if fl == 'active' then
inv:set_stack('amulet',1,sorcery.amulet.setrune(inv:get_stack('amulet',1),nil,user))
-- only special wrenches can extract runes intact
if wrench.powers.extract then wwear 'extract'
minetest.sound_play('sorcery_chime', { pos = pos, gain = 0.5 })
elseif wrench.powers.purge then wwear 'purge'
inv:set_stack(tl,ti,ItemStack(nil))
minetest.sound_play('sorcery_disjoin', { pos = pos, gain = 0.5 })
end
elseif tl == 'active' and wrench.powers.imbue then
local amulet = sorcery.amulet.setrune(inv:get_stack('amulet',1), inv:get_stack(tl,ti), user)
local spell = sorcery.amulet.getspell(amulet)
sorcery.vfx.enchantment_sparkle({
under = pos;
above = vector.add(pos,{x=0,y=1,z=0});
}, spell.tone:brighten(1.2):hex())
minetest.sound_play('xdecor_enchanting', { pos = pos, gain = 0.5 })
inv:set_stack('amulet',1,amulet)
wwear 'imbue'
end
-- trigger the update early to clean up the ghost image :/
-- minetest needs a cleaner way to handle these
runeforge_update(pos)
end;
on_metadata_inventory_put = function(pos, list, idx, stack, user)
local inv = minetest.get_meta(pos):get_inventory()
if list == 'amulet' then
inv:set_stack('active',1,ItemStack(sorcery.amulet.getrune(stack)))
end
runeforge_update(pos)
if not inv:is_empty('phial') then
minetest.get_node_timer(pos):start(calc_phial_props(inv:get_stack('phial',1)))
end
end;
on_metadata_inventory_take = function(pos, list, idx, stack, user)
if list == 'amulet' then
minetest.get_meta(pos):get_inventory():set_stack('active',1,ItemStack())
end
runeforge_update(pos)
end;
allow_metadata_inventory_put = function(pos,list,idx,stack,user)
if list == 'amulet' then
if minetest.get_item_group(stack:get_name(), 'sorcery_amulet') ~= 0 then
return 1
end
end
if list == 'phial' then
if minetest.get_item_group(stack:get_name(), 'sorcery_phial') ~= 0 then
return stack:get_count()
end
end
if list == 'wrench' then
if minetest.get_item_group(stack:get_name(), 'sorcery_rune_wrench') ~= 0 then
return 1
end
end
return 0
end;
allow_metadata_inventory_take = function(pos,list,idx,stack,user)
if list == 'amulet' or list == 'wrench' then return 1 end
if list == 'phial' or list == 'refuse' then return stack:get_count() end
return 0
end;
allow_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
local inv = minetest.get_meta(pos):get_inventory()
local probe = sorcery.spell.probe(pos)
local wrench if not inv:is_empty('wrench') then
wrench = inv:get_stack('wrench',1):get_definition()._proto
end
if fl == 'cache' then
if probe.disjunction then return 0 end
if tl == 'cache' then return 1 end
if tl == 'active' and inv:is_empty('active') then
if wrench and wrench.powers.imbue and not inv:is_empty('amulet') then
local amulet = inv:get_stack('amulet',1)
local rune = inv:get_stack(fl,fi)
local runeid = rune:get_definition()._proto.id
local runegrade = rune:get_meta():get_int('rune_grade')
if sorcery.data.runes[runeid].amulets[amulet:get_definition()._sorcery.amulet.base] then
local spell do -- haaaack
local i=ItemStack(amulet:get_name())
local im = i:get_meta()
im:set_string('amulet_rune',runeid)
im:set_int('amulet_rune_grade',runegrade)
spell = sorcery.amulet.getspell(i)
end
if not spell.mingrade or runegrade >= spell.mingrade then
return 1
end
end
end
end
end
if fl == 'active' then
if probe.disjunction then return 0 end
if tl == 'cache' and wrench and (wrench.powers.extract or wrench.powers.purge) then return 1 end
end
return 0
end;
})
do local m = sorcery.data.metals
-- temporary recipe until a fancier multi-part crafting path can be come up with
-- TODO: better than this
minetest.register_craft {
output = 'sorcery:runeforge';
recipe = {
{'default:copper_ingot',m.vidrium.parts.block,'default:copper_ingot'};
{'default:diamond',m.iridium.parts.ingot,'default:diamond'};
{'default:tin_ingot','sorcery:core_syncretic','default:tin_ingot'};
};
}
end