starlit  Check-in [a810a756ce]

Overview
Comment:cleanups, fixes, begin canister rework, begin ecology
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: a810a756ce2ab6fe9009feee5e3634704d85eecb804d1ed7fd8f3ee3d0b55af6
User & Date: lexi on 2024-05-01 13:46:45
Other Links: manifest | tags
Context
2024-05-01
16:25
fixes, sounds; add license info check-in: 0e67c606c9 user: lexi tags: trunk
13:46
cleanups, fixes, begin canister rework, begin ecology check-in: a810a756ce user: lexi tags: trunk
2024-04-29
22:49
rename again check-in: 187bf04c87 user: lexi tags: trunk
Changes

Modified mods/starlit-electronics/sw.lua from [288b4b5859] to [d777bc86b5].

     3      3   --  🄯 EUPL v1.2
     4      4   --  ? 
     5      5   
     6      6   -------------------------------
     7      7   -- basic suit nano abilities --
     8      8   -------------------------------
     9      9   local function shredder(prop)
    10         -	local function getItemsForFab(fab)
           10  +	local function fabToItemsAndCharges(fab)
    11     11   		local elt
    12     12   		if fab then
    13     13   			elt = fab:elementalize()
    14     14   		else
    15     15   			elt = {}
    16     16   		end
    17         -		local items = {}
           17  +		local items,charges = {},{}
    18     18   		if elt.element then
    19     19   			for k,v in pairs(elt.element) do
    20         -				local st = ItemStack {
    21         -					name = starlit.world.material.element.db[k].form.element;
    22         -					count = v;
    23         -				}
    24         -				table.insert(items, st)
           20  +				local forms =  starlit.world.material.element.db[k].form
           21  +				if forms.brick then
           22  +					local st = ItemStack {
           23  +						name = forms.brick;
           24  +						count = math.floor(v);
           25  +					}
           26  +					table.insert(items, st)
           27  +				else -- gas, liquid
           28  +					table.insert(charges, {id = k, mass = v})
           29  +				end
    25     30   			end
    26     31   		end
    27         -		return items
           32  +		print(dump(items))
           33  +		return items, charges
    28     34   	end
    29     35   
    30     36   	return function(user, ctx)
    31     37   		local function cleanup()
    32     38   			user.action.prog.shred = nil
    33     39   			if user.action.sfx.shred then
    34     40   				minetest.sound_fade(user.action.sfx.shred, 1, 0)
................................................................................
   101    107   					user:suitSound 'starlit-success'
   102    108   					if fab then
   103    109   						local vf = fab
   104    110   						if vary then
   105    111   							local rng = (starlit.world.seedbank+0xa891f62)[minetest.hash_node_position(what)]
   106    112   							vf = vf + vary(rng, {})
   107    113   						end
   108         -						local items = getItemsForFab(vf)
          114  +						local items, charges = fabToItemsAndCharges(vf)
   109    115   						for i, it in ipairs(items) do user:give(it) end
          116  +						-- TODO give gasses, liquids
   110    117   					end
   111    118   				else
   112    119   					user:suitSound 'starlit-error'
   113    120   				end
   114    121   				cleanup()
   115    122   			end
   116    123   		elseif ctx.how.state == 'halt' then

Modified mods/starlit-material/elements.lua from [5740080b38] to [2a0859e47b].

     1      1   local lib = starlit.mod.lib
     2      2   local W = starlit.world
     3      3   local M = W.material
     4      4   
     5      5   M.element.meld {
     6      6   	hydrogen = {
     7         -		name = 'hydrogen', sym = 'H', n = 1;
            7  +		name = 'hydrogen', sym = 'H', n = 1;  density = 8.988e-5;
     8      8   		gas = true;
     9      9   		color = lib.color(1,0.8,.3);
    10     10   	};
    11     11   	beryllium = {
    12         -		name = 'Beryllium', sym = 'Be', n = 4;
           12  +		name = 'beryllium', sym = 'Be', n = 4;  density = 0;
    13     13   		metal = true; -- rare emerald-stuff
    14     14   		color = lib.color(0.2,1,0.2);
    15     15   	};
    16     16   	oxygen = {
    17         -		name = 'oxygen', sym = 'O', n = 8;
           17  +		name = 'oxygen', sym = 'O', n = 8;  density = 0.001429;
    18     18   		gas = true;
    19     19   		color = lib.color(.2,1,.2);
    20     20   	};
    21     21   	carbon = {
    22         -		name = 'carbon', sym = 'C', n = 6;
           22  +		name = 'carbon', sym = 'C', n = 6, density = 2.266; -- g/cm³
    23     23   		color = lib.color(.7,.2,.1);
    24     24   	};
    25     25   	silicon = {
    26         -		name = 'silicon', sym = 'Si', n = 14;
           26  +		name = 'silicon', sym = 'Si', n = 14, density = 2.329;
    27     27   		metal = true; -- can be forged into an ingot
    28     28   		color = lib.color(.6,.6,.4);
    29     29   	};
    30     30   	potassium = {
    31         -		name = 'potassium', sym = 'K', n = 19;
           31  +		name = 'potassium', sym = 'K', n = 19, density = 0.862;
    32     32   		-- potassium is technically a metal but it's so soft
    33     33   		-- it can be easily nanoworked without high temps, so
    34     34   		-- ingots make no sense
    35     35   		color = lib.color(1,.8,0.1);
    36     36   	};
    37     37   	calcium = {
    38         -		name = 'calcium', sym = 'Ca', n = 20;
           38  +		name = 'calcium', sym = 'Ca', n = 20; density = 1.55;
    39     39   		metal = true;
    40     40   		color = lib.color(1,1,0.7);
    41     41   	};
    42     42   	aluminum = {
    43         -		name = 'aluminum', sym = 'Al', n = 13;
           43  +		name = 'aluminum', sym = 'Al', n = 13;  density = 2.7;
    44     44   		metal = true;
    45     45   		color = lib.color(0.9,.95,1);
    46     46   	};
    47     47   	iron = {
    48         -		name = 'iron', sym = 'Fe', n = 26;
           48  +		name = 'iron', sym = 'Fe', n = 26;  density = 7.874;
    49     49   		metal = true;
    50     50   		color = lib.color(.3,.3,.3);
    51     51   	};
    52     52   	copper = {
    53         -		name = 'copper', sym = 'Cu', n = 29;
           53  +		name = 'copper', sym = 'Cu', n = 29;  density = 8.96;
    54     54   		metal = true;
    55     55   		color = lib.color(.8,.4,.1);
    56     56   	};
    57     57   	lithium = {
    58         -		name = 'lithium', sym = 'Li', n = 3;
           58  +		name = 'lithium', sym = 'Li', n = 3;  density = 0.534;
    59     59   		-- i think lithium is considered a metal but we don't mark it as
    60     60   		-- one here because making a 'lithium ingot' is insane (even possible?)
    61     61   		color = lib.color(1,0.8,.3);
    62     62   	};
    63     63   	titanium = {
    64         -		name = 'titanium', sym = 'Ti', n = 22;
           64  +		name = 'titanium', sym = 'Ti', n = 22;  density = 4.506;
    65     65   		metal = true;
    66     66   		color = lib.color(.7,.7,.7);
    67     67   	};
    68     68   	vanadium = {
    69         -		name = 'vanadium', sym = 'V', n = 23;
           69  +		name = 'vanadium', sym = 'V', n = 23; density = 6;
    70     70   		metal = true;
    71     71   		color = lib.color(.3,0.5,.3);
    72     72   	};
    73     73   	xenon = {
    74         -		name = 'xenon', sym = 'Xe', n = 54;
           74  +		name = 'xenon', sym = 'Xe', n = 54;  density = 0.005894;
    75     75   		gas = true;
    76     76   		color = lib.color(.5,.1,1);
    77     77   	};
    78     78   	argon = {
    79         -		name = 'argon', sym = 'Ar', n = 18;
           79  +		name = 'argon', sym = 'Ar', n = 18;  density = 0.001784;
    80     80   		gas = true;
    81     81   		color = lib.color(0,0.1,.9);
    82     82   	};
    83     83   	osmium = {
    84         -		name = 'osmium', sym = 'Os', n = 76;
           84  +		name = 'osmium', sym = 'Os', n = 76;  density = 22.59;
    85     85   		metal = true;
    86     86   		color = lib.color(.8,.1,1);
    87     87   	};
    88     88   	iridium = {
    89         -		name = 'iridium', sym = 'Ir', n = 77;
           89  +		name = 'iridium', sym = 'Ir', n = 77; density = 22.56;
    90     90   		metal = true;
    91     91   		color = lib.color(.8,0,.5);
    92     92   	};
    93     93   	technetium = {
    94         -		name = 'technetium', sym = 'Tc', n = 43;
           94  +		name = 'technetium', sym = 'Tc', n = 43;  density = 11;
    95     95   		desc = 'Prized by the higher Powers for subtle interactions that elude mere human scholars, technetium is of particular use in nuclear nanobatteries.';
    96     96   		metal = true;
    97     97   		color = lib.color(.2,0.2,1);
    98     98   	};
    99     99   	uranium = {
   100         -		name = 'uranium', sym = 'U', n = 92;
          100  +		name = 'uranium', sym = 'U', n = 92;  density = 19.1;
   101    101   		desc = 'A weak but relatively plentiful nuclear fuel.';
   102    102   		metal = true;
   103    103   		color = lib.color(.2,.7,0);
   104    104   	};
   105    105   	thorium = {
   106         -		name = 'thorium', sym = 'Th', n = 90;
          106  +		name = 'thorium', sym = 'Th', n = 90;  density = 11.7;
   107    107   		desc = 'A frighteningly powerful nuclear fuel.';
   108    108   		metal = true;
   109    109   		color = lib.color(.7,.3,.1);
   110    110   	};
   111    111   	silver = {
   112         -		name = 'silver', sym = 'Ag', n = 47;
          112  +		name = 'silver', sym = 'Ag', n = 47;  density = 10.49;
   113    113   		metal = true;
   114    114   		color = lib.color(.7,.7,.8);
   115    115   	};
   116    116   	gold = {
   117         -		name = 'gold', sym = 'Au', n = 79;
          117  +		name = 'gold', sym = 'Au', n = 79;  density = 19.30;
   118    118   		metal = true;
   119    119   		color = lib.color(1,.8,0);
   120    120   	};
   121    121   }

Modified mods/starlit-material/init.lua from [fffa847df5] to [b9ae2a76fd].

     2      2   local M = {
     3      3   	canisterSizes = lib.registry.mk 'starlit_material:canister-size';
     4      4   }
     5      5   M.canisterSizes.foreach('starlit_material:canister_link', {}, function(id, sz)
     6      6   	starlit.item.canister.link(minetest.get_current_modname() .. ':canister_' .. id, {
     7      7   		name = sz.name;
     8      8   		slots = sz.slots;
     9         -		vol = 0.1; -- too big for suit?
            9  +		vol = sz.vol; -- too big for suit?
    10     10   		desc = sz.desc;
    11     11   	})
    12     12   end)
    13     13   M.canisterSizes.meld {
    14         -	tiny = {name = 'Tiny Canister', slots = 1, vol = 0.05};
    15         -	small = {name = 'Small Canister', slots = 3, vol = 0.2};
    16         -	mid = {name = 'Canister', slots = 5, vol = 0.5};
    17         -	large = {name = 'Large Canister', slots = 10, vol = 1.0};
    18         -	storage = {name = 'Storage Canister', slots = 50, vol = 5.0};
           14  +	tiny    = {name = 'Tiny Canister',    vol = 1.0};
           15  +	small   = {name = 'Small Canister',   vol = 2.0};
           16  +	mid     = {name = 'Canister',         vol = 4.0};
           17  +	large   = {name = 'Large Canister',   vol = 8.0};
           18  +	storage = {name = 'Storage Canister', vol = 16.0};
    19     19   }
           20  +
           21  +starlit.mod.material = M
    20     22   
    21     23   starlit.include 'elements'
           24  +starlit.include 'liquids'
    22     25   

Modified mods/starlit-scenario/init.lua from [bf764e9a62] to [2d3d2d3760].

     1      1   local lib = starlit.mod.lib
     2      2   local scenario = starlit.world.scenario
            3  +local log = starlit.logger 'scenario'
     3      4   
     4      5   local function makeChip(label, schem, sw)
     5      6   	local E = starlit.mod.electronics
     6      7   	local files = {}
     7      8   	local sz = 0
     8      9   	for _, e in ipairs(schem) do
     9     10   		local p = E.sw.findSchematicFor(e[1])
    10     11   		if p then
    11     12   			local file = {
    12     13   				kind = 'sw', name = '', drm = e[2];
    13         -				body = {pgmId = p};
           14  +				body = {pgmId = p, conf = {}};
    14     15   			}
    15     16   			table.insert(files, file)
    16     17   			sz = sz + E.chip.fileSize(file)
    17     18   		end
    18     19   	end
    19     20   	for _, e in ipairs(sw) do
    20     21   		local file = {
    21     22   			kind = 'sw', name = '', drm = e[2];
    22         -			body = {pgmId = e[1]};
           23  +			body = {pgmId = e[1], conf = {}};
    23     24   		}
    24     25   		table.insert(files, file)
    25     26   		sz = sz + E.chip.fileSize(file)
    26     27   	end
    27     28   	local chip = ItemStack(assert(E.chip.findBest(function(c)
    28     29   		return c.flash >= sz, c.ram + c.clockRate
    29     30   	end)))
................................................................................
    60     61   
    61     62   local battery = function(name)
    62     63   	local s = ItemStack(name)
    63     64   	starlit.mod.electronics.battery.setChargeF(s, 1.0)
    64     65   	return s
    65     66   end
    66     67   
           68  +local function volume(kind,name,mass)
           69  +	local sorted = {}
           70  +	for k,v in pairs(starlit.item.canister.db) do
           71  +		table.insert(sorted, {id=k, can=v})
           72  +	end
           73  +	table.sort(sorted, function(a,b) return a.can.vol < b.can.vol end)
           74  +
           75  +	local liq = starlit.world.material[kind].db[name]
           76  +
           77  +	local can
           78  +	for i, v in ipairs(sorted) do
           79  +		if v.can.vol <= liq.density * mass then
           80  +			can = ItemStack(i)
           81  +			break
           82  +		end
           83  +	end
           84  +	if can == nil then log.fatal('failed to find canister size for gift %s', kind) end
           85  +
           86  +	local st = starlit.item.canister.meta(can)
           87  +	st.write('contents', {kind = kind, id = name, mass = mass})
           88  +
           89  +	return can
           90  +end
           91  +
    67     92   
    68     93   table.insert(scenario, {
    69     94   	id = 'starlit_scenario:imperialExpat';
    70     95   	name = 'Imperial Expat';
    71     96   	desc = "Hoping to escape a miserable life deep in the grinding gears of the capitalist machine for the bracing freedom of the frontier, you sought entry as a colonist to the new Commune world of Thousand Petal. Fate -- which is to say, terrorists -- intervened, and you wound up stranded on Farthest Shadow with little more than the nanosuit on your back, ship blown to tatters and your soul thoroughly mauled by the explosion of a twisted alien artifact -- which SOMEONE neglected to inform you your ride would be carrying.\nAt least you got some nifty psionic powers out of this whole clusterfuck. Hopefully they're safe to use.";
    72     97   
    73     98   	species = 'human';
................................................................................
    91    116   			-- came with this chip already plugged in. it's apparently true
    92    117   			-- what they say: the Commune is always prepared for everything.
    93    118   			-- E V E R Y T H I N G.
    94    119   		};
    95    120   		suitGuns = {};
    96    121   		suitAmmo = {};
    97    122   		suitCans = {
    98         -			ItemStack('starlit_material:canister_small');
          123  +-- 			ItemStack('starlit_material:canister_small');
          124  +			volume('liquid', 'water', 5);
    99    125   		};
   100    126   		carry = {
   101    127   			chipLibrary.compendium;
   102    128   			-- you bought this on a whim before you left the Empire, and
   103    129   			-- just happened to still have it on your person when everything
   104    130   			-- went straight to the Wild Gods' privy
   105    131   		};

Modified mods/starlit-secrets/init.lua from [0a228a51be] to [50a2d37741].

    38     38   				'the firstborn smonked hella weed';
    39     39   			};
    40     40   			-- body can also be a function(user,secret)
    41     41   		}
    42     42      }
    43     43   }
    44     44   
    45         -TODO would it be useful to impl horn clauses and a general fact database?
    46         -     is that level of flexibility meaningful? or are simply flags better
    47         -
    48     45   a secret can be a single piece of information predicated
    49     46   on a fact, in which case the secret and fact should share
    50     47   the same ID. the ID should be as non-indicative as possible
    51     48   to avoid spoilers for devs of unrelated code.
    52     49   
    53     50   a secret can also be manually unlocked e.g. by using an item
    54     51   
    55     52   ]==]--
    56     53   
    57     54   function sec.prereqCheck(user, pr)
    58     55   end

Modified mods/starlit/effect.lua from [eed7790b6a] to [9a580a99bf].

   307    307   				s.queue(spec.stop, function()
   308    308   					for _,snd in pairs(snds) do s.silence(snd) end
   309    309   				end)
   310    310   			end
   311    311   		end)
   312    312   	end
   313    313   	s.silence = function(sound)
          314  +		if not sound.handle then return end
   314    315   		if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle)
   315    316   		else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end
   316    317   	end
   317    318   	local startqueued, termqueued = false, false
   318    319   	local myid = #starlit.effect.active+1
   319    320   	s.cancel = function()
   320    321   		s.abort()

Modified mods/starlit/element.lua from [db8d1e2ad1] to [e6ca28c621].

    12     12   			-- for the object in question; e.g a normal chunk will be
    13     13   			-- 100 $element, an ingot will be 1000 $element
    14     14   		})
    15     15   	elseif m.gas then
    16     16   		M.gas.link(id, {
    17     17   			name = m.name;
    18     18   			composition = starlit.type.fab{element = {[id] = 1}};
           19  +			density = m.density;
    19     20   		})
    20     21   	elseif m.liquid then
    21     22   		M.liquid.link(id, {
    22     23   			name = m.name;
    23     24   			composition = starlit.type.fab{element = {[id] = 1}};
           25  +			density = m.density;
    24     26   		})
    25     27   	end
    26     28   end)
    27     29   
    28     30   local F = string.format
    29     31   
    30     32   local function mkEltIndicator(composition)
................................................................................
    63     65   			props = {
    64     66   				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
    65     67   			}
    66     68   		}
    67     69   	end
    68     70   	local comp = {[id] = 1}
    69     71   	local iblit = mkEltIndicator(comp)
           72  +	local function img(s)
           73  +		return iblit(s:colorize(m.color):render())
           74  +	end
           75  +
    70     76   	m.form = m.form or {}
    71         -	m.form.element = eltID
    72     77   
    73         -	local powder = F('starlit-element-%s-powder.png', id);
           78  +
           79  +	if not (m.gas or m.liquid) then
           80  +		local brickID = eltID .. '_brick'
           81  +		local brickName = F('%s Brick', lib.str.capitalize(m.name))
           82  +		m.form.brick = brickID
           83  +		minetest.register_craftitem(brickID, {
           84  +			short_description = brickName;
           85  +			description = tt(brickName, F('A small brick of %s, ready to be worked by a matter compiler', m.name), 1);
           86  +			inventory_image = img(lib.image 'starlit-item-brick.png');
           87  +			wield_image = lib.image 'starlit-item-brick.png':colorize(m.color):render();
           88  +			stack_max = 500;
           89  +			groups = {element=1, brick=1};
           90  +			_starlit = {
           91  +				mass = 1;
           92  +				material = {
           93  +					kind = 'element';
           94  +					element = id;
           95  +				};
           96  +				fab = starlit.type.fab {
           97  +					element = comp;
           98  +				};
           99  +			};
          100  +		});
          101  +	end
          102  +
          103  +	--[[
          104  +	local chunk = F('starlit-element-%s-powder.png', id);
    74    105   	minetest.register_craftitem(eltID, {
    75    106   		short_description = eltName;
    76         -		description = tt(eltName, F('Elemental %s kept in suspension by a nanide storage system, ready to be worked by a cold matter compiler', m.name), 1);
    77         -		inventory_image = iblit(powder);
          107  +		description = tt(eltName, F('A 1g chunk of elemental %s, ready to be worked by a cold matter compiler', m.name), 1);
          108  +		inventory_image = iblit(chunk);
    78    109   		wield_image = powder;
    79    110   		stack_max = 1000; -- 1kg
    80         -		groups = {element = 1, powder = 1, specialInventory = 1};
          111  +		groups = {element = 1, chunk = 1};
    81    112   		_starlit = {
    82    113   			mass = 1;
    83    114   			material = {
    84    115   				kind = 'element';
    85    116   				element = id;
    86    117   			};
    87    118   			fab = starlit.type.fab {
    88    119   				element = comp;
    89    120   			};
    90    121   		};
    91    122   	});
          123  +	]]
    92    124   end)
    93    125   
    94    126   
    95    127   M.metal.foreach('starlit:gen-forms', {}, function(id, m)
    96    128   	local baseID = F('%s:metal_%s_', minetest.get_current_modname(), id)
    97         -	local brickID, ingotID = baseID .. 'brick', baseID .. 'ingot'
    98         -	local brickName, ingotName =
    99         -		F('%s Brick', lib.str.capitalize(m.name)),
   100         -		F('%s Ingot', lib.str.capitalize(m.name))
          129  +	local ingotID = baseID .. 'ingot'
          130  +	local ingotName = F('%s Ingot', lib.str.capitalize(m.name))
   101    131   	m.form = m.form or {}
   102         -	m.form.brick = brickID
   103    132   	m.form.ingot = ingotID
   104    133   	local tt = function(t, d, g)
   105    134   		return starlit.ui.tooltip {
   106    135   			title = t, desc = d;
   107    136   			color = lib.color(0.1,0.1,0.1);
   108    137   			props = {
   109    138   				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
................................................................................
   119    148   		return t
   120    149   	end
   121    150   	local iblit = mkEltIndicator(mcomp)
   122    151   	local function img(s)
   123    152   		return iblit(s:colorize(m.color):render())
   124    153   	end
   125    154   
   126         -	minetest.register_craftitem(brickID, {
   127         -		short_description = brickName;
   128         -		description = tt(brickName, F('A solid brick of %s, ready to be worked by a matter compiler', m.name), 100);
   129         -		inventory_image = img(lib.image 'starlit-item-brick.png');
   130         -		wield_image = lib.image 'starlit-item-brick.png':colorize(m.color):render();
   131         -		stack_max = 10;
   132         -		groups = {metal = 1, ingot = 1};
   133         -		_starlit = {
   134         -			mass = 100;
   135         -			material = {
   136         -				kind = 'metal';
   137         -				metal = id;
   138         -			};
   139         -			fab = starlit.type.fab {
   140         -				flag = {smelt= true};
   141         -				element = comp(1e2);
   142         -			};
   143         -		};
   144         -	});
   145         -
   146    155   	minetest.register_craftitem(ingotID, {
   147    156   		short_description = ingotName;
   148    157   		description = tt(ingotName, F('A solid ingot of %s, ready to be worked by a large matter compiler', m.name), 1e3);
   149    158   		inventory_image = img(lib.image('starlit-item-ingot.png'));
   150    159   		wield_image = lib.image 'starlit-item-ingot.png':colorize(m.color):render();
   151    160   		groups = {metal = 1, ingot = 1};
   152    161   		stack_max = 5;
................................................................................
   161    170   				element = comp(1e3);
   162    171   			};
   163    172   		};
   164    173   	});
   165    174   
   166    175   
   167    176   end)
          177  +
          178  +local function canisterMeta(stack)
          179  +	return lib.marshal.metaStore {
          180  +		contents = {key = 'starlit:canister_contents', type = starlit.store.volume};
          181  +	} (stack)
          182  +end
   168    183   
   169    184   local function canisterDesc(stack, def)
   170    185   	def = def or stack:get_definition()._starlit.canister
   171    186   	local props = {
   172         -		{title = 'Charge Slots', affinity = 'info', desc = tostring(def.slots)};
          187  +		{title = 'Volume', affinity = 'info', desc = lib.math.si('L', def.vol,nil,nil,2)};
   173    188   	};
   174    189   	if stack then
          190  +	--[[
   175    191   		local inv = starlit.item.container(stack)
   176    192   		for i,e in ipairs(inv:list 'elem') do
   177    193   			local comp = e:get_definition()._starlit.fab
   178    194   			table.insert(props, {
   179    195   				title = comp:formula();
   180    196   				desc = lib.math.si('g', e:get_count());
   181    197   				affinity = 'good';
   182    198   			})
          199  +		end ]]
          200  +		local itemMeta = canisterMeta(stack)
          201  +		local e = itemMeta.read 'contents'
          202  +		local mass = lib.math.si('g', e.mass, nil, nil, 2)
          203  +		local def, meas
          204  +		if e.kind == 'liquid' then
          205  +			def = M.liquid.db[e.id]
          206  +			local vol =  lib.math.si('L', e.mass * def.composition.density, nil, nil, 2)
          207  +			meas = string.format("%s %s (%s %s)", vol, def.name, e.mass, def.composition:formula())
          208  +		elseif e.kind == 'gas' then
          209  +			def = M.gas.db[e.id]
          210  +			meas = string.format("%s %s (%s)", mass, def.name, def.composition:formula())
   183    211   		end
   184         -		-- TODO list masses
          212  +		local comp = def.composition
          213  +		table.insert(props, {
          214  +			title = meas;
          215  +			desc = def.desc;
          216  +			affinity = 'info';
          217  +		})
   185    218   	end
   186    219   	return starlit.ui.tooltip {
   187         -		title = def.name, desc = def.desc or 'A canister that can store a charge of elemental powder, gas, or liquid';
          220  +		title = def.name, desc = def.desc or 'A canister that can store a charge of gas or liquid';
   188    221   		color = lib.color(0.2,0.1,0.1);
   189    222   		props = props;
   190    223   	};	
   191    224   end
   192    225   
   193    226   starlit.item.canister = lib.registry.mk 'starlit:canister';
   194    227   starlit.item.canister.foreach('starlit:item-gen', {}, function(id, c)
................................................................................
   212    245   						sz = c.slots;
   213    246   					};
   214    247   				};
   215    248   			};
   216    249   		};
   217    250   	})
   218    251   end)
          252  +
          253  +starlit.item.canister.meta = canisterMeta

Modified mods/starlit/init.lua from [1f09b99871] to [6f8dfbcd79].

    45     45   	};
    46     46   	liveUI = {
    47     47   		-- cached subset of activeUI containing those UIs needing live updates
    48     48   	};
    49     49   
    50     50   	interface = lib.registry.mk 'starlit:interface';
    51     51   	item = {
           52  +		food = lib.registry.mk 'starlit:food';
    52     53   	};
    53     54   
    54     55   	region = {
    55     56   		radiator = {
    56     57   			store = AreaStore();
    57     58   			emitters = {}
    58     59   		};
................................................................................
    91     92   		fact = lib.registry.mk 'starlit:fact';
    92     93   		time = {
    93     94   			calendar = {
    94     95   				empire  = {
    95     96   					name = 'Imperial Regnal Calendar';
    96     97   					year = function(t, long)
    97     98   						local reigns = {
    98         -							-- if anyone actually makes it to his Honor & Glory Unfailing Persigan I i will be
           99  +							-- if anyone actually makes it to his Honor & Glory Unfailing Persivan I i will be
    99    100   							-- exceptionally flattered
   100    101   							{4, 'Emperor', 'Atavarka', 'the Bold'}; -- died at war
   101    102   							{9, 'Emperor', 'Vatikserka', 'the Unconquered'}; -- died at war
   102    103   							{22, 'Emperor', 'Rusifend', 'the Wise'}; -- poisoned at diplomacy
   103    104   							{61, 'Empress', 'Tafseshendi', 'the Great'}; -- died of an 'insurrection of the innards' after a celebrated reign
   104    105   							{291, 'Emperor', 'Treptebaska', 'the Unwise'}; -- murdered by his wife in short order
   105    106   							{292, 'Empress', 'Vilintalti', 'the Impious'}; -- removed by the praetorian elite
   106    107   							{298, 'Emperor', 'Radavan', 'the Reckless'}; -- died at war
   107    108   							{316, 'Emperor', 'Suldibrand', 'the Forsaken of Men'}; -- fucked around. found out.
   108         -							{320, 'Emperor', 'Persigan', 'the Deathless'};
          109  +							{350, 'Emperor', 'Persivan', 'the Deathless'};
   109    110   						}
   110    111   						local year, r = math.floor(t / 414)
   111    112   						for i=1, #reigns do if reigns[i+1][1] < year then r = reigns[i+1] end end
   112    113   						local reignBegin, title, name, epithet = lib.tbl.unpack(r)
   113    114   						local ry = 1 + (year - reignBegin)
   114    115   						return long and string.format('Year %s of the Reign of HH&GU %s %s %s',
   115    116   							ry, title, name, epithet) or string.format('Y. %s %s', name, ry)
................................................................................
   248    249   		end)
   249    250   	end
   250    251   	start()
   251    252   end
   252    253   
   253    254   starlit.include 'stats'
   254    255   starlit.include 'world'
          256  +starlit.include 'food'
   255    257   starlit.include 'fab'
   256    258   starlit.include 'tiers'
   257    259   starlit.include 'species'
   258    260   
   259    261   starlit.include 'store'
   260    262   
   261    263   starlit.include 'ui'
................................................................................
   356    358   		user.action.bits = bit.bor(user.action.bits, 0x100)
   357    359   		--return user:trigger('secondary', {state = 'prog', delta = 0})
   358    360   	elseif pointChanged(oldTgt, point) then
   359    361   		user:trigger('retarget', {oldTgt = oldTgt})
   360    362   	end
   361    363   end
   362    364   -- sigh
          365  +--[[
   363    366   core.noneitemdef_default.on_place = function(...)
   364    367   	if not triggerPower(...) then
   365    368   		minetest.item_place(...)
   366    369   	end
   367    370   end
   368    371   core.noneitemdef_default.on_use           = function(...) triggerPower(...) end
   369    372   core.noneitemdef_default.on_secondary_use = function(...) triggerPower(...) end
          373  +]]
          374  +print(dump(core.noneitemdef_default))
          375  +minetest.register_item(":", {
          376  +	type = "none",
          377  +	wield_image = "wieldhand.png",
          378  +	wield_scale = {x=1,y=1,z=2.5},
          379  +	on_secondary_use = function(...) triggerPower(...) end;
          380  +-- 	on_use = function(...) print'base' end;
          381  +	after_use = function(...) triggerPower(...) end;
          382  +})
          383  +minetest.register_item("starlit:_hand_dig", {
          384  +	type = "none",
          385  +	wield_image = "wieldhand.png",
          386  +	wield_scale = {x=1,y=1,z=2.5},
          387  +	tool_capabilities = {
          388  +		groupcaps = {
          389  +			plant = {maxlevel=1, times = {.50,.5,.5}};
          390  +			dirt = {maxlevel=1, times = {2.5,1,1}};
          391  +		};
          392  +	}
          393  +})
   370    394   
   371    395   minetest.register_on_player_inventory_action(function(luser, act, inv, p)
   372    396   	local name = luser:get_player_name()
   373    397   	local user = starlit.activeUsers[name]
   374    398   	-- allow UIs to update on UI changes
   375    399   	local state = starlit.activeUI[name]
   376    400   	if state then

Modified mods/starlit/interfaces.lua from [1cb802f20f] to [f0b5b90e43].

   298    298   						}) end
   299    299   					end
   300    300   				end
   301    301   				local menu = { kind = 'vert', mode = 'sw', padding = 0.5 }
   302    302   				if swm then table.insert(menu, abilityMenu(swm)) end
   303    303   
   304    304   				local inv = user.entity:get_inventory()
          305  +				--[[
   305    306   				local cans = inv:get_list 'starlit_suit_canisters'
   306    307   				if cans and next(cans) then for i, st in ipairs(cans) do
   307    308   					local id = string.format('starlit_canister_%u_elem', i)
   308    309   					local esz = inv:get_size(id)
   309    310   					if esz > 0 then
   310    311   						local eltW, eltH = listWrap(esz, 5)
   311    312   						table.insert(menu, {kind = 'hztl',
   312    313   							{kind = 'img', desc='Elements', img = 'starlit-ui-icon-element.png', w=1,h=1};
   313    314   							{kind = 'list', target = 'current_player', inv = id,
   314    315   								listContent = 'element', w = eltW, h = eltH, spacing = 0.1};
   315    316   						})
   316    317   					end
   317    318   				end end
          319  +				]]
   318    320   
   319    321   				if #menu == 0 then
   320    322   					table.insert(menu, {
   321    323   						kind = 'img';
   322    324   						img = 'starlit-ui-alert.png';
   323    325   						w=2, h=2;
   324    326   					})
................................................................................
   343    345   				local tb = {
   344    346   					kind = 'vert', mode = 'sw';
   345    347   					padding = 0.5, 
   346    348   					{kind = 'hztl', padding = 0.25;
   347    349   						{kind = 'label', text = 'Name', w = 2, h = barh};
   348    350   						{kind = 'label', text = user.persona.name, w = 4, h = barh}};
   349    351   				}
   350         -				local statBars = {'hunger', 'thirst', 'fatigue', 'morale'}
          352  +				local statBars = {'hunger', 'thirst', 'fatigue', 'morale', 'irradiation', 'illness'}
   351    353   				for idx, id in ipairs(statBars) do
   352    354   					local s = starlit.world.stats[id]
   353    355   					local amt, sv = user:effectiveStat(id)
   354    356   					local min, max = starlit.world.species.statRange(user.persona.species, user.persona.speciesVariant, id)
   355    357   					local st = string.format('%s / %s', s.desc(amt, true), s.desc(max))
   356    358   					table.insert(tb, {kind = 'hztl', padding = 0.25;
   357    359   						{kind = 'label', w=2, h=barh, text = s.name};

Modified mods/starlit/species.lua from [3944fdb227] to [642ae8f101].

    14     14   
    15     15   -- constants
    16     16   local animationFrameRate = 60
    17     17   
    18     18   local species = {
    19     19   	human = {
    20     20   		name = 'Human';
    21         -		desc = 'The weeds of the galactic flowerbed. Humans are one of the Lesser Races, excluded from the ranks of the Greatest Races by souls that lack, in normal circumstances, external psionic channels. Their mastery of the universe cut unexpectedly short, forever locked out of FTL travel, short-lived without augments, and alternately pitied or scorned by the lowest of the low, humans flourish nonetheless due to a capacity for adaptation unmatched among the Thinking Few, terrifyingly rapid reproductive cycles -- and a keen facility for bribery. While the lack of human psions remains a sensitive topic, humans (unlike the bitter and emotional Kruthandi) are practical enough to hire the talent they cannot possess, and have even built a small number of symbiotic civilizations with the more indulging of the Powers. In a galaxy where nearly all sophont life is specialized to a fault, humans have found the unique niche of occupying no particular niche.';
           21  +		desc = 'The weeds of the galactic flowerbed. Humans are one of the Lesser Races, excluded from the ranks of the Starlit by souls that lack, in normal circumstances, external psionic channels. Their mastery of the universe cut unexpectedly short, forever locked out of FTL travel, short-lived without augments, and alternately pitied or scorned by the lowest of the low, humans flourish nonetheless due to a capacity for adaptation unmatched among the Thinking Few, terrifyingly rapid reproductive cycles -- and a keen facility for bribery. While the lack of human psions remains a sensitive topic, humans (unlike the bitter and emotional Kruthandi) are practical enough to hire the talent they cannot possess, and have even built a small number of symbiotic civilizations with the more indulging of the Powers. In a galaxy where nearly all sophont life is specialized to a fault, humans have found the unique niche of occupying no particular niche.';
    22     22   		scale = 1.0;
    23     23   		params = {
    24     24   			{'eyeColor',  'Eye Color',  'tone', {hue=327, sat=0, lum=0}};
    25     25   			{'hairColor', 'Hair Color', 'tone', {hue=100, sat=0, lum=0}};
    26     26   			{'skinTone',  'Skin Tone',  'tone', {hue=  0, sat=0, lum=0}};
    27     27   		};
    28     28   		tempRange = {
................................................................................
    55     55   					morale = 0.8; -- you are not She-Bear Grylls
    56     56   				};
    57     57   				traits = {
    58     58   					health = 400;
    59     59   					lungCapacity = .6;
    60     60   					irradiation = 0.8; -- you are smaller, so it takes less rads to kill ya
    61     61   					sturdiness = 0; -- women are more fragile and thus susceptible to blunt force trauma
    62         -					metabolism = 1800; --Cal
           62  +					metabolism = 1800e3 / 24 / 60 / 60; --kCal/s
    63     63   					painTolerance = 0.4;
           64  +					dehydration = 3; -- mL/s
    64     65   				};
    65     66   			};
    66     67   			male = {
    67     68   				name = 'Human Male';
    68     69   				eyeHeight = 1.6;
    69     70   				stats = {
    70     71   					psiRegen = 1.0;
    71     72   					psiPower = 1.0;
    72     73   					psi = 1.0;
    73     74   					hunger = 1.0;
           75  +					thirst = 1.0;
    74     76   					staminaRegen = .7; -- men are strong but have inferior endurance
    75     77   				};
    76     78   				traits = {
    77     79   					health = 500;
    78     80   					painTolerance = 1.0;
    79     81   					lungCapacity = 1.0;
    80     82   					sturdiness = 0.3;
    81         -					metabolism = 2200; --Cal
           83  +					metabolism = 2200e3 / 24 / 60 / 60; --Cal/s
           84  +					dehydration = 5; -- mL/s
    82     85   				};
    83     86   			};
    84     87   		};
    85     88   		traits = {};
    86     89   	};
    87     90   }
           91  +
    88     92   
    89     93   starlit.world.species = {
    90     94   	index = species;
    91     95   	paramTypes = paramTypes;
    92     96   }
           97  +
           98  +starlit.world.species.pheno = lib.class {
           99  +	name = 'starlit:species.pheno';
          100  +	construct = function(pSp, pVar)
          101  +		local sp, var = starlit.world.species.lookup(pSp, pVar)
          102  +		return {
          103  +			species = sp, variant = var;
          104  +			pSpecies = pSp, pVariant = pVar;
          105  +		};
          106  +	end;
          107  +	__index = {
          108  +		trait = function(me, st, dflt)
          109  +			local v = me.variant.traits[st] or me.species.traits[st]
          110  +			return v or dflt
          111  +		end;
          112  +	};
          113  +}
    93    114   
    94    115   function starlit.world.species.mkDefaultParamsTable(pSpecies, pVariant)
    95    116   	local sp = species[pSpecies]
    96    117   	local var = sp.variants[pVariant]
    97    118   	local vpd = var.defaults or {}
    98    119   	local tbl = {}
    99    120   	for _, p in pairs(sp.params) do
................................................................................
   108    129   	return {
   109    130   		species = pSpecies;
   110    131   		speciesVariant = pVariant;
   111    132   		bodyParams = starlit.world.species.paramsFromTable(pSpecies,
   112    133   			starlit.world.species.mkDefaultParamsTable(pSpecies, pVariant)
   113    134   		);
   114    135   		statDeltas = {};
          136  +		facts = {};
   115    137   	}
   116    138   end
   117    139   
   118    140   local function spLookup(pSpecies, pVariant)
   119    141   	local sp = species[pSpecies]
   120    142   	local var = sp.variants[pVariant or next(sp.variants)]
   121    143   	return sp, var
................................................................................
   160    182   		local delta = max - min
   161    183   		return min + delta*p
   162    184   	end
   163    185   	local ps = starlit.world.species.mkPersonaFor(pSpecies,pVariant)
   164    186   	local startingHP = pct('health', 1.0)
   165    187   	if circumstances.injured    then startingHP = pct('health', circumstances.injured) end
   166    188   	if circumstances.psiCharged then ps.statDeltas.psi = pct('psi', circumstances.psiCharged) end
          189  +	for k,v in pairs(starlit.world.stats) do ps.statDeltas[k] = 0 end
   167    190   	ps.statDeltas.warmth = 20 -- don't instantly start dying of frostbite
   168    191   
   169    192   	entity:set_properties{hp_max = var.traits.health or sp.traits.health}
   170    193   	entity:set_hp(startingHP, 'initial hp')
   171    194   	return ps
   172    195   end
   173    196   

Modified mods/starlit/stats.lua from [c766e87490] to [523263fc9a].

     1      1   local lib = starlit.mod.lib
     2      2   
     3      3   local function U(unit, prec, fixed)
            4  +	local trunc = 2
     4      5   	if fixed then
     5      6   		return function(amt, excludeUnit)
     6         -			if excludeUnit then return tostring(amt/prec) end
     7         -			return string.format("%s %s", amt/prec, unit)
            7  +			local ta = lib.math.trim(amt/prec, trunc)
            8  +			if excludeUnit then return tostring(ta) end
            9  +			return string.format("%s %s", ta, unit)
     8     10   		end
     9     11   	else
    10     12   		return function(amt, excludeUnit)
    11         -			if excludeUnit then return tostring(amt/prec) end
    12         -			return lib.math.si(unit, amt/prec)
           13  +			local ta = lib.math.trim(amt/prec, trunc)
           14  +			if excludeUnit then return tostring(ta) end
           15  +			return lib.math.si(unit, amt/prec, nil, nil, trunc)
    13     16   		end
    14     17   	end
    15     18   end
    16     19   
    17     20   local function C(h, s, l)
    18     21   	return lib.color {hue = h, sat = s or 1, lum = l or .7}
    19     22   end
................................................................................
    22     25   	-- numina is measured in daψ
    23     26   	warmth     = {min = -1000, max = 1000, base = 0, desc = U('°C', 10, true), color = C(5), name = 'Warmth'};
    24     27   	-- warmth in measured in °C×10
    25     28   	fatigue    = {min = 0, max = 76 * 60, base = 0, desc = U('hr', 60, true), color = C(288,.3,.5), name = 'Fatigue'};
    26     29   	-- fatigue is measured in minutes one needs to sleep to cure it
    27     30   	stamina    = {min = 0, max = 20 * 100, base = true, desc = U('m', 100), color = C(88), name = 'Stamina'};
    28     31   	-- stamina is measured in how many 10th-nodes (== cm) one can sprint
    29         -	hunger     = {min = 0, max = 20000, base = 0, desc = U('Cal', 1), color = C(43,.5,.4), name = 'Hunger'};
    30         -	-- hunger is measured in calories one must consume to cure it
    31         -	thirst     = {min = 0, max = 1600, base = 0, desc = U('l', 100), color = C(217, .25,.4), name = 'Thirst'};
    32         -	-- thirst is measured in centiliters of H²O required to cure it
           32  +	hunger     = {min = 0, max = 2000e3, base = 0, desc = U('kCal', 1000, true), color = C(43,.5,.4), name = 'Hunger'};
           33  +	-- hunger is measured in calories one must consume to cure it. at a 2kCal deficit, you start dying
           34  +	thirst     = {min = 0, max = 4e3, base = 0, desc = U('L', 1e3), color = C(217, .25,.4), name = 'Thirst'};
           35  +	-- thirst is measured in mL of H²O required to cure it
    33     36   	morale     = {min = 0, max = 24 * 60 * 10, base = true, desc = U('hr', 60, true), color = C(0,0,.8), name = 'Morale'};
    34     37   	-- morale is measured in minutes. e.g. at base rate morale degrades by
    35     38   	-- 60 points every hour. morale can last up to 10 days
    36     39   	irradiation = {min = 0, max = 20000, base = 0, desc = U('Gy', 1000), color = C(141,1,.5), name = 'Irradiation'};
    37     40   	-- irrad is measured is milligreys
    38     41   	-- 1Gy counters natural healing
    39     42   	-- ~3Gy counters basic nanomedicine

Modified mods/starlit/store.lua from [73c6f814ce] to [9cc49bcaa7].

     8      8   local T,G = lib.marshal.t, lib.marshal.g
     9      9   starlit.store = {} -- the serialization equivalent of .type
    10     10   
    11     11   -------------
    12     12   -- persona --
    13     13   ------------- -----------------------------------------------
    14     14   -- a Persona is a structure that defines the nature of     --
    15         --- an (N)PC and how it interacts with the Starsoul-managed --
           15  +-- an (N)PC and how it interacts with the Starlit-managed  --
    16     16   -- portion of the game world -- things like name, species, --
    17     17   -- stat values, physical characteristics, and so forth     --
    18     18   
    19     19   local statStructFields = {}
    20     20   for k,v in pairs(starlit.world.stats) do
    21     21   	statStructFields[k] = v.srzType or (
    22     22   		(v.base == true or v.base > 0) and T.s16 or T.u16
................................................................................
    47     47   starlit.store.suitMeta = lib.marshal.metaStore {
    48     48   	batteries = {key = 'starlit:suit_slots_bat', type = T.inventoryList};
    49     49   	chips = {key = 'starlit:suit_slots_chips', type = T.inventoryList};
    50     50   	elements = {key = 'starlit:suit_slots_elem', type = T.inventoryList};
    51     51   	guns = {key = 'starlit:suit_slots_gun', type = T.inventoryList};
    52     52   	ammo = {key = 'starlit:suit_slots_ammo', type = T.inventoryList};
    53     53   }
           54  +
           55  +starlit.store.volume = G.struct {
           56  +	kind = T.str;
           57  +	id = T.str;
           58  +	mass = T.decimal;
           59  +}

Modified mods/starlit/suit.lua from [7112e6c94b] to [7f11ba51a8].

    61     61   			return w
    62     62   		end;
    63     63   		onReconfigure = function(self, inv)
    64     64   			-- apply any changes to item metadata and export any subinventories
    65     65   			-- to the provided invref, as they may have changed
    66     66   			local sc = starlit.item.container(self.item, inv, {pfx = 'starlit_suit'})
    67     67   			sc:push()
    68         -			self:pullCanisters(inv)
           68  +-- 			self:pullCanisters(inv)
    69     69   		end;
    70     70   		onItemMove = function(self, user, list, act, what)
    71     71   			-- called when the suit inventory is changed
    72     72   			if act == 'put' then
    73     73   				if list == 'starlit_suit_bat' then
    74     74   					user:suitSound('starlit-suit-battery-in')
    75     75   				elseif list == 'starlit_suit_chips' then
................................................................................
    86     86   					user:suitSound('starlit-insert-snap')
    87     87   				end
    88     88   			end
    89     89   		end;
    90     90   		def = function(self)
    91     91   			return self.item:get_definition()._starlit.suit
    92     92   		end;
           93  +		--[[
    93     94   		pullCanisters = function(self, inv)
    94     95   			starlit.item.container.dropPrefix(inv, 'starlit_canister')
    95     96   			self:forCanisters(inv, function(sc) sc:pull() end)
    96     97   		end;
    97     98   		pushCanisters = function(self, inv, st, i)
    98     99   			self:forCanisters(inv, function(sc)
    99    100   				sc:push()
................................................................................
   108    109   					local sc = starlit.item.container(st, inv, {pfx = pfx})
   109    110   					if fn(sc, st, i, pfx) then
   110    111   						inv:set_stack('starlit_suit_canisters', i, st)
   111    112   					end
   112    113   				end
   113    114   			end end
   114    115   		end;
          116  +		]]
   115    117   		establishInventories = function(self, obj)
   116    118   			local inv = obj:get_inventory()
   117    119   			local ct = suitContainer(self.item, inv)
   118    120   			ct:pull()
          121  +			--[[
   119    122   			self:pullCanisters(inv)
   120    123   
   121         -			--[[
   122    124   			local def = self:def()
   123    125   			local sst = suitStore(self.item)
   124    126   			local function readList(listName, prop)
   125    127   				inv:set_size(listName, def.slots[prop])
   126    128   				if def.slots[prop] > 0 then
   127    129   					local lst = sst.read(prop)
   128    130   					inv:set_list(listName, lst)
................................................................................
   213    215   		on_use = function(st, luser, pointed)
   214    216   			local user = starlit.activeUsers[luser:get_player_name()]
   215    217   			if not user then return end
   216    218   			-- have mercy on users who've lost their suits and wound
   217    219   			-- up naked and dying of exposure
   218    220   			if user:naked() then
   219    221   				local ss = st:take_item(1)
   220         -				user:setSuit(starlit.type.suit(ss))
          222  +				user:changeSuit(starlit.type.suit(ss))
   221    223   				user:suitSound('starlit-suit-don')
   222    224   				return st
   223    225   			end
   224    226   		end;
   225    227   		inventory_image = icon:render();
   226    228   		_starlit = {
   227    229   			container = {

Modified mods/starlit/terrain.lua from [5a8b3b76d0] to [b5b4e3205a].

    78     78   			def.img .. '.png';
    79     79   			'default_dirt.png';
    80     80   			{
    81     81   				name = 'default_dirt.png^' .. def.img ..'_side.png';
    82     82   				tileable_vertical = false;
    83     83   			};
    84     84   		};
    85         -		groups = {grass = 1, sub_walk = 1};
           85  +		groups = {grass = 1, dirt = 1, sub_walk = 1};
    86     86   		drop = '';
    87     87   		sounds = grassSounds;
    88     88   		_starlit = grassfst(2);
    89     89   	})
    90     90   	for i=2,0,-1 do
    91     91   		local opacity = tostring((i/2.0) * 255)
    92     92   

Modified mods/starlit/user.lua from [aee7410825] to [43bf528964].

    63     63   	end;
    64     64   	__index = {
    65     65   		pullPersona = function(self)
    66     66   			-- if later records are added in public updates, extend this function to merge them
    67     67   			-- into one object
    68     68   			local s = userStore(self.entity)
    69     69   			self.persona = s.read 'persona'
           70  +			self.pheno = starlit.world.species.pheno(self.persona.species, self.persona.speciesVariant)
    70     71   		end;
    71     72   		pushPersona = function(self)
    72     73   			local s = userStore(self.entity)
    73     74   			s.write('persona', self.persona)
    74     75   		end;
    75     76   		uiColor = function(self) return lib.color {hue=238,sat=.5,lum=.5} end;
    76     77   		statDelta = function(self, stat, d, cause, abs)
................................................................................
    96     97   			end
    97     98   			self:updateHUD()
    98     99   			-- TODO trigger relevant animations?
    99    100   		end;
   100    101   		lookupSpecies = function(self)
   101    102   			return starlit.world.species.lookup(self.persona.species, self.persona.speciesVariant)
   102    103   		end;
   103         -		phenoTrait = function(self, trait)
   104         -			local s,v = self:lookupSpecies()
   105         -			return v.traits[trait] or s.traits[trait] or 0
          104  +		phenoTrait = function(self, trait, dflt)
          105  +-- 			local s,v = self:lookupSpecies()
          106  +-- 			return v.traits[trait] or s.traits[trait] or 0
          107  +			return self.pheno:trait(trait, dflt)
   106    108   		end;
   107    109   		statRange = function(self, stat) --> min, max, base
   108    110   			return starlit.world.species.statRange(
   109    111   				self.persona.species, self.persona.speciesVariant, stat)
   110    112   		end;
   111    113   		effectiveStat = function(self, stat)
   112    114   			local val
................................................................................
   345    347   					local hot = self:effectiveStat 'irradiation'
   346    348   					local color = self:uiColor():lerp(lib.color(0.3, 1, 0), math.min(1, hot/5))
   347    349   					local txt = string.format("%sGy", math.floor(hot))
   348    350   					return (hot/5), txt, color
   349    351   				end;
   350    352   			}
   351    353   			self.hud.elt.crosshair = self:attachImage {
   352         -				name = 'crosshair ';
          354  +				name = 'crosshair';
   353    355   				tex = '';
   354    356   				pos = {x=.5, y=.5};
   355    357   				scale = {x=1,y=1};
   356    358   				ofs = {x=0, y=0};
   357    359   				align = {x=0, y=0};
   358    360   				update = function(user, set)
   359    361   					local imgs = {
................................................................................
   375    377   				align = {x=0, y=-1};
   376    378   				z = -1;
   377    379   				update = function(user, set)
   378    380   					set('text', hudAdjustBacklight(hudCenterBG):render())
   379    381   				end;
   380    382   			};
   381    383   		end;
          384  +		-- horrible horrible HACK
          385  +		setModeHand = function(self)
          386  +			local inv = self.entity:get_inventory()
          387  +			local hnd
          388  +			if self.actMode == 'off'
          389  +				then hnd = ItemStack('starlit:_hand_dig')
          390  +				else hnd = ItemStack()
          391  +			end
          392  +			inv:set_stack('hand', 1, hnd)
          393  +		end;
   382    394   		onModeChange = function(self, oldMode, silent)
   383    395   			self.hud.elt.crosshair.update()
   384    396   			if not silent then
   385    397   				local sfxt = {
   386    398   					off = 'starlit-mode-off';
   387    399   					nano = 'starlit-mode-nano';
   388    400   					psi = 'starlit-mode-psi';
   389    401   					weapon = 'starlit-mode-weapon';
   390    402   				}
   391    403   				local sfx = self.actMode and sfxt[self.actMode] or sfxt.off
   392    404   				self:suitSound(sfx)
          405  +				self:setModeHand()
   393    406   			end
   394    407   		end;
   395    408   		actModeSet = function(self, mode, silent)
   396    409   			if not mode then mode = 'off' end
   397    410   			local oldMode = self.actMode
   398    411   			self.actMode = mode
   399    412   			self:onModeChange(oldMode, silent)
................................................................................
   415    428   			return minetest.get_player_information(self.name)
   416    429   		end;
   417    430   		onSignup = function(self)
   418    431   			local meta = self.entity:get_meta()
   419    432   			local inv = self.entity:get_inventory()
   420    433   			-- the sizes indicated here are MAXIMA. limitations on e.g. the number of elements that may be carried are defined by your suit and enforced through callbacks and UI generation code, not inventory size
   421    434   			inv:set_size('main', 6) -- carried items and tools. main hotbar.
          435  +			inv:set_size('hand', 1) -- horrible hack to allow both tools and intrinsics
   422    436   
   423    437   			inv:set_size('starlit_suit', 1) -- your environment suit (change at wardrobe)
   424    438   			inv:set_size('starlit_cfg', 1) -- the item you're reconfiguring / container you're accessing
   425    439   
   426    440   			local scenario
   427    441   			for _, e in pairs(starlit.world.scenario) do
   428    442   				if e.id == starlit.world.defaultScenario then
................................................................................
   499    513   			self:updateSuit()
   500    514   			return true
   501    515   		end;
   502    516   		onJoin = function(self)
   503    517   			local me = self.entity
   504    518   			local meta = me:get_meta()
   505    519   			self:pullPersona()
          520  +			self:setModeHand()
   506    521   
   507    522   			-- formspec_version and real_coordinates are apparently just
   508    523   			-- completely ignored here
   509    524   			me:set_formspec_prepend [[
   510    525   				bgcolor[#00000000;true]
   511    526   				style_type[button,button_exit,image_button,item_image_button;border=false]
   512    527   				style_type[button;bgimg=starlit-ui-button-hw.png;bgimg_middle=8;content_offset=0,-2]
................................................................................
   781    796   		give = function(self, item)
   782    797   			local inv = self.entity:get_inventory()
   783    798   			local function is(grp)
   784    799   				return minetest.get_item_group(item:get_name(), grp) ~= 0
   785    800   			end
   786    801   			-- TODO notif popups
   787    802   			if is 'specialInventory' then
          803  +			--[[
   788    804   				if is 'powder' then
   789    805   					if self:naked() then return item end
   790    806   					local cans = inv:get_list 'starlit_suit_canisters'
   791    807   					if cans and next(cans) then for i, st in ipairs(cans) do
   792    808   						local lst = string.format('starlit_canister_%u_elem', i)
   793    809   						item = inv:add_item(lst, item)
   794    810   						if item:is_empty() then break end
   795    811   					end end
   796    812   					self:forSuit(function(x) x:pushCanisters(inv) end)
   797    813   				end
   798    814   				return item
          815  +				]]
   799    816   			else
   800    817   				return inv:add_item('main', item)
   801    818   			end
   802    819   		end;
   803    820   		thrustUpon = function(self, item)
   804    821   			local r = self:give(st)
   805    822   			if not r:is_empty() then
   806    823   				return minetest.add_item(self.entity:get_pos(), r)
   807    824   			end
   808    825   		end;
          826  +		consume = function(self, stack, n)
          827  +			n = n or 1
          828  +			if n == 0 then n = stack:get_count() end
          829  +			local fd = stack:take_item(n)
          830  +			local stats = starlit.world.food.effectiveStats(fd)
          831  +
          832  +			return stack
          833  +		end;
   809    834   	};
   810    835   }
   811    836   
   812    837   local biointerval = 3.0
   813    838   starlit.startJob('starlit:bio', biointerval, function(delta)
   814    839   	for id, u in pairs(starlit.activeUsers) do
          840  +		local p = u.pheno
          841  +		local bmr = p:trait 'metabolism' * biointerval
          842  +		-- TODO apply modifiers
          843  +
          844  +		local dehydration = p:trait 'dehydration' * biointerval
          845  +		-- you dehydrate faster in higher temp
          846  +		dehydration = dehydration * math.max(1, starlit.world.climate.temp(u.entity:get_pos()) / 10)
   815    847   
          848  +		u:statDelta('hunger', bmr)
          849  +		u:statDelta('thirst', dehydration)
   816    850   	end
   817    851   end)
   818    852   
   819    853   local cbit = {
   820    854   	up   = 0x001;
   821    855   	down = 0x002;
   822    856   	left = 0x004;

Modified mods/starlit/world.lua from [830720f731] to [d9bc181e37].

    89     89   end
    90     90   
    91     91   world.ecology.biomes.foreach('starlit:biome-gen', {}, function(id, b)
    92     92   	b.def.name = id
    93     93   	minetest.register_biome(b.def)
    94     94   end)
    95     95   
    96         -world.ecology.biomes.link('starlit:steppe', {
    97         -	nightTempDelta = -30;
    98         -	waterTempDelta = 0;
    99         -	--               W    Sp   Su    Au   W
   100         -	seasonalTemp = {-50, -10, 5, 5, -20, -50};
   101         -	def = {
   102         -		node_top      = 'starlit:greengraze', depth_top = 1;
   103         -		node_filler   = 'starlit:soil',    depth_filler = 4;
   104         -		node_riverbed = 'starlit:sand',  depth_riverbed = 4;
   105         -		y_min = 0;
   106         -		y_max = 512;
   107         -		heat_point = 10;
   108         -		humidity_point = 30;
   109         -	};
   110         -})
   111         -	
   112         -world.ecology.biomes.link('starlit:ocean', {
   113         -	nightTempDelta = -35;
   114         -	waterTempDelta = 5;
   115         -	seasonalTemp = {0}; -- no seasonal variance
   116         -	def = {
   117         -		y_max = 3;
   118         -		y_min = -512;
   119         -		heat_point = 15;
   120         -		humidity_point = 50;
   121         -		node_top    = 'starlit:sand', depth_top    = 1;
   122         -		node_filler = 'starlit:sand', depth_filler = 3;
   123         -	};
   124         -})
           96  +world.ecology.plants.foreach('starlit:plant-gen', {}, function(id, b)
           97  +	local stageCt = #b.stages
           98  +	local function stageID(n)
           99  +		if n == stageCt then return id end
          100  +		return id .. string.format('_stage_%s', n)
          101  +	end
          102  +	b.stageNodes = {}
          103  +	local function regStage(n, st)
          104  +		local base = {
          105  +			description = b.name;
          106  +			drawtype = "plantlike";
          107  +			tiles = { tostring(st.tex) };
          108  +			paramtype = "light";
          109  +			paramtype2 = "meshoptions";
          110  +			walkable = false;
          111  +			buildable_to = true;
          112  +			groups = {
          113  +				plant = 1;
          114  +				plant_grow = stageCt ~= n and 1 or 0;
          115  +			};
          116  +			drop = st.drop;
          117  +			_starlit = {
          118  +				plant = {
          119  +					id = id, stage = n;
          120  +				};
          121  +			};
          122  +		}
          123  +		if st.swap then
          124  +			base.node_dig_prediction = stageID(st.swap)
          125  +			function base.on_dig(pos, node, digger)
          126  +				node.name = stageID(st.swap)
          127  +				minetest.swap_node(pos, node)
          128  +				return true
          129  +			end
          130  +		end
          131  +		return base
          132  +	end
          133  +	for i, v in ipairs(b.stages) do
          134  +		local n = regStage(i, v)
          135  +		b.stageNodes[i] = n
          136  +		minetest.register_node(stageID(i), n)
          137  +	end
          138  +	b.fullyGrown = stageID(stageCt)
          139  +
          140  +	local dec = {
          141  +		deco_type = 'simple';
          142  +		decoration = b.fullyGrown;
          143  +		height = 1;
          144  +		param2 = 0;
          145  +	}
          146  +	for k,v in pairs(b.decoration) do dec[k] = v end
          147  +	b.decoration = minetest.register_decoration(dec)
          148  +end)
   125    149   
   126    150   local toward = lib.math.toward
   127    151   local hfinterval = 1.5
   128    152   starlit.startJob('starlit:heatflow', hfinterval, function(delta)
   129    153   
   130    154   	-- our base thermal conductivity (κ) is measured in °C/°C/s. say the
   131    155   	-- player is in -30°C weather, and has an internal temperature of

Modified mods/vtlib/image.lua from [479480cc25] to [9575362639].

    19     19   					str = '(' .. i.img:render() .. ')^' .. str
    20     20   				end
    21     21   				if str ~= '' then
    22     22   					str = str .. '('
    23     23   					bracket = true
    24     24   				end
    25     25   				str = str .. self.string
    26         -				end
           26  +			end
    27     27   			for _,e in pairs(self.fx) do
    28     28   				str = str .. '^[' .. e
    29     29   				-- be sure to escape ones that take arguments
    30     30   				-- correctly!
    31     31   			end
    32     32   			if bracket then str = str .. ')' end
    33     33   			return str
................................................................................
    82     82   					color = lib.color(color)
    83     83   				end
    84     84   				color = color:to_hsl()
    85     85   			end
    86     86   			return image.change(self, {
    87     87   				fx = lib.tbl.append(self.fx, {
    88     88   					string.format('hsl:%s:%s:%s',
    89         -						color.hue, color.sat*100, color.lum*100)
           89  +						color.hue, color.sat, color.lum)
    90     90   				})
    91     91   			})
    92     92   		end;
    93     93   
    94     94   		rehue = function(self, hue)
    95     95   			return self.shift{hue=hue, sat=0, lum=0}
    96     96   		end;

Modified mods/vtlib/marshal.lua from [ec9d9f2682] to [670a4be42e].

   201    201   	report('defining struct name=%q fields=%s', name, dump(def))
   202    202   	return {
   203    203   		name = name;
   204    204   		enc = function(obj)
   205    205   			local enc = m.streamEncoder()
   206    206   			local n = 0
   207    207   			for k,ty in pairs(def) do n=n+1
          208  +				if obj[k] == nil then error('missing key '..dump(k)..' for type '..ty.name) end
   208    209   				local encoded = ty.enc(obj[k])
   209    210   				enc.push(T.u8.enc(#k), size.enc(#encoded), k, encoded)
   210    211   			end
   211    212   			return size.enc(n) .. enc.peek()
   212    213   		end;
   213    214   		dec = debugger.wrap(function(blob)
   214    215   			if blob == '' then

Modified mods/vtlib/math.lua from [28fc5b216f] to [988785061c].

    22     22   	return dsq / (dist^2)
    23     23   	-- [0,1) == less then
    24     24   	-- 1 == equal
    25     25   	-- >1 == greater than
    26     26   end
    27     27   
    28     28   -- produce an SI expression for a quantity
    29         -fn.si = function(unit, val, full, uncommonScales)
           29  +fn.si = function(unit, val, full, uncommonScales, prec)
    30     30   	if val == 0 then return '0 ' .. unit end
    31     31   	local scales = {
    32     32   		{30, 'Q', 'quetta',true,  'q', 'quecto',true};
    33     33   		{27, 'R', 'ronna', true,  'r', 'ronto', true};
    34     34   		{24, 'Y', 'yotta', true,  'y', 'yocto', true};
    35     35   		{21, 'Z', 'zetta', true,  'z', 'zepto', true};
    36     36   		{18, 'E', 'exa',   true,  'a', 'atto',  true};
................................................................................
    45     45   	for i, s in ipairs(scales) do
    46     46   		local amt, smaj, pmaj, cmaj,
    47     47   		           smin, pmin, cmin = lib.tbl.unpack(s)
    48     48   
    49     49   		if math.abs(val) > 1 then
    50     50   			if uncommonScales or cmaj then
    51     51   				local denom = 10^amt
           52  +				local vd = val/denom
           53  +				if prec then vd = lib.math.trim(vd, prec) end
    52     54   				if math.abs(val) >= (10^(amt)) then
    53     55   					return string.format("%s %s%s",
    54         -						val / denom, (full and pmaj or smaj), unit)
           56  +						vd, (full and pmaj or smaj), unit)
    55     57   				end
    56     58   			end
    57     59   		elseif math.abs(val) < 1 then
    58     60   			if uncommonScales or cmin then
    59     61   				local denom = 10^-amt
           62  +				local vd = val/denom
           63  +				if prec then vd = lib.math.trim(vd, prec) end
    60     64   				if math.abs(val) <= (10^-(amt-1)) then
    61     65   					return string.format("%s %s%s",
    62         -						val / denom, (full and pmin or smin), unit)
           66  +						vd, (full and pmin or smin), unit)
    63     67   				end
    64     68   			end
    65     69   		end
    66     70   	end
    67     71   
    68     72   	return string.format("%s %s", val, unit)
    69     73   end