-- [ʞ] fab.lua
-- ~ lexi hale <lexi@hale.su>
-- 🄯 EUPL1.2
-- ? fabrication spec class
-- a type.fab supports two operators:
--
-- + used for compounding recipes. that is,
-- a+b = compose a new spec from the spec parts a and b.
-- this is used e.g. for creating tier-based
-- fabspecs.
--
-- * used for determining quantities. that is,
-- f*x = spec to make x instances of f
--
-- new fab fields must be defined in starlit.type.fab.fields.
-- this maps a name to fn(a,b,n) -> quant, where a is the first
-- argument, b is a compounding amount, and n is a quantity of
-- items to produce. fields that are unnamed will be underwritten
local fab
local function fQuant(a,b,n) return ((a or 0)+(b or 0))*n end
local function fFac (a,b,n)
if a == nil and b == nil then return nil end
local f if a == nil or b == nil then
f = a or b
else
f = (a or 1)*(b or 1)
end
return f*n
end
local function fReq (a,b,n) return a or b end
local function fFlag (a,b,n) return a and b end
local function fSize (a,b,n) return math.max(a,b) end
local F = string.format
local lib = starlit.mod.lib
local function fRawMat(class)
return function(x,n,stack)
local def = stack:get_definition()._starlit
if not def.material then return 0 end
local mf = fab {[def.material.kind] = {[def.material[def.material.kind]] = def.mass}}
-- this is bugged: the same item can satisfy both e.g. metal.steel and element.fe
-- if not (mf[class] and mf[class][x]) then
-- mf = mf:elementalize()
if not (mf[class] and mf[class][x]) then return 0 end
-- end
local perItem = mf[class][x]
local wholeStack = perItem * stack:get_count()
local deduct = ItemStack()
local taken = 0 repeat
taken = taken + perItem
deduct:add_item(stack:take_item(1))
until taken >= n or stack:is_empty()
return taken, deduct
--[[ outsmarted myself with this one :/
local fab = def.recover or def.fab
-- we ignore recover_vary bc this needs to be deterministic
local function tryFab(fab)
if not fab then return 0 end
if fab[class] and fab[class][x] then
local perItem = fab[class][x]
local wholeStack = perItem * stack:get_count()
print('fab has substance', n, perItem, wholeStack)
local deduct = ItemStack()
local taken = 0 repeat
taken = taken + perItem
deduct:add_item(stack:take_item(1))
until taken >= n
return taken, deduct
end
return 0
end
local z,c = tryFab(fab)
if z == 0 then -- does it work if we break down the constituent compounds?
z,c = tryFab(fab:elementalize())
end]]
end
end
local function fCanister(class)
return function(x, n, stack)
local amt, deduct = 0
return amt, deduct
end
end
local fields = {
-- fabrication eligibility will be determined by which kinds
-- of input a particular fabricator can introduce. e.g. a
-- printer with a but no cache can only print items whose
-- recipe only names elements as ingredients
element = {
name = {"element", "elements"};
string = function(x, n, long)
local el = starlit.world.material.element.db[x]
return lib.math.siUI('g', n) .. ' ' .. ((not long and el.sym) or el.name)
end;
image = function(x, n)
return string.format('starlit-element-%s.png', x)
end;
inventory = fRawMat 'element';
op = fQuant;
};
metal ={
name = {"metal", "metals"};
string = function(x, n)
local met = starlit.world.material.metal.db[x]
return lib.math.siUI('g', n) .. ' ' .. met.name
end;
image = function(x, n)
local met = starlit.world.material.metal.db[x]
return ItemStack(met.form.brick):get_definition().inventory_image
end;
inventory = fRawMat 'metal';
op = fQuant;
};
liquid = {
name = {"liquid", "liquids"};
string = function(x, n)
local liq = starlit.world.material.liquid.db[x]
return lib.math.siUI('L', n) .. ' ' .. liq.name
end;
inventory = fCanister 'liquid';
op = fQuant;
};
gas = {
name = {"gas", "gasses"};
string = function(x, n)
local gas = starlit.world.material.gas.db[x]
return lib.math.siUI('g', n) .. ' ' .. gas.name
end;
inventory = fCanister 'gas';
op = fQuant;
};
-- crystal = {
-- op = fQuant;
-- };
item = {
name = {"item", "items"};
string = function(x, n)
local i = minetest.registered_items[x]
return tostring(n) .. 'x ' .. i.short_description
end;
image = function(x, n)
return ItemStack(x):get_definition().inventory_image
end;
inventory = function(x, n, stack)
x = ItemStack(x)
if not x:equals(stack) then return nil end
local deduct = stack:take_item(x:get_count() * n)
return deduct:get_count(), deduct
end;
};
-- factors
cost = {
name = {"cost", "costs"};
op=fFac; -- units vary
string = function(x,n)
local units = {
power = 'J';
}
local s
if units[x] then
s = lib.math.siUI(units[x], n)
elseif starlit.world.stats[x] then
s = starlit.world.stats[x].desc(n)
else
s = tostring(n)
end
return string.format('%s: %s',x,s)
end;
image = function(x,n)
local icons = {
power = 'starlit-ui-icon-stat-power.png';
numina = 'starlit-ui-icon-stat-numina.png'
}
return icons[x]
end;
};
time = {op=fFac}; -- (s)
-- print: base printing time
size = {op=fSize};
-- printBay: size of the printer bay necessary to produce the item
req = {op=fReq};
flag = {op=fFlag}; -- means that can be used to produce the item & misc flags
-- print: allow production with a printer
-- smelt: allow production with a smelter
-- all else defaults to underwrite
}
local order = {
'element', 'metal', 'liquid', 'gas', 'item',
'cost'
}
local lib = starlit.mod.lib
fab = lib.class {
__name = 'starlit:fab';
fields = fields;
order = order;
construct = function(q) return q end;
__index = {
elementalize = function(self)
local e = fab {element = self.element or {}}
for _, kind in pairs {'metal', 'gas', 'liquid'} do
for m,mass in pairs(self[kind] or {}) do
local mc = starlit.world.material[kind].db[m].composition
e = e + mc:elementalize()*mass
end
end
return e
end;
elementSeq = function(self)
local el = {}
local em = self.element
local s = 0
local eldb = starlit.world.material.element.db
for k in pairs(em) do table.insert(el, k) s=s+eldb[k].n end
table.sort(el, function(a,b)
return eldb[a].n > eldb[b].n
end)
return el, em, s
end;
formula = function(self)
local ts,f=0
if self.element then
f = {}
local el, em, s = self:elementSeq()
local eldb = starlit.world.material.element.db
for i, e in ipairs(el) do
local sym, n = eldb[e].sym, em[e]
if n > 0 then
table.insert(f, string.format("%s%s",
sym, n>1 and lib.str.nIdx(n) or ''))
end
end
f = table.concat(f)
ts = ts + s
end
local sub = {}
for _, w in pairs {'metal', 'gas', 'liquid'} do
if self[w] then
local mdb = starlit.world.material[w].db
for k, amt in pairs(self[w]) do
local mf, s = mdb[k].composition:formula()
if amt > 0 then table.insert(sub, {
f = string.format("(%s)%s",mf,
lib.str.nIdx(amt));
s = s;
}) end
ts = ts + s*amt
end
end
end
table.sort(sub, function(a,b) return a.s > b.s end)
local fml = {}
for i, v in ipairs(sub) do fml[i] = v.f end
if f then table.insert(fml, f) end
fml = table.concat(fml, ' + ')
return fml, ts
end;
visualize = function(self)
local all = {}
for i,o in ipairs(order) do
local t = {}
if self[o] then
for mat,amt in pairs(self[o]) do
local v = {}
v.id = mat
v.n = amt
if fields[o].string then
v.label = fields[o].string(mat,amt,true)
end
if fields[o].image then
v.img = fields[o].image(mat,amt)
end
table.insert(t,v)
end
end
if fields[o].sort then
table.sort(t, function(a,b) return fields[o].sort(a.id, b.id) end)
end
if next(t) then table.insert(all, {
id=o, list=t;
header=fields[o].name[t[2] and 2 or 1];
}) end
end
return all
end;
seek = function(self, invs)
local consumed = {}
local spec = fab{item={}} -- used to generate a convenient visualization
local unsatisfied = fab{}
local cache = {}
local leftover = fab{}
local function alreadyGot(inv,slot)
local already = cache[inv] and cache[inv][slot] and true
if cache[inv] == nil then cache[inv] = {} end
cache[inv][slot] = true
return already
end
for ci, cat in ipairs(order) do
local scan = fields[cat].inventory
if scan and self[cat] then
for substance, amt in pairs(self[cat]) do
-- print('check substance', substance, amt, dump(self[cat]))
local amtFound = 0
local stacks = {}
for ii, inv in ipairs(invs) do
-- print(' - check inventory',ii,inv,'for',cat,substance,amt)
for oi, o in ipairs(inv) do
-- print(' - check stack', oi, o)
local st = ItemStack(o)
if not st:is_empty() then
local avail, deduct = scan(substance,amt,st)
if avail > 0 then
amtFound = amtFound + avail
-- print(' - found amt', amtFound,ii,oi)
if not alreadyGot(ii,oi) then
local sv = {
inv=ii, slot=oi;
consume=deduct, remain=st;
satisfy=fab{[cat]={[substance]=avail}}
}
table.insert(stacks, sv)
end
if amtFound >= amt then goto suffice end
end
end
end
end
::insufficient:: do -- record the failure and move on
if unsatisfied[cat] == nil then unsatisfied[cat] = {} end
unsatisfied[cat][substance] = amt-amtFound
end
::suffice:: -- commit the stack diff
for si,sv in ipairs(stacks) do
-- table.insert(consumed, sv)
local di = ItemStack(sv.consume)
local din = ItemStack(sv.consume):get_name()
if not spec.item[din] then spec.item[din] = 0 end
spec.item[din] = spec.item[din] + di:get_count()
local lo = amtFound-amt if lo > 0 then
leftover = leftover + fab{[cat]={[substance]=lo}}
end
end
end
end
end
return (next(unsatisfied) == nil), consumed, unsatisfied, leftover, spec
end;
};
__tostring = function(self)
local t = {}
for i,o in ipairs(order) do
if self[o] and fields[o].string then
for mat,amt in pairs(self[o]) do
if amt > 0 then
table.insert(t, fields[o].string(mat, amt))
end
end
end
end
return table.concat(t, ", ")
end;
__add = function(a,b)
local new = fab {}
for cat, vals in pairs(a) do
new[cat] = lib.tbl.copy(vals)
end
for cat, vals in pairs(b) do
if not new[cat] then
new[cat] = lib.tbl.copy(vals)
else
local f = fields[cat].op
for k,v in pairs(vals) do
local n = f(new[cat][k], v, 1)
new[cat][k] = n > 0 and n or nil
end
end
end
return new
end;
__mul = function(x,n)
local new = fab {}
for cat, vals in pairs(x) do
new[cat] = {}
local f = fields[cat].op
for k,v in pairs(vals) do
local num = f(v,nil,n)
new[cat][k] = num > 0 and num or nil
end
end
return new
end;
__div = function(x,n)
return x * (1/n)
end;
}
starlit.type.fab = fab