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]
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