sorcery  liquid.lua at [0dbb4f52c3]

File liquid.lua artifact 1c87060d5d part of check-in 0dbb4f52c3


-- liquid.lua
-- the liquid registry is used to keep track of abstract liquids,
-- their properties, and their representation in-game.

sorcery.registry.mk('liquid', false)
local mkunit = function(unit,fac)
	return function(amt)
		-- allow division for more accurate results
		if fac >= 0 then
			amt = amt * fac
		else
			amt = amt / (-fac)
		end
		return string.format('%s %s%s', amt, unit, (amt == 1) and '' or 's')
	end
end
sorcery.liquid = {
	constants = {
		drams_per_glass = 64;
		pints_per_glass = 0.5;
		glasses_per_bottle = 3;
		bottles_per_bucket = 3;
		bottles_per_trough = 6;
	};
	unit = mkunit;
}
local constants = sorcery.liquid.constants
sorcery.liquid.units = {
	dram = mkunit('dram', constants.drams_per_glass);
	pint = mkunit('pint', constants.pints_per_glass);
	draught = mkunit('draught', -3);
	drink = mkunit('drink', 1);
};

local L = sorcery.lib
local log = sorcery.logger('liquid')

sorcery.liquid.fill_from_basin = function(ctr, liquid, basin)
	local liq = sorcery.register.liquid.db[liquid]
	local filled
	if type(ctr) == 'string'
		then filled = liq.containers[ctr] ctr=ItemStack(ctr)
		else filled = liq.containers[ctr:get_name()]
	end
	if type(filled) == 'string' then
		local fs = sorcery.itemclass.get(filled, 'container')
		if not fs then log.err(filled,'is named as filled container but does not have the required itemclass definition') return end

		local item_name = filled
		filled = {
			min = fs.charge, max = fs.charge, res = 1;
			make = function(amt,ct) return ItemStack{
				name = item_name, count = ct
			} end
		}
	end
	if not filled then return nil end

	local num_ctrs = ctr:get_count()
	local res = filled.res or 1
	local qty = math.min(
		math.max((filled.min or 1)*num_ctrs, basin),
		(filled.max or 1)*num_ctrs)
	
	if basin >= qty then
		return filled.make(qty / num_ctrs, num_ctrs), basin - qty
	end
end

sorcery.liquid.mktrough = function(liq)
	-- troughs are used for collecting liquid from the environment,
	-- like rainwater and tree sap. they hold twice as much as a bucket
	local Q = constants.glasses_per_bottle
	local trough_mkid = function(l,i)
		if type(l) == 'string' then l = sorcery.register.liquid.db[l] end
		if (not l) or (not i) or i < 1 then return 'sorcery:trough' end
		return string.format('%s:trough_%s_%u', l.mod,l.sid,i)
	end
	local lid = function(l) return trough_mkid(liq, l) end

	local M = constants.bottles_per_trough
	local mkbox = function(lvl)
		local pxl = function(tbl) -- for mapping to txcoords
			return L.tbl.map(tbl, function(x)
				return (1/16 * x) - 0.5
			end)
		end
		local h = 12
		local geom = {
			pxl {2,0,2; 14, 2, 14};
			pxl {2,2,2; 4,h,14};
			pxl {2,2,2; 14,h,4};

			pxl {12,2,2; 14,h,14};
			pxl {2,2,12; 14,h,14};
		}
		if lvl > 0 then
			local fac = lvl / M
			return L.tbl.append({
				pxl {4,2,4; 12, 2 + ((h-3)*fac), 12};
			}, geom)
		else return geom end
	end
	local f = liq and 1 or 0
	for i = 1*f,M*f do
		local top = L.image('sorcery_trough_top_overlay.png')
		if liq then top = top:blit( 
			L.image('sorcery_node_liquid.png'):multiply(L.color(liq.color))
		) else top=top:blit(
			L.image('sorcery_trough_bottom.png')
		) end
		local ttlc = function(liq,i) return
			liq and string.format('%s Trough', L.str.capitalize(liq.name)),
			liq and string.format('%s of %s', liq.measure(i * Q), liq.name)
		end
		local trough_title, trough_content = ttlc(liq,i)
		local function trough_caption(pos,i,l) 
			local trough_title, trough_content = ttlc(l or liq,i)
			minetest.get_meta(pos):set_string('infotext', i > 0 and string.format(
				'%s\n(%s)', trough_title, trough_content
			) or 'Empty Trough')
		end
		sorcery.register.residue.link(lid(i),lid(0))
		minetest.register_node(':'..lid(i), {
			description = liq and L.ui.tooltip {
				title = trough_title;
				color = L.color(liq.color);
				desc = trough_content;
			} or 'Trough';
			short_description = liq and string.format('%s Trough', L.str.capitalize(liq.name)) or 'Trough';
			drawtype = 'nodebox';
			paramtype = 'light';
			groups = {
				dig_immediate = 3; not_in_creative_inventory = liq and 1;
				attached_node = 1;
				sorcery_trough = 1; sorcery_container = 2; metal = 1;
				sorcery_collect_rainwater = (liq == nil or (liq.collect_rainwater and i ~= M)) and 1 or nil;
			};
			on_construct = function(pos)
				trough_caption(pos,i)
			end;
			on_rightclick = i > 0 and function(pos, node, who, stack)
				if not stack or stack:is_empty() then return end
				if liq then
					local filled, amtleft = sorcery.liquid.fill_from_basin(stack, liq.id, i * Q)
					if filled then
						sorcery.liquid.sound_dip(i - amtleft, i, pos)
						minetest.swap_node(pos, {name = lid(amtleft / Q)})
						trough_caption(pos,amtleft/Q)
						return filled
					end
				end
			end;
			node_box = { type = 'fixed', fixed = mkbox(i) };
			tiles = {
				top:render();
				'sorcery_trough_side.png';
				'sorcery_trough_bottom.png';
			};
			_sorcery = {
				material = liq == nil and {
					metal = true;
					name = 'aluminum';
					data = sorcery.data.metals.aluminum;
					value = 7*4;
				} or nil;
				container = {
					type = 'bucket';
					hold = 'liquid';
					has = liq and liq.id;
					charge = liq and Q * i;
					empty = 'sorcery:trough';
					max = constants.bottles_per_trough * Q;
					set_node_vol = liq and function(pos, vol)
						vol = math.min(M, math.max(0, math.floor(vol / Q)))
						minetest.swap_node(pos, {name = lid(vol)})
						trough_caption(pos, vol)
					end;
					set_node_liq = function(pos, liq, vol)
						log.act('adding', vol, 'to trough at', liq)
						vol = vol or Q * i
						local idx = math.min(M, math.floor(vol/Q))
						minetest.swap_node(pos, {name = trough_mkid(liq, idx)})
						trough_caption(pos, idx, liq)
					end
				}
			};
		})
	end
end
sorcery.liquid.mktrough()

sorcery.liquid.measure_default = sorcery.liquid.units.dram

sorcery.liquid.container = function(liq, ctr)
	return liq.containers[({
		bottle = 'vessels:glass_bottle';
		glass = 'vessels:drinking_glass';
		keg = 'sorcery:keg';
		trough = 'sorcery:trough';
	})[ctr] or ctr]
end

sorcery.liquid.register = function(liq)
	local fmt = string.format
	local Q = constants.glasses_per_bottle
	liq.sid = liq.sid or liq.id:gsub('^[^:]+:','')
	liq.mod = liq.mod or liq.id:gsub('^([^:]+):.*','%1')
	if not liq.measure then
		liq.measure = sorcery.liquid.measure_default
	end
	if liq.autogen then
		local glass = fmt('%s:liquid_%s_glass', liq.mod, liq.sid);
		local bottle = fmt('%s:liquid_%s_bottle', liq.mod, liq.sid);
		liq.containers = liq.containers or {}
		-- liq.containers['vessels:drinking_glass'] = glass;
		liq.containers['vessels:glass_bottle'] = bottle;

		local img_bottle = liq.img_bottle or L.image('vessels_glass_bottle.png'):blit(
				L.image(fmt('sorcery_liquid_%s.png', liq.imgvariant or 'dull'))
					:multiply(L.color(liq.color))):render()

		-- local img_glass = L.image('vessels_drinking_glass.png'):blit(
		-- 		L.image(fmt('sorcery_liquid_glass_%s.png', liq.imgvariant or 'dull'))
		-- 			:multiply(L.color(liq.color)))

		sorcery.lib.node.reg_autopreserve(':'..bottle, {
			description = liq.desc_bottle or fmt('%s Bottle', L.str.capitalize(liq.name));
			inventory_image = img_bottle;
			drawtype = 'plantlike', tiles = {img_bottle};
			is_ground_content = false, walkable = false;
			sunlight_propagates = true, paramtype = 'light';
			light_source = liq.glow or 0;
			selection_box = { type = 'fixed', fixed = {-0.25, -0.5, -0.25, 0.25, 0.3, 0.25} };
			sounds = default.node_sound_glass_defaults();
			groups = L.tbl.merge({dig_immediate = 3; attached_node = 1; vessel = 1}, liq.bottle_groups or {});
			_sorcery = {
				container = {
					type = 'vessel', hold = 'liquid';
					has = liq.id;
					empty = 'vessels:glass_bottle';
					charge = Q;
				}
			};
		})
	end

	sorcery.register.liquid.link(liq.id, liq)

	if liq.usetrough then
		sorcery.liquid.mktrough(liq)
		liq.containers = liq.containers or {}
		liq.containers['sorcery:trough'] = {
			max = constants.bottles_per_trough * Q, res = Q;
			make = function(amt,ct)
				return ItemStack{
					name = string.format('%s:trough_%s_%u', liq.mod, liq.sid, math.min(amt/Q, constants.bottles_per_trough));
					count = ct;
				}
			end;
		}
	end
end;

sorcery.liquid.sound_pour = function(amt_input, amt_basin, pos)
	log.act('playing sound at',pos)
	minetest.sound_play('default_water_footstep', {
		gain = math.min(0.5 + amt_input / 9.0,3.5);
		-- pitch = 1.0;
		pos = pos;
	}, true)
end;

sorcery.liquid.sound_dip = function(amt_output, amt_basin, pos)
	sorcery.liquid.sound_pour(amt_output, amt_basin, pos)
end;


-- pre-register basic liquids used in Sorcery and common ones sorcery depends on

sorcery.liquid.register{
	id = 'default:water';
	name = 'water';
	kind = 'default:drink';
	color = {10,85,255};
	proto = nil;
	src = 'default:water_source';
	usetrough = true;
	collect_rainwater = true;
	containers = {
		['vessels:glass_bottle'] = 'sorcery:potion_water';
		['bucket:bucket_empty'] = 'bucket:bucket_water';
	};
}

sorcery.liquid.register {
	id = 'farming:ethanol';
	name = 'ethanol';
	kind = 'default:fuel';
	color = {175,185,130};
	proto = nil;
	measure = function(u) return string.format('%s pints', u * 5) end;
	containers = {
		['vessels:glass_bottle'] = 'farming:ethanol_bottle';
	};
}

sorcery.liquid.register {
	id = 'sorcery:blood';
	name = 'blood';
	kind = 'sorcery:reagent';
	color = {255,10,30};
	proto = nil;
	usetrough = true;
	measure = function(u) return string.format('%s cc', u * 236.5) end;
	containers = {
		['vessels:glass_bottle'] = 'sorcery:blood';
	};
}

minetest.register_abm {
	label = 'Rainfall';
	nodenames = {'group:sorcery_collect_rainwater'};
	interval =  120;
	chance = 27;
	min_y = -400;
	catch_up = true;
	action = function(pos, node)
		-- TODO vary by season and biome?
		if minetest.get_natural_light(vector.offset(pos,0,1,0), 0.5) >= 15 then
			if node.name == 'sorcery:trough' then
				node.name = 'default:trough_water_1'
			else
				local lvl = minetest.registered_nodes[node.name]._sorcery.container.charge / constants.glasses_per_bottle
				node.name = 'default:trough_water_' .. tostring(lvl+1)
			end
			minetest.set_node(pos, node)
		end
	end;
}